< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Session.SessionManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Session/SessionManager.cs
Line coverage
22%
Covered lines: 112
Uncovered lines: 392
Coverable lines: 504
Total lines: 2142
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 && !nowPlayingQueue.SequenceEqual(session.NowPlayingQueue))
 460            {
 461                session.NowPlayingQueue = nowPlayingQueue;
 462
 463                var itemIds = Array.ConvertAll(nowPlayingQueue, queue => queue.Id);
 464                session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos(
 465                    _libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }),
 466                    new DtoOptions(true));
 467            }
 468        }
 469
 470        /// <summary>
 471        /// Removes the now playing item id.
 472        /// </summary>
 473        /// <param name="session">The session.</param>
 474        private void RemoveNowPlayingItem(SessionInfo session)
 475        {
 0476            session.NowPlayingItem = null;
 0477            session.FullNowPlayingItem = null;
 0478            session.PlayState = new PlayerStateInfo();
 479
 0480            if (!string.IsNullOrEmpty(session.DeviceId))
 481            {
 0482                ClearTranscodingInfo(session.DeviceId);
 483            }
 0484        }
 485
 486        private static string GetSessionKey(string appName, string deviceId)
 15487            => appName + deviceId;
 488
 489        /// <summary>
 490        /// Gets the connection.
 491        /// </summary>
 492        /// <param name="appName">Type of the client.</param>
 493        /// <param name="appVersion">The app version.</param>
 494        /// <param name="deviceId">The device id.</param>
 495        /// <param name="deviceName">Name of the device.</param>
 496        /// <param name="remoteEndPoint">The remote end point.</param>
 497        /// <param name="user">The user.</param>
 498        /// <returns>SessionInfo.</returns>
 499        private SessionInfo GetSessionInfo(
 500            string appName,
 501            string appVersion,
 502            string deviceId,
 503            string deviceName,
 504            string remoteEndPoint,
 505            User user)
 506        {
 15507            CheckDisposed();
 508
 15509            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 510
 15511            var key = GetSessionKey(appName, deviceId);
 15512            SessionInfo newSession = CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, u
 15513            SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, newSession);
 15514            if (ReferenceEquals(newSession, sessionInfo))
 515            {
 15516                OnSessionStarted(newSession);
 517            }
 518
 15519            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 15520            sessionInfo.UserName = user?.Username;
 15521            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 15522            sessionInfo.RemoteEndPoint = remoteEndPoint;
 15523            sessionInfo.Client = appName;
 524
 15525            if (!sessionInfo.HasCustomDeviceName || string.IsNullOrEmpty(sessionInfo.DeviceName))
 526            {
 15527                sessionInfo.DeviceName = deviceName;
 528            }
 529
 15530            sessionInfo.ApplicationVersion = appVersion;
 531
 15532            if (user is null)
 533            {
 0534                sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>();
 535            }
 536
 15537            return sessionInfo;
 538        }
 539
 540        private SessionInfo CreateSessionInfo(
 541            string key,
 542            string appName,
 543            string appVersion,
 544            string deviceId,
 545            string deviceName,
 546            string remoteEndPoint,
 547            User user)
 548        {
 15549            var sessionInfo = new SessionInfo(this, _logger)
 15550            {
 15551                Client = appName,
 15552                DeviceId = deviceId,
 15553                ApplicationVersion = appVersion,
 15554                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
 15555                ServerId = _appHost.SystemId
 15556            };
 557
 15558            var username = user?.Username;
 559
 15560            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 15561            sessionInfo.UserName = username;
 15562            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 15563            sessionInfo.RemoteEndPoint = remoteEndPoint;
 564
 15565            if (string.IsNullOrEmpty(deviceName))
 566            {
 0567                deviceName = "Network Device";
 568            }
 569
 15570            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new()
 15571            {
 15572                DeviceId = deviceId
 15573            };
 15574            if (string.IsNullOrEmpty(deviceOptions.CustomName))
 575            {
 15576                sessionInfo.DeviceName = deviceName;
 577            }
 578            else
 579            {
 0580                sessionInfo.DeviceName = deviceOptions.CustomName;
 0581                sessionInfo.HasCustomDeviceName = true;
 582            }
 583
 15584            return sessionInfo;
 585        }
 586
 587        private List<User> GetUsers(SessionInfo session)
 588        {
 0589            var users = new List<User>();
 590
 0591            if (session.UserId.IsEmpty())
 592            {
 0593                return users;
 594            }
 595
 0596            var user = _userManager.GetUserById(session.UserId);
 597
 0598            if (user is null)
 599            {
 0600                throw new InvalidOperationException("User not found");
 601            }
 602
 0603            users.Add(user);
 604
 0605            users.AddRange(session.AdditionalUsers
 0606                .Select(i => _userManager.GetUserById(i.UserId))
 0607                .Where(i => i is not null));
 608
 0609            return users;
 610        }
 611
 612        private void StartCheckTimers()
 613        {
 0614            _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
 615
 0616            if (_config.Configuration.InactiveSessionThreshold > 0)
 617            {
 0618                _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes
 619            }
 620            else
 621            {
 0622                StopInactiveCheckTimer();
 623            }
 0624        }
 625
 626        private void StopIdleCheckTimer()
 627        {
 0628            if (_idleTimer is not null)
 629            {
 0630                _idleTimer.Dispose();
 0631                _idleTimer = null;
 632            }
 0633        }
 634
 635        private void StopInactiveCheckTimer()
 636        {
 0637            if (_inactiveTimer is not null)
 638            {
 0639                _inactiveTimer.Dispose();
 0640                _inactiveTimer = null;
 641            }
 0642        }
 643
 644        private async void CheckForIdlePlayback(object state)
 645        {
 646            var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
 647                .ToList();
 648
 649            if (playingSessions.Count > 0)
 650            {
 651                var idle = playingSessions
 652                    .Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5)
 653                    .ToList();
 654
 655                foreach (var session in idle)
 656                {
 657                    _logger.LogDebug("Session {0} has gone idle while playing", session.Id);
 658
 659                    try
 660                    {
 661                        await OnPlaybackStopped(new PlaybackStopInfo
 662                        {
 663                            Item = session.NowPlayingItem,
 664                            ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
 665                            SessionId = session.Id,
 666                            MediaSourceId = session.PlayState?.MediaSourceId,
 667                            PositionTicks = session.PlayState?.PositionTicks
 668                        }).ConfigureAwait(false);
 669                    }
 670                    catch (Exception ex)
 671                    {
 672                        _logger.LogDebug(ex, "Error calling OnPlaybackStopped");
 673                    }
 674                }
 675            }
 676            else
 677            {
 678                StopIdleCheckTimer();
 679            }
 680        }
 681
 682        private async void CheckForInactiveSteams(object state)
 683        {
 684            var inactiveSessions = Sessions.Where(i =>
 685                    i.NowPlayingItem is not null
 686                    && i.PlayState.IsPaused
 687                    && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionTh
 688
 689            foreach (var session in inactiveSessions)
 690            {
 691                _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session
 692
 693                try
 694                {
 695                    await SendPlaystateCommand(
 696                        session.Id,
 697                        session.Id,
 698                        new PlaystateRequest()
 699                        {
 700                            Command = PlaystateCommand.Stop,
 701                            ControllingUserId = session.UserId.ToString(),
 702                            SeekPositionTicks = session.PlayState?.PositionTicks
 703                        },
 704                        CancellationToken.None).ConfigureAwait(true);
 705                }
 706                catch (Exception ex)
 707                {
 708                    _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", 
 709                }
 710            }
 711
 712            bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
 713
 714            if (!playingSessions)
 715            {
 716                StopInactiveCheckTimer();
 717            }
 718        }
 719
 720        private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
 721        {
 0722            if (session is null)
 723            {
 0724                return null;
 725            }
 726
 0727            var item = session.FullNowPlayingItem;
 0728            if (item is not null && item.Id.Equals(itemId))
 729            {
 0730                return item;
 731            }
 732
 0733            item = _libraryManager.GetItemById(itemId);
 734
 0735            session.FullNowPlayingItem = item;
 736
 0737            return item;
 738        }
 739
 740        /// <summary>
 741        /// Used to report that playback has started for an item.
 742        /// </summary>
 743        /// <param name="info">The info.</param>
 744        /// <returns>Task.</returns>
 745        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 746        public async Task OnPlaybackStart(PlaybackStartInfo info)
 747        {
 748            CheckDisposed();
 749
 750            ArgumentNullException.ThrowIfNull(info);
 751
 752            var session = GetSession(info.SessionId);
 753
 754            var libraryItem = info.ItemId.IsEmpty()
 755                ? null
 756                : GetNowPlayingItem(session, info.ItemId);
 757
 758            await UpdateNowPlayingItem(session, info, libraryItem, true).ConfigureAwait(false);
 759
 760            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 761            {
 762                ClearTranscodingInfo(session.DeviceId);
 763            }
 764
 765            session.StartAutomaticProgress(info);
 766
 767            var users = GetUsers(session);
 768
 769            if (libraryItem is not null)
 770            {
 771                foreach (var user in users)
 772                {
 773                    OnPlaybackStart(user, libraryItem);
 774                }
 775            }
 776
 777            if (!string.IsNullOrEmpty(info.LiveStreamId))
 778            {
 779                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 780            }
 781
 782            var eventArgs = new PlaybackStartEventArgs
 783            {
 784                Item = libraryItem,
 785                Users = users,
 786                MediaSourceId = info.MediaSourceId,
 787                MediaInfo = info.Item,
 788                DeviceName = session.DeviceName,
 789                ClientName = session.Client,
 790                DeviceId = session.DeviceId,
 791                Session = session,
 792                PlaybackPositionTicks = info.PositionTicks,
 793                PlaySessionId = info.PlaySessionId
 794            };
 795
 796            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 797
 798            // Nothing to save here
 799            // Fire events to inform plugins
 800            EventHelper.QueueEventIfNotNull(
 801                PlaybackStart,
 802                this,
 803                eventArgs,
 804                _logger);
 805
 806            StartCheckTimers();
 807        }
 808
 809        /// <summary>
 810        /// Called when [playback start].
 811        /// </summary>
 812        /// <param name="user">The user object.</param>
 813        /// <param name="item">The item.</param>
 814        private void OnPlaybackStart(User user, BaseItem item)
 815        {
 0816            var data = _userDataManager.GetUserData(user, item);
 817
 0818            data.PlayCount++;
 0819            data.LastPlayedDate = DateTime.UtcNow;
 820
 0821            if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume)
 822            {
 0823                data.Played = true;
 824            }
 825            else
 826            {
 0827                data.Played = false;
 828            }
 829
 0830            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
 0831        }
 832
 833        /// <inheritdoc />
 834        public Task OnPlaybackProgress(PlaybackProgressInfo info)
 835        {
 0836            return OnPlaybackProgress(info, false);
 837        }
 838
 839        private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId)
 840        {
 0841            var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary<s
 842
 0843            if (!string.IsNullOrEmpty(playSessionId))
 844            {
 0845                if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId 
 846                {
 0847                    if (!string.IsNullOrEmpty(currentPlaySessionId))
 848                    {
 0849                        activeSessionMappings.TryRemove(currentPlaySessionId, out _);
 850                    }
 851
 0852                    activeSessionMappings[sessionId] = playSessionId;
 0853                    activeSessionMappings[playSessionId] = sessionId;
 854                }
 855            }
 856            else
 857            {
 0858                if (!activeSessionMappings.TryGetValue(sessionId, out _))
 859                {
 0860                    activeSessionMappings[sessionId] = string.Empty;
 861                }
 862            }
 0863        }
 864
 865        /// <summary>
 866        /// Used to report playback progress for an item.
 867        /// </summary>
 868        /// <param name="info">The playback progress info.</param>
 869        /// <param name="isAutomated">Whether this is an automated update.</param>
 870        /// <returns>Task.</returns>
 871        public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
 872        {
 873            CheckDisposed();
 874
 875            ArgumentNullException.ThrowIfNull(info);
 876
 877            var session = GetSession(info.SessionId, false);
 878            if (session is null)
 879            {
 880                return;
 881            }
 882
 883            var libraryItem = info.ItemId.IsEmpty()
 884                ? null
 885                : GetNowPlayingItem(session, info.ItemId);
 886
 887            await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
 888
 889            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 890            {
 891                ClearTranscodingInfo(session.DeviceId);
 892            }
 893
 894            var users = GetUsers(session);
 895
 896            // only update saved user data on actual check-ins, not automated ones
 897            if (libraryItem is not null && !isAutomated)
 898            {
 899                foreach (var user in users)
 900                {
 901                    OnPlaybackProgress(user, libraryItem, info);
 902                }
 903            }
 904
 905            if (!string.IsNullOrEmpty(info.LiveStreamId))
 906            {
 907                UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId);
 908            }
 909
 910            var eventArgs = new PlaybackProgressEventArgs
 911            {
 912                Item = libraryItem,
 913                Users = users,
 914                PlaybackPositionTicks = session.PlayState.PositionTicks,
 915                MediaSourceId = session.PlayState.MediaSourceId,
 916                MediaInfo = info.Item,
 917                DeviceName = session.DeviceName,
 918                ClientName = session.Client,
 919                DeviceId = session.DeviceId,
 920                IsPaused = info.IsPaused,
 921                PlaySessionId = info.PlaySessionId,
 922                IsAutomated = isAutomated,
 923                Session = session
 924            };
 925
 926            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 927
 928            PlaybackProgress?.Invoke(this, eventArgs);
 929
 930            if (!isAutomated)
 931            {
 932                session.StartAutomaticProgress(info);
 933            }
 934
 935            StartCheckTimers();
 936        }
 937
 938        private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
 939        {
 0940            var data = _userDataManager.GetUserData(user, item);
 941
 0942            var positionTicks = info.PositionTicks;
 943
 0944            var changed = false;
 945
 0946            if (positionTicks.HasValue)
 947            {
 0948                _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 0949                changed = true;
 950            }
 951
 0952            var tracksChanged = UpdatePlaybackSettings(user, info, data);
 0953            if (!tracksChanged)
 954            {
 0955                changed = true;
 956            }
 957
 0958            if (changed)
 959            {
 0960                _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.N
 961            }
 0962        }
 963
 964        private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
 965        {
 0966            var changed = false;
 967
 0968            if (user.RememberAudioSelections)
 969            {
 0970                if (data.AudioStreamIndex != info.AudioStreamIndex)
 971                {
 0972                    data.AudioStreamIndex = info.AudioStreamIndex;
 0973                    changed = true;
 974                }
 975            }
 976            else
 977            {
 0978                if (data.AudioStreamIndex.HasValue)
 979                {
 0980                    data.AudioStreamIndex = null;
 0981                    changed = true;
 982                }
 983            }
 984
 0985            if (user.RememberSubtitleSelections)
 986            {
 0987                if (data.SubtitleStreamIndex != info.SubtitleStreamIndex)
 988                {
 0989                    data.SubtitleStreamIndex = info.SubtitleStreamIndex;
 0990                    changed = true;
 991                }
 992            }
 993            else
 994            {
 0995                if (data.SubtitleStreamIndex.HasValue)
 996                {
 0997                    data.SubtitleStreamIndex = null;
 0998                    changed = true;
 999                }
 1000            }
 1001
 01002            return changed;
 1003        }
 1004
 1005        /// <summary>
 1006        /// Used to report that playback has ended for an item.
 1007        /// </summary>
 1008        /// <param name="info">The info.</param>
 1009        /// <returns>Task.</returns>
 1010        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 1011        /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</excepti
 1012        public async Task OnPlaybackStopped(PlaybackStopInfo info)
 1013        {
 1014            CheckDisposed();
 1015
 1016            ArgumentNullException.ThrowIfNull(info);
 1017
 1018            if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
 1019            {
 1020                throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."
 1021            }
 1022
 1023            var session = GetSession(info.SessionId);
 1024
 1025            session.StopAutomaticProgress();
 1026
 1027            var libraryItem = info.ItemId.IsEmpty()
 1028                ? null
 1029                : GetNowPlayingItem(session, info.ItemId);
 1030
 1031            // Normalize
 1032            if (string.IsNullOrEmpty(info.MediaSourceId))
 1033            {
 1034                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 1035            }
 1036
 1037            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 1038            {
 1039                var current = session.NowPlayingItem;
 1040
 1041                if (current is null || !info.ItemId.Equals(current.Id))
 1042                {
 1043                    MediaSourceInfo mediaSource = null;
 1044
 1045                    if (libraryItem is IHasMediaSources)
 1046                    {
 1047                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 1048                    }
 1049
 1050                    info.Item = GetItemInfo(libraryItem, mediaSource);
 1051                }
 1052                else
 1053                {
 1054                    info.Item = current;
 1055                }
 1056            }
 1057
 1058            if (info.Item is not null)
 1059            {
 1060                var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.Inv
 1061
 1062                _logger.LogInformation(
 1063                    "Playback stopped reported by app {0} {1} playing {2}. Stopped at {3} ms",
 1064                    session.Client,
 1065                    session.ApplicationVersion,
 1066                    info.Item.Name,
 1067                    msString);
 1068            }
 1069
 1070            if (info.NowPlayingQueue is not null)
 1071            {
 1072                session.NowPlayingQueue = info.NowPlayingQueue;
 1073            }
 1074
 1075            session.PlaylistItemId = info.PlaylistItemId;
 1076
 1077            RemoveNowPlayingItem(session);
 1078
 1079            var users = GetUsers(session);
 1080            var playedToCompletion = false;
 1081
 1082            if (libraryItem is not null)
 1083            {
 1084                foreach (var user in users)
 1085                {
 1086                    playedToCompletion = OnPlaybackStopped(user, libraryItem, info.PositionTicks, info.Failed);
 1087                }
 1088            }
 1089
 1090            if (!string.IsNullOrEmpty(info.LiveStreamId))
 1091            {
 1092                await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false);
 1093            }
 1094
 1095            var eventArgs = new PlaybackStopEventArgs
 1096            {
 1097                Item = libraryItem,
 1098                Users = users,
 1099                PlaybackPositionTicks = info.PositionTicks,
 1100                PlayedToCompletion = playedToCompletion,
 1101                MediaSourceId = info.MediaSourceId,
 1102                MediaInfo = info.Item,
 1103                DeviceName = session.DeviceName,
 1104                ClientName = session.Client,
 1105                DeviceId = session.DeviceId,
 1106                Session = session,
 1107                PlaySessionId = info.PlaySessionId
 1108            };
 1109
 1110            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 1111
 1112            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
 1113        }
 1114
 1115        private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
 1116        {
 01117            if (playbackFailed)
 1118            {
 01119                return false;
 1120            }
 1121
 01122            var data = _userDataManager.GetUserData(user, item);
 1123            bool playedToCompletion;
 01124            if (positionTicks.HasValue)
 1125            {
 01126                playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 1127            }
 1128            else
 1129            {
 1130                // If the client isn't able to report this, then we'll just have to make an assumption
 01131                data.PlayCount++;
 01132                data.Played = item.SupportsPlayedStatus;
 01133                data.PlaybackPositionTicks = 0;
 01134                playedToCompletion = true;
 1135            }
 1136
 01137            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None)
 1138
 01139            return playedToCompletion;
 1140        }
 1141
 1142        /// <summary>
 1143        /// Gets the session.
 1144        /// </summary>
 1145        /// <param name="sessionId">The session identifier.</param>
 1146        /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
 1147        /// <returns>SessionInfo.</returns>
 1148        /// <exception cref="ResourceNotFoundException">
 1149        /// No session with an Id equal to <c>sessionId</c> was found
 1150        /// and <c>throwOnMissing</c> is <c>true</c>.
 1151        /// </exception>
 1152        private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
 1153        {
 01154            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 01155            if (session is null && throwOnMissing)
 1156            {
 01157                throw new ResourceNotFoundException(
 01158                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1159            }
 1160
 01161            return session;
 1162        }
 1163
 1164        private SessionInfo GetSessionToRemoteControl(string sessionId)
 1165        {
 1166            // Accept either device id or session id
 01167            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 1168
 01169            if (session is null)
 1170            {
 01171                throw new ResourceNotFoundException(
 01172                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1173            }
 1174
 01175            return session;
 1176        }
 1177
 1178        private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo)
 1179        {
 151180            return new SessionInfoDto
 151181            {
 151182                PlayState = sessionInfo.PlayState,
 151183                AdditionalUsers = sessionInfo.AdditionalUsers,
 151184                Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities),
 151185                RemoteEndPoint = sessionInfo.RemoteEndPoint,
 151186                PlayableMediaTypes = sessionInfo.PlayableMediaTypes,
 151187                Id = sessionInfo.Id,
 151188                UserId = sessionInfo.UserId,
 151189                UserName = sessionInfo.UserName,
 151190                Client = sessionInfo.Client,
 151191                LastActivityDate = sessionInfo.LastActivityDate,
 151192                LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn,
 151193                LastPausedDate = sessionInfo.LastPausedDate,
 151194                DeviceName = sessionInfo.DeviceName,
 151195                DeviceType = sessionInfo.DeviceType,
 151196                NowPlayingItem = sessionInfo.NowPlayingItem,
 151197                NowViewingItem = sessionInfo.NowViewingItem,
 151198                DeviceId = sessionInfo.DeviceId,
 151199                ApplicationVersion = sessionInfo.ApplicationVersion,
 151200                TranscodingInfo = sessionInfo.TranscodingInfo,
 151201                IsActive = sessionInfo.IsActive,
 151202                SupportsMediaControl = sessionInfo.SupportsMediaControl,
 151203                SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
 151204                NowPlayingQueue = sessionInfo.NowPlayingQueue,
 151205                NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems,
 151206                HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
 151207                PlaylistItemId = sessionInfo.PlaylistItemId,
 151208                ServerId = sessionInfo.ServerId,
 151209                UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag,
 151210                SupportedCommands = sessionInfo.SupportedCommands
 151211            };
 1212        }
 1213
 1214        /// <inheritdoc />
 1215        public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, Cancellati
 1216        {
 01217            CheckDisposed();
 1218
 01219            var generalCommand = new GeneralCommand
 01220            {
 01221                Name = GeneralCommandType.DisplayMessage
 01222            };
 1223
 01224            generalCommand.Arguments["Header"] = command.Header;
 01225            generalCommand.Arguments["Text"] = command.Text;
 1226
 01227            if (command.TimeoutMs.HasValue)
 1228            {
 01229                generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture);
 1230            }
 1231
 01232            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1233        }
 1234
 1235        /// <inheritdoc />
 1236        public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, Cancellati
 1237        {
 01238            CheckDisposed();
 1239
 01240            var session = GetSessionToRemoteControl(sessionId);
 1241
 01242            if (!string.IsNullOrEmpty(controllingSessionId))
 1243            {
 01244                var controllingSession = GetSession(controllingSessionId);
 01245                AssertCanControl(session, controllingSession);
 1246            }
 1247
 01248            return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
 1249        }
 1250
 1251        private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, Cancella
 1252        {
 1253            var controllers = session.SessionControllers;
 1254            var messageId = Guid.NewGuid();
 1255
 1256            foreach (var controller in controllers)
 1257            {
 1258                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
 1259            }
 1260        }
 1261
 1262        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data,
 1263        {
 1264            IEnumerable<Task> GetTasks()
 1265            {
 1266                var messageId = Guid.NewGuid();
 1267                foreach (var session in sessions)
 1268                {
 1269                    var controllers = session.SessionControllers;
 1270                    foreach (var controller in controllers)
 1271                    {
 1272                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
 1273                    }
 1274                }
 1275            }
 1276
 211277            return Task.WhenAll(GetTasks());
 1278        }
 1279
 1280        /// <inheritdoc />
 1281        public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, Cancellati
 1282        {
 1283            CheckDisposed();
 1284
 1285            var session = GetSessionToRemoteControl(sessionId);
 1286
 1287            var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
 1288
 1289            List<BaseItem> items;
 1290
 1291            if (command.PlayCommand == PlayCommand.PlayInstantMix)
 1292            {
 1293                items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
 1294                    .ToList();
 1295
 1296                command.PlayCommand = PlayCommand.PlayNow;
 1297            }
 1298            else
 1299            {
 1300                var list = new List<BaseItem>();
 1301                foreach (var itemId in command.ItemIds)
 1302                {
 1303                    var subItems = TranslateItemForPlayback(itemId, user);
 1304                    list.AddRange(subItems);
 1305                }
 1306
 1307                items = list;
 1308            }
 1309
 1310            if (command.PlayCommand == PlayCommand.PlayShuffle)
 1311            {
 1312                items.Shuffle();
 1313                command.PlayCommand = PlayCommand.PlayNow;
 1314            }
 1315
 1316            command.ItemIds = items.Select(i => i.Id).ToArray();
 1317
 1318            if (user is not null)
 1319            {
 1320                if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
 1321                {
 1322                    throw new ArgumentException(
 1323                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username))
 1324                }
 1325            }
 1326
 1327            if (user is not null
 1328                && command.ItemIds.Length == 1
 1329                && user.EnableNextEpisodeAutoPlay
 1330                && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
 1331            {
 1332                var series = episode.Series;
 1333                if (series is not null)
 1334                {
 1335                    var episodes = series.GetEpisodes(
 1336                            user,
 1337                            new DtoOptions(false)
 1338                            {
 1339                                EnableImages = false
 1340                            },
 1341                            user.DisplayMissingEpisodes)
 1342                        .Where(i => !i.IsVirtualItem)
 1343                        .SkipWhile(i => !i.Id.Equals(episode.Id))
 1344                        .ToList();
 1345
 1346                    if (episodes.Count > 0)
 1347                    {
 1348                        command.ItemIds = episodes.Select(i => i.Id).ToArray();
 1349                    }
 1350                }
 1351            }
 1352
 1353            if (!string.IsNullOrEmpty(controllingSessionId))
 1354            {
 1355                var controllingSession = GetSession(controllingSessionId);
 1356                AssertCanControl(session, controllingSession);
 1357                if (!controllingSession.UserId.IsEmpty())
 1358                {
 1359                    command.ControllingUserId = controllingSession.UserId;
 1360                }
 1361            }
 1362
 1363            await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(fals
 1364        }
 1365
 1366        /// <inheritdoc />
 1367        public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken
 1368        {
 1369            CheckDisposed();
 1370            var session = GetSession(sessionId);
 1371            await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).Configur
 1372        }
 1373
 1374        /// <inheritdoc />
 1375        public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancell
 1376        {
 1377            CheckDisposed();
 1378            var session = GetSession(sessionId);
 1379            await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).Conf
 1380        }
 1381
 1382        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
 1383        {
 01384            var item = _libraryManager.GetItemById(id);
 1385
 01386            if (item is null)
 1387            {
 01388                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForPlayback", id);
 01389                return Array.Empty<BaseItem>();
 1390            }
 1391
 01392            if (item is IItemByName byName)
 1393            {
 01394                return byName.GetTaggedItems(new InternalItemsQuery(user)
 01395                {
 01396                    IsFolder = false,
 01397                    Recursive = true,
 01398                    DtoOptions = new DtoOptions(false)
 01399                    {
 01400                        EnableImages = false,
 01401                        Fields = new[]
 01402                        {
 01403                            ItemFields.SortName
 01404                        }
 01405                    },
 01406                    IsVirtualItem = false,
 01407                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01408                });
 1409            }
 1410
 01411            if (item.IsFolder)
 1412            {
 01413                var folder = (Folder)item;
 1414
 01415                return folder.GetItemList(new InternalItemsQuery(user)
 01416                {
 01417                    Recursive = true,
 01418                    IsFolder = false,
 01419                    DtoOptions = new DtoOptions(false)
 01420                    {
 01421                        EnableImages = false,
 01422                        Fields = new ItemFields[]
 01423                        {
 01424                            ItemFields.SortName
 01425                        }
 01426                    },
 01427                    IsVirtualItem = false,
 01428                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01429                });
 1430            }
 1431
 01432            return new[] { item };
 1433        }
 1434
 1435        private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
 1436        {
 01437            var item = _libraryManager.GetItemById(id);
 1438
 01439            if (item is null)
 1440            {
 01441                _logger.LogError("A nonexistent item Id {0} was passed into TranslateItemForInstantMix", id);
 01442                return new List<BaseItem>();
 1443            }
 1444
 01445            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToLis
 1446        }
 1447
 1448        /// <inheritdoc />
 1449        public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, Cancellation
 1450        {
 01451            var generalCommand = new GeneralCommand
 01452            {
 01453                Name = GeneralCommandType.DisplayContent,
 01454                Arguments =
 01455                {
 01456                    ["ItemId"] = command.ItemId,
 01457                    ["ItemName"] = command.ItemName,
 01458                    ["ItemType"] = command.ItemType.ToString()
 01459                }
 01460            };
 1461
 01462            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1463        }
 1464
 1465        /// <inheritdoc />
 1466        public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, Cancel
 1467        {
 01468            CheckDisposed();
 1469
 01470            var session = GetSessionToRemoteControl(sessionId);
 1471
 01472            if (!string.IsNullOrEmpty(controllingSessionId))
 1473            {
 01474                var controllingSession = GetSession(controllingSessionId);
 01475                AssertCanControl(session, controllingSession);
 01476                if (!controllingSession.UserId.IsEmpty())
 1477                {
 01478                    command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
 1479                }
 1480            }
 1481
 01482            return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
 1483        }
 1484
 1485        private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
 1486        {
 01487            ArgumentNullException.ThrowIfNull(session);
 1488
 01489            ArgumentNullException.ThrowIfNull(controllingSession);
 01490        }
 1491
 1492        /// <summary>
 1493        /// Sends the restart required message.
 1494        /// </summary>
 1495        /// <param name="cancellationToken">The cancellation token.</param>
 1496        /// <returns>Task.</returns>
 1497        public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
 1498        {
 01499            CheckDisposed();
 1500
 01501            return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
 1502        }
 1503
 1504        /// <summary>
 1505        /// Adds the additional user.
 1506        /// </summary>
 1507        /// <param name="sessionId">The session identifier.</param>
 1508        /// <param name="userId">The user identifier.</param>
 1509        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1510        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1511        public void AddAdditionalUser(string sessionId, Guid userId)
 1512        {
 01513            CheckDisposed();
 1514
 01515            var session = GetSession(sessionId);
 1516
 01517            if (session.UserId.Equals(userId))
 1518            {
 01519                throw new ArgumentException("The requested user is already the primary user of the session.");
 1520            }
 1521
 01522            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
 1523            {
 01524                var user = _userManager.GetUserById(userId);
 01525                var newUser = new SessionUserInfo
 01526                {
 01527                    UserId = userId,
 01528                    UserName = user.Username
 01529                };
 1530
 01531                session.AdditionalUsers = [.. session.AdditionalUsers, newUser];
 1532            }
 01533        }
 1534
 1535        /// <summary>
 1536        /// Removes the additional user.
 1537        /// </summary>
 1538        /// <param name="sessionId">The session identifier.</param>
 1539        /// <param name="userId">The user identifier.</param>
 1540        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1541        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1542        public void RemoveAdditionalUser(string sessionId, Guid userId)
 1543        {
 01544            CheckDisposed();
 1545
 01546            var session = GetSession(sessionId);
 1547
 01548            if (session.UserId.Equals(userId))
 1549            {
 01550                throw new ArgumentException("The requested user is already the primary user of the session.");
 1551            }
 1552
 01553            var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId));
 1554
 01555            if (user is not null)
 1556            {
 01557                var list = session.AdditionalUsers.ToList();
 01558                list.Remove(user);
 1559
 01560                session.AdditionalUsers = list.ToArray();
 1561            }
 01562        }
 1563
 1564        /// <summary>
 1565        /// Authenticates the new session.
 1566        /// </summary>
 1567        /// <param name="request">The authenticationrequest.</param>
 1568        /// <returns>The authentication result.</returns>
 1569        public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
 1570        {
 151571            return AuthenticateNewSessionInternal(request, true);
 1572        }
 1573
 1574        /// <summary>
 1575        /// Directly authenticates the session without enforcing password.
 1576        /// </summary>
 1577        /// <param name="request">The authentication request.</param>
 1578        /// <returns>The authentication result.</returns>
 1579        public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
 1580        {
 01581            return AuthenticateNewSessionInternal(request, false);
 1582        }
 1583
 1584        internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enf
 1585        {
 1586            CheckDisposed();
 1587
 1588            ArgumentException.ThrowIfNullOrEmpty(request.App);
 1589            ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
 1590            ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
 1591            ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
 1592
 1593            User user = null;
 1594            if (!request.UserId.IsEmpty())
 1595            {
 1596                user = _userManager.GetUserById(request.UserId);
 1597            }
 1598
 1599            user ??= _userManager.GetUserByName(request.Username);
 1600
 1601            if (enforcePassword)
 1602            {
 1603                user = await _userManager.AuthenticateUser(
 1604                    request.Username,
 1605                    request.Password,
 1606                    request.RemoteEndPoint,
 1607                    true).ConfigureAwait(false);
 1608            }
 1609
 1610            if (user is null)
 1611            {
 1612                await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
 1613                throw new AuthenticationException("Invalid username or password entered.");
 1614            }
 1615
 1616            if (!string.IsNullOrEmpty(request.DeviceId)
 1617                && !_deviceManager.CanAccessDevice(user, request.DeviceId))
 1618            {
 1619                throw new SecurityException("User is not allowed access from this device.");
 1620            }
 1621
 1622            int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
 1623            int maxActiveSessions = user.MaxActiveSessions;
 1624            _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCoun
 1625            if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
 1626            {
 1627                throw new SecurityException("User is at their maximum number of sessions.");
 1628            }
 1629
 1630            var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.Dev
 1631
 1632            var session = await LogSessionActivity(
 1633                request.App,
 1634                request.AppVersion,
 1635                request.DeviceId,
 1636                request.DeviceName,
 1637                request.RemoteEndPoint,
 1638                user).ConfigureAwait(false);
 1639
 1640            var returnResult = new AuthenticationResult
 1641            {
 1642                User = _userManager.GetUserDto(user, request.RemoteEndPoint),
 1643                SessionInfo = ToSessionInfoDto(session),
 1644                AccessToken = token,
 1645                ServerId = _appHost.SystemId
 1646            };
 1647
 1648            await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
 1649            return returnResult;
 1650        }
 1651
 1652        internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, str
 1653        {
 1654            // This should be validated above, but if it isn't don't delete all tokens.
 1655            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 1656
 1657            var existing = _deviceManager.GetDevices(
 1658                new DeviceQuery
 1659                {
 1660                    DeviceId = deviceId,
 1661                    UserId = user.Id
 1662                }).Items;
 1663
 1664            foreach (var auth in existing)
 1665            {
 1666                try
 1667                {
 1668                    // Logout any existing sessions for the user on this device
 1669                    await Logout(auth).ConfigureAwait(false);
 1670                }
 1671                catch (Exception ex)
 1672                {
 1673                    _logger.LogError(ex, "Error while logging out existing session.");
 1674                }
 1675            }
 1676
 1677            _logger.LogInformation("Creating new access token for user {0}", user.Id);
 1678            var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).C
 1679
 1680            return device.AccessToken;
 1681        }
 1682
 1683        /// <inheritdoc />
 1684        public async Task Logout(string accessToken)
 1685        {
 1686            CheckDisposed();
 1687
 1688            ArgumentException.ThrowIfNullOrEmpty(accessToken);
 1689
 1690            var existing = _deviceManager.GetDevices(
 1691                new DeviceQuery
 1692                {
 1693                    Limit = 1,
 1694                    AccessToken = accessToken
 1695                }).Items;
 1696
 1697            if (existing.Count > 0)
 1698            {
 1699                await Logout(existing[0]).ConfigureAwait(false);
 1700            }
 1701        }
 1702
 1703        /// <inheritdoc />
 1704        public async Task Logout(Device device)
 1705        {
 1706            CheckDisposed();
 1707
 1708            _logger.LogInformation("Logging out access token {0}", device.AccessToken);
 1709
 1710            await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
 1711
 1712            var sessions = Sessions
 1713                .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
 1714                .ToList();
 1715
 1716            foreach (var session in sessions)
 1717            {
 1718                try
 1719                {
 1720                    await ReportSessionEnded(session.Id).ConfigureAwait(false);
 1721                }
 1722                catch (Exception ex)
 1723                {
 1724                    _logger.LogError(ex, "Error reporting session ended");
 1725                }
 1726            }
 1727        }
 1728
 1729        /// <inheritdoc />
 1730        public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
 1731        {
 1732            CheckDisposed();
 1733
 1734            var existing = _deviceManager.GetDevices(new DeviceQuery
 1735            {
 1736                UserId = userId
 1737            });
 1738
 1739            foreach (var info in existing.Items)
 1740            {
 1741                if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
 1742                {
 1743                    await Logout(info).ConfigureAwait(false);
 1744                }
 1745            }
 1746        }
 1747
 1748        /// <summary>
 1749        /// Reports the capabilities.
 1750        /// </summary>
 1751        /// <param name="sessionId">The session identifier.</param>
 1752        /// <param name="capabilities">The capabilities.</param>
 1753        public void ReportCapabilities(string sessionId, ClientCapabilities capabilities)
 1754        {
 01755            CheckDisposed();
 1756
 01757            var session = GetSession(sessionId);
 1758
 01759            ReportCapabilities(session, capabilities, true);
 01760        }
 1761
 1762        private void ReportCapabilities(
 1763            SessionInfo session,
 1764            ClientCapabilities capabilities,
 1765            bool saveCapabilities)
 1766        {
 151767            session.Capabilities = capabilities;
 1768
 151769            if (saveCapabilities)
 1770            {
 01771                CapabilitiesChanged?.Invoke(
 01772                    this,
 01773                    new SessionEventArgs
 01774                    {
 01775                        SessionInfo = session
 01776                    });
 1777
 01778                _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
 1779            }
 151780        }
 1781
 1782        /// <summary>
 1783        /// Converts a BaseItem to a BaseItemInfo.
 1784        /// </summary>
 1785        private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
 1786        {
 01787            ArgumentNullException.ThrowIfNull(item);
 1788
 01789            var dtoOptions = _itemInfoDtoOptions;
 1790
 01791            if (_itemInfoDtoOptions is null)
 1792            {
 01793                dtoOptions = new DtoOptions
 01794                {
 01795                    AddProgramRecordingInfo = false
 01796                };
 1797
 01798                var fields = dtoOptions.Fields.ToList();
 1799
 01800                fields.Remove(ItemFields.CanDelete);
 01801                fields.Remove(ItemFields.CanDownload);
 01802                fields.Remove(ItemFields.ChildCount);
 01803                fields.Remove(ItemFields.CustomRating);
 01804                fields.Remove(ItemFields.DateLastMediaAdded);
 01805                fields.Remove(ItemFields.DateLastRefreshed);
 01806                fields.Remove(ItemFields.DateLastSaved);
 01807                fields.Remove(ItemFields.DisplayPreferencesId);
 01808                fields.Remove(ItemFields.Etag);
 01809                fields.Remove(ItemFields.ItemCounts);
 01810                fields.Remove(ItemFields.MediaSourceCount);
 01811                fields.Remove(ItemFields.MediaStreams);
 01812                fields.Remove(ItemFields.MediaSources);
 01813                fields.Remove(ItemFields.People);
 01814                fields.Remove(ItemFields.PlayAccess);
 01815                fields.Remove(ItemFields.People);
 01816                fields.Remove(ItemFields.ProductionLocations);
 01817                fields.Remove(ItemFields.RecursiveItemCount);
 01818                fields.Remove(ItemFields.RemoteTrailers);
 01819                fields.Remove(ItemFields.SeasonUserData);
 01820                fields.Remove(ItemFields.Settings);
 01821                fields.Remove(ItemFields.SortName);
 01822                fields.Remove(ItemFields.Tags);
 01823                fields.Remove(ItemFields.ExtraIds);
 1824
 01825                dtoOptions.Fields = fields.ToArray();
 1826
 01827                _itemInfoDtoOptions = dtoOptions;
 1828            }
 1829
 01830            var info = _dtoService.GetBaseItemDto(item, dtoOptions);
 1831
 01832            if (mediaSource is not null)
 1833            {
 01834                info.MediaStreams = mediaSource.MediaStreams.ToArray();
 1835            }
 1836
 01837            return info;
 1838        }
 1839
 1840        private string GetImageCacheTag(User user)
 1841        {
 1842            try
 1843            {
 01844                return _imageProcessor.GetImageCacheTag(user);
 1845            }
 01846            catch (Exception e)
 1847            {
 01848                _logger.LogError(e, "Error getting image information for profile image");
 01849                return null;
 1850            }
 01851        }
 1852
 1853        /// <inheritdoc />
 1854        public void ReportNowViewingItem(string sessionId, string itemId)
 1855        {
 01856            ArgumentException.ThrowIfNullOrEmpty(itemId);
 1857
 01858            var item = _libraryManager.GetItemById(new Guid(itemId));
 01859            var session = GetSession(sessionId);
 1860
 01861            session.NowViewingItem = GetItemInfo(item, null);
 01862        }
 1863
 1864        /// <inheritdoc />
 1865        public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
 1866        {
 01867            var session = Sessions.FirstOrDefault(i =>
 01868                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1869
 01870            if (session is not null)
 1871            {
 01872                session.TranscodingInfo = info;
 1873            }
 01874        }
 1875
 1876        /// <inheritdoc />
 1877        public void ClearTranscodingInfo(string deviceId)
 1878        {
 01879            ReportTranscodingInfo(deviceId, null);
 01880        }
 1881
 1882        /// <inheritdoc />
 1883        public SessionInfo GetSession(string deviceId, string client, string version)
 1884        {
 01885            return Sessions.FirstOrDefault(i =>
 01886                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
 01887                    && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
 1888        }
 1889
 1890        /// <inheritdoc />
 1891        public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, st
 1892        {
 01893            ArgumentNullException.ThrowIfNull(info);
 1894
 01895            var user = info.UserId.IsEmpty()
 01896                ? null
 01897                : _userManager.GetUserById(info.UserId);
 1898
 01899            appVersion = string.IsNullOrEmpty(appVersion)
 01900                ? info.AppVersion
 01901                : appVersion;
 1902
 01903            var deviceName = info.DeviceName;
 01904            var appName = info.AppName;
 1905
 01906            if (string.IsNullOrEmpty(deviceId))
 1907            {
 01908                deviceId = info.DeviceId;
 1909            }
 1910
 1911            // Prevent argument exception
 01912            if (string.IsNullOrEmpty(appVersion))
 1913            {
 01914                appVersion = "1";
 1915            }
 1916
 01917            return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
 1918        }
 1919
 1920        /// <inheritdoc />
 1921        public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpo
 1922        {
 1923            var items = _deviceManager.GetDevices(new DeviceQuery
 1924            {
 1925                AccessToken = token,
 1926                Limit = 1
 1927            }).Items;
 1928
 1929            if (items.Count == 0)
 1930            {
 1931                return null;
 1932            }
 1933
 1934            return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false)
 1935        }
 1936
 1937        /// <inheritdoc/>
 1938        public IReadOnlyList<SessionInfoDto> GetSessions(
 1939            Guid userId,
 1940            string deviceId,
 1941            int? activeWithinSeconds,
 1942            Guid? controllableUserToCheck,
 1943            bool isApiKey)
 1944        {
 01945            var result = Sessions;
 01946            if (!string.IsNullOrEmpty(deviceId))
 1947            {
 01948                result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1949            }
 1950
 01951            var userCanControlOthers = false;
 01952            var userIsAdmin = false;
 01953            User user = null;
 1954
 01955            if (isApiKey)
 1956            {
 01957                userCanControlOthers = true;
 01958                userIsAdmin = true;
 1959            }
 01960            else if (!userId.IsEmpty())
 1961            {
 01962                user = _userManager.GetUserById(userId);
 01963                if (user is not null)
 1964                {
 01965                    userCanControlOthers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers);
 01966                    userIsAdmin = user.HasPermission(PermissionKind.IsAdministrator);
 1967                }
 1968                else
 1969                {
 01970                    return [];
 1971                }
 1972            }
 1973
 01974            if (!controllableUserToCheck.IsNullOrEmpty())
 1975            {
 01976                result = result.Where(i => i.SupportsRemoteControl);
 1977
 01978                var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value);
 01979                if (controlledUser is null)
 1980                {
 01981                    return [];
 1982                }
 1983
 01984                if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl))
 1985                {
 1986                    // Controlled user has device sharing disabled
 01987                    result = result.Where(i => !i.UserId.IsEmpty());
 1988                }
 1989
 01990                if (!userCanControlOthers)
 1991                {
 1992                    // User cannot control other user's sessions, validate user id.
 01993                    result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 1994                }
 1995
 01996                result = result.Where(i =>
 01997                {
 01998                    if (isApiKey)
 01999                    {
 02000                        return true;
 02001                    }
 02002
 02003                    if (user is null)
 02004                    {
 02005                        return false;
 02006                    }
 02007
 02008                    return string.IsNullOrWhiteSpace(i.DeviceId) || _deviceManager.CanAccessDevice(user, i.DeviceId);
 02009                });
 2010            }
 02011            else if (!userIsAdmin)
 2012            {
 2013                // Request isn't from administrator, limit to "own" sessions.
 02014                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId));
 2015            }
 2016
 02017            if (!userIsAdmin)
 2018            {
 2019                // Don't report acceleration type for non-admin users.
 02020                result = result.Select(r =>
 02021                {
 02022                    if (r.TranscodingInfo is not null)
 02023                    {
 02024                        r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
 02025                    }
 02026
 02027                    return r;
 02028                });
 2029            }
 2030
 02031            if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
 2032            {
 02033                var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
 02034                result = result.Where(i => i.LastActivityDate >= minActiveDate);
 2035            }
 2036
 02037            return result.Select(ToSessionInfoDto).ToList();
 2038        }
 2039
 2040        /// <inheritdoc />
 2041        public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
 2042        {
 02043            CheckDisposed();
 2044
 02045            var adminUserIds = _userManager.Users
 02046                .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
 02047                .Select(i => i.Id)
 02048                .ToList();
 2049
 02050            return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
 2051        }
 2052
 2053        /// <inheritdoc />
 2054        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, Cancellati
 2055        {
 02056            CheckDisposed();
 2057
 02058            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
 2059
 02060            if (sessions.Count == 0)
 2061            {
 02062                return Task.CompletedTask;
 2063            }
 2064
 02065            return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
 2066        }
 2067
 2068        /// <inheritdoc />
 2069        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken 
 2070        {
 02071            CheckDisposed();
 2072
 02073            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
 02074            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2075        }
 2076
 2077        /// <inheritdoc />
 2078        public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationTok
 2079        {
 02080            CheckDisposed();
 2081
 02082            var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 2083
 02084            return SendMessageToSessions(sessions, name, data, cancellationToken);
 2085        }
 2086
 2087        /// <inheritdoc />
 2088        public async ValueTask DisposeAsync()
 2089        {
 2090            if (_disposed)
 2091            {
 2092                return;
 2093            }
 2094
 2095            foreach (var session in _activeConnections.Values)
 2096            {
 2097                await session.DisposeAsync().ConfigureAwait(false);
 2098            }
 2099
 2100            if (_idleTimer is not null)
 2101            {
 2102                await _idleTimer.DisposeAsync().ConfigureAwait(false);
 2103                _idleTimer = null;
 2104            }
 2105
 2106            if (_inactiveTimer is not null)
 2107            {
 2108                await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
 2109                _inactiveTimer = null;
 2110            }
 2111
 2112            await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
 2113
 2114            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 2115            _disposed = true;
 2116        }
 2117
 2118        private async void OnApplicationStopping()
 2119        {
 2120            _logger.LogInformation("Sending shutdown notifications");
 2121            try
 2122            {
 2123                var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.Serv
 2124
 2125                await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(
 2126            }
 2127            catch (Exception ex)
 2128            {
 2129                _logger.LogError(ex, "Error sending server shutdown notifications");
 2130            }
 2131
 2132            // Close open websockets to allow Kestrel to shut down cleanly
 2133            foreach (var session in _activeConnections.Values)
 2134            {
 2135                await session.DisposeAsync().ConfigureAwait(false);
 2136            }
 2137
 2138            _activeConnections.Clear();
 2139            _activeLiveStreamSessions.Clear();
 2140        }
 2141    }
 2142}

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)