< 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
18%
Covered lines: 76
Uncovered lines: 326
Coverable lines: 402
Total lines: 1904
Line coverage: 18.9%
Branch coverage
15%
Covered branches: 21
Total branches: 138
Branch coverage: 15.2%
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%16.041694.44%
CreateSession(...)50%12.371286.36%
GetUsers(...)0%2040%
StartCheckTimers()0%4260%
StopIdleCheckTimer()0%620%
StopInactiveCheckTimer()0%620%
GetNowPlayingItem(...)0%2040%
OnPlaybackStart(...)0%2040%
OnPlaybackProgress(...)100%210%
OnPlaybackProgress(...)0%4260%
UpdatePlaybackSettings(...)0%156120%
OnPlaybackStopped(...)0%2040%
GetSession(...)0%620%
GetSessionToRemoteControl(...)0%620%
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%2040%
RemoveAdditionalUser(...)0%2040%
AuthenticateNewSession(...)100%11100%
AuthenticateDirect(...)100%210%
ReportCapabilities(...)100%210%
ReportCapabilities(...)25%9.49430%
GetItemInfo(...)0%2040%
GetImageCacheTag(...)100%210%
ReportNowViewingItem(...)100%210%
ReportTranscodingInfo(...)0%620%
ClearTranscodingInfo(...)100%210%
GetSession(...)100%210%
GetSessionByAuthenticationToken(...)0%7280%
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
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Concurrent;
 7using System.Collections.Generic;
 8using System.Globalization;
 9using System.Linq;
 10using System.Threading;
 11using System.Threading.Tasks;
 12using Jellyfin.Data.Entities;
 13using Jellyfin.Data.Entities.Security;
 14using Jellyfin.Data.Enums;
 15using Jellyfin.Data.Events;
 16using Jellyfin.Data.Queries;
 17using Jellyfin.Extensions;
 18using MediaBrowser.Common.Events;
 19using MediaBrowser.Common.Extensions;
 20using MediaBrowser.Controller;
 21using MediaBrowser.Controller.Authentication;
 22using MediaBrowser.Controller.Configuration;
 23using MediaBrowser.Controller.Devices;
 24using MediaBrowser.Controller.Drawing;
 25using MediaBrowser.Controller.Dto;
 26using MediaBrowser.Controller.Entities;
 27using MediaBrowser.Controller.Events;
 28using MediaBrowser.Controller.Events.Authentication;
 29using MediaBrowser.Controller.Events.Session;
 30using MediaBrowser.Controller.Library;
 31using MediaBrowser.Controller.Net;
 32using MediaBrowser.Controller.Session;
 33using MediaBrowser.Model.Dto;
 34using MediaBrowser.Model.Entities;
 35using MediaBrowser.Model.Library;
 36using MediaBrowser.Model.Querying;
 37using MediaBrowser.Model.Session;
 38using MediaBrowser.Model.SyncPlay;
 39using Microsoft.EntityFrameworkCore;
 40using Microsoft.Extensions.Hosting;
 41using Microsoft.Extensions.Logging;
 42using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 43
 44namespace Emby.Server.Implementations.Session
 45{
 46    /// <summary>
 47    /// Class SessionManager.
 48    /// </summary>
 49    public sealed class SessionManager : ISessionManager, IAsyncDisposable
 50    {
 51        private readonly IUserDataManager _userDataManager;
 52        private readonly IServerConfigurationManager _config;
 53        private readonly ILogger<SessionManager> _logger;
 54        private readonly IEventManager _eventManager;
 55        private readonly ILibraryManager _libraryManager;
 56        private readonly IUserManager _userManager;
 57        private readonly IMusicManager _musicManager;
 58        private readonly IDtoService _dtoService;
 59        private readonly IImageProcessor _imageProcessor;
 60        private readonly IMediaSourceManager _mediaSourceManager;
 61        private readonly IServerApplicationHost _appHost;
 62        private readonly IDeviceManager _deviceManager;
 63        private readonly CancellationTokenRegistration _shutdownCallback;
 3264        private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
 3265            = new(StringComparer.OrdinalIgnoreCase);
 66
 67        private Timer _idleTimer;
 68        private Timer _inactiveTimer;
 69
 70        private DtoOptions _itemInfoDtoOptions;
 71        private bool _disposed = false;
 72
 73        public SessionManager(
 74            ILogger<SessionManager> logger,
 75            IEventManager eventManager,
 76            IUserDataManager userDataManager,
 77            IServerConfigurationManager config,
 78            ILibraryManager libraryManager,
 79            IUserManager userManager,
 80            IMusicManager musicManager,
 81            IDtoService dtoService,
 82            IImageProcessor imageProcessor,
 83            IServerApplicationHost appHost,
 84            IDeviceManager deviceManager,
 85            IMediaSourceManager mediaSourceManager,
 86            IHostApplicationLifetime hostApplicationLifetime)
 87        {
 3288            _logger = logger;
 3289            _eventManager = eventManager;
 3290            _userDataManager = userDataManager;
 3291            _config = config;
 3292            _libraryManager = libraryManager;
 3293            _userManager = userManager;
 3294            _musicManager = musicManager;
 3295            _dtoService = dtoService;
 3296            _imageProcessor = imageProcessor;
 3297            _appHost = appHost;
 3298            _deviceManager = deviceManager;
 3299            _mediaSourceManager = mediaSourceManager;
 32100            _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
 101
 32102            _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
 32103        }
 104
 105        /// <summary>
 106        /// Occurs when playback has started.
 107        /// </summary>
 108        public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
 109
 110        /// <summary>
 111        /// Occurs when playback has progressed.
 112        /// </summary>
 113        public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
 114
 115        /// <summary>
 116        /// Occurs when playback has stopped.
 117        /// </summary>
 118        public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
 119
 120        /// <inheritdoc />
 121        public event EventHandler<SessionEventArgs> SessionStarted;
 122
 123        /// <inheritdoc />
 124        public event EventHandler<SessionEventArgs> CapabilitiesChanged;
 125
 126        /// <inheritdoc />
 127        public event EventHandler<SessionEventArgs> SessionEnded;
 128
 129        /// <inheritdoc />
 130        public event EventHandler<SessionEventArgs> SessionActivity;
 131
 132        /// <inheritdoc />
 133        public event EventHandler<SessionEventArgs> SessionControllerConnected;
 134
 135        /// <summary>
 136        /// Gets all connections.
 137        /// </summary>
 138        /// <value>All connections.</value>
 39139        public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate)
 140
 141        private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e
 142        {
 0143            foreach (var session in Sessions)
 144            {
 0145                if (string.Equals(session.DeviceId, e.Argument.Item1, StringComparison.Ordinal))
 146                {
 0147                    if (!string.IsNullOrWhiteSpace(e.Argument.Item2.CustomName))
 148                    {
 0149                        session.HasCustomDeviceName = true;
 0150                        session.DeviceName = e.Argument.Item2.CustomName;
 151                    }
 152                    else
 153                    {
 0154                        session.HasCustomDeviceName = false;
 155                    }
 156                }
 157            }
 0158        }
 159
 160        private void CheckDisposed()
 161        {
 74162            ObjectDisposedException.ThrowIf(_disposed, this);
 74163        }
 164
 165        private void OnSessionStarted(SessionInfo info)
 166        {
 16167            if (!string.IsNullOrEmpty(info.DeviceId))
 168            {
 16169                var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
 170
 16171                if (capabilities is not null)
 172                {
 16173                    ReportCapabilities(info, capabilities, false);
 174                }
 175            }
 176
 16177            _eventManager.Publish(new SessionStartedEventArgs(info));
 178
 16179            EventHelper.QueueEventIfNotNull(
 16180                SessionStarted,
 16181                this,
 16182                new SessionEventArgs
 16183                {
 16184                    SessionInfo = info
 16185                },
 16186                _logger);
 16187        }
 188
 189        private async ValueTask OnSessionEnded(SessionInfo info)
 190        {
 191            EventHelper.QueueEventIfNotNull(
 192                SessionEnded,
 193                this,
 194                new SessionEventArgs
 195                {
 196                    SessionInfo = info
 197                },
 198                _logger);
 199
 200            _eventManager.Publish(new SessionEndedEventArgs(info));
 201
 202            await info.DisposeAsync().ConfigureAwait(false);
 203        }
 204
 205        /// <inheritdoc />
 206        public void UpdateDeviceName(string sessionId, string reportedDeviceName)
 207        {
 0208            var session = GetSession(sessionId);
 0209            if (session is not null)
 210            {
 0211                session.DeviceName = reportedDeviceName;
 212            }
 0213        }
 214
 215        /// <summary>
 216        /// Logs the user activity.
 217        /// </summary>
 218        /// <param name="appName">Type of the client.</param>
 219        /// <param name="appVersion">The app version.</param>
 220        /// <param name="deviceId">The device id.</param>
 221        /// <param name="deviceName">Name of the device.</param>
 222        /// <param name="remoteEndPoint">The remote end point.</param>
 223        /// <param name="user">The user.</param>
 224        /// <returns>SessionInfo.</returns>
 225        public async Task<SessionInfo> LogSessionActivity(
 226            string appName,
 227            string appVersion,
 228            string deviceId,
 229            string deviceName,
 230            string remoteEndPoint,
 231            User user)
 232        {
 233            CheckDisposed();
 234
 235            ArgumentException.ThrowIfNullOrEmpty(appName);
 236            ArgumentException.ThrowIfNullOrEmpty(appVersion);
 237            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 238
 239            var activityDate = DateTime.UtcNow;
 240            var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
 241            var lastActivityDate = session.LastActivityDate;
 242            session.LastActivityDate = activityDate;
 243
 244            if (user is not null)
 245            {
 246                var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
 247
 248                if ((activityDate - userLastActivityDate).TotalSeconds > 60)
 249                {
 250                    try
 251                    {
 252                        user.LastActivityDate = activityDate;
 253                        await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 254                    }
 255                    catch (DbUpdateConcurrencyException e)
 256                    {
 257                        _logger.LogDebug(e, "Error updating user's last activity date.");
 258                    }
 259                }
 260            }
 261
 262            if ((activityDate - lastActivityDate).TotalSeconds > 10)
 263            {
 264                SessionActivity?.Invoke(
 265                    this,
 266                    new SessionEventArgs
 267                    {
 268                        SessionInfo = session
 269                    });
 270            }
 271
 272            return session;
 273        }
 274
 275        /// <inheritdoc />
 276        public void OnSessionControllerConnected(SessionInfo session)
 277        {
 0278            EventHelper.QueueEventIfNotNull(
 0279                SessionControllerConnected,
 0280                this,
 0281                new SessionEventArgs
 0282                {
 0283                    SessionInfo = session
 0284                },
 0285                _logger);
 0286        }
 287
 288        /// <inheritdoc />
 289        public async Task CloseIfNeededAsync(SessionInfo session)
 290        {
 291            if (!session.SessionControllers.Any(i => i.IsSessionActive))
 292            {
 293                var key = GetSessionKey(session.Client, session.DeviceId);
 294
 295                _activeConnections.TryRemove(key, out _);
 296                if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
 297                {
 298                    await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false);
 299                }
 300
 301                await OnSessionEnded(session).ConfigureAwait(false);
 302            }
 303        }
 304
 305        /// <inheritdoc />
 306        public async ValueTask ReportSessionEnded(string sessionId)
 307        {
 308            CheckDisposed();
 309            var session = GetSession(sessionId, false);
 310
 311            if (session is not null)
 312            {
 313                var key = GetSessionKey(session.Client, session.DeviceId);
 314
 315                _activeConnections.TryRemove(key, out _);
 316
 317                await OnSessionEnded(session).ConfigureAwait(false);
 318            }
 319        }
 320
 321        private Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId)
 322        {
 0323            return _mediaSourceManager.GetMediaSource(item, mediaSourceId, liveStreamId, false, CancellationToken.None);
 324        }
 325
 326        /// <summary>
 327        /// Updates the now playing item id.
 328        /// </summary>
 329        /// <returns>Task.</returns>
 330        private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bo
 331        {
 332            if (string.IsNullOrEmpty(info.MediaSourceId))
 333            {
 334                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 335            }
 336
 337            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 338            {
 339                var current = session.NowPlayingItem;
 340
 341                if (current is null || !info.ItemId.Equals(current.Id))
 342                {
 343                    var runtimeTicks = libraryItem.RunTimeTicks;
 344
 345                    MediaSourceInfo mediaSource = null;
 346                    if (libraryItem is IHasMediaSources)
 347                    {
 348                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 349
 350                        if (mediaSource is not null)
 351                        {
 352                            runtimeTicks = mediaSource.RunTimeTicks;
 353                        }
 354                    }
 355
 356                    info.Item = GetItemInfo(libraryItem, mediaSource);
 357
 358                    info.Item.RunTimeTicks = runtimeTicks;
 359                }
 360                else
 361                {
 362                    info.Item = current;
 363                }
 364            }
 365
 366            session.NowPlayingItem = info.Item;
 367            session.LastActivityDate = DateTime.UtcNow;
 368
 369            if (updateLastCheckInTime)
 370            {
 371                session.LastPlaybackCheckIn = DateTime.UtcNow;
 372            }
 373
 374            if (info.IsPaused && session.LastPausedDate is null)
 375            {
 376                session.LastPausedDate = DateTime.UtcNow;
 377            }
 378            else if (!info.IsPaused)
 379            {
 380                session.LastPausedDate = null;
 381            }
 382
 383            session.PlayState.IsPaused = info.IsPaused;
 384            session.PlayState.PositionTicks = info.PositionTicks;
 385            session.PlayState.MediaSourceId = info.MediaSourceId;
 386            session.PlayState.LiveStreamId = info.LiveStreamId;
 387            session.PlayState.CanSeek = info.CanSeek;
 388            session.PlayState.IsMuted = info.IsMuted;
 389            session.PlayState.VolumeLevel = info.VolumeLevel;
 390            session.PlayState.AudioStreamIndex = info.AudioStreamIndex;
 391            session.PlayState.SubtitleStreamIndex = info.SubtitleStreamIndex;
 392            session.PlayState.PlayMethod = info.PlayMethod;
 393            session.PlayState.RepeatMode = info.RepeatMode;
 394            session.PlayState.PlaybackOrder = info.PlaybackOrder;
 395            session.PlaylistItemId = info.PlaylistItemId;
 396
 397            var nowPlayingQueue = info.NowPlayingQueue;
 398
 399            if (nowPlayingQueue?.Length > 0)
 400            {
 401                session.NowPlayingQueue = nowPlayingQueue;
 402
 403                var itemIds = Array.ConvertAll(nowPlayingQueue, queue => queue.Id);
 404                session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos(
 405                    _libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }),
 406                    new DtoOptions(true));
 407            }
 408        }
 409
 410        /// <summary>
 411        /// Removes the now playing item id.
 412        /// </summary>
 413        /// <param name="session">The session.</param>
 414        private void RemoveNowPlayingItem(SessionInfo session)
 415        {
 0416            session.NowPlayingItem = null;
 0417            session.PlayState = new PlayerStateInfo();
 418
 0419            if (!string.IsNullOrEmpty(session.DeviceId))
 420            {
 0421                ClearTranscodingInfo(session.DeviceId);
 422            }
 0423        }
 424
 425        private static string GetSessionKey(string appName, string deviceId)
 16426            => appName + deviceId;
 427
 428        /// <summary>
 429        /// Gets the connection.
 430        /// </summary>
 431        /// <param name="appName">Type of the client.</param>
 432        /// <param name="appVersion">The app version.</param>
 433        /// <param name="deviceId">The device id.</param>
 434        /// <param name="deviceName">Name of the device.</param>
 435        /// <param name="remoteEndPoint">The remote end point.</param>
 436        /// <param name="user">The user.</param>
 437        /// <returns>SessionInfo.</returns>
 438        private SessionInfo GetSessionInfo(
 439            string appName,
 440            string appVersion,
 441            string deviceId,
 442            string deviceName,
 443            string remoteEndPoint,
 444            User user)
 445        {
 16446            CheckDisposed();
 447
 16448            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 449
 16450            var key = GetSessionKey(appName, deviceId);
 451
 16452            CheckDisposed();
 453
 16454            if (!_activeConnections.TryGetValue(key, out var sessionInfo))
 455            {
 16456                sessionInfo = CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
 16457                _activeConnections[key] = sessionInfo;
 458            }
 459
 16460            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 16461            sessionInfo.UserName = user?.Username;
 16462            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 16463            sessionInfo.RemoteEndPoint = remoteEndPoint;
 16464            sessionInfo.Client = appName;
 465
 16466            if (!sessionInfo.HasCustomDeviceName || string.IsNullOrEmpty(sessionInfo.DeviceName))
 467            {
 16468                sessionInfo.DeviceName = deviceName;
 469            }
 470
 16471            sessionInfo.ApplicationVersion = appVersion;
 472
 16473            if (user is null)
 474            {
 0475                sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>();
 476            }
 477
 16478            return sessionInfo;
 479        }
 480
 481        private SessionInfo CreateSession(
 482            string key,
 483            string appName,
 484            string appVersion,
 485            string deviceId,
 486            string deviceName,
 487            string remoteEndPoint,
 488            User user)
 489        {
 16490            var sessionInfo = new SessionInfo(this, _logger)
 16491            {
 16492                Client = appName,
 16493                DeviceId = deviceId,
 16494                ApplicationVersion = appVersion,
 16495                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
 16496                ServerId = _appHost.SystemId
 16497            };
 498
 16499            var username = user?.Username;
 500
 16501            sessionInfo.UserId = user?.Id ?? Guid.Empty;
 16502            sessionInfo.UserName = username;
 16503            sessionInfo.UserPrimaryImageTag = user?.ProfileImage is null ? null : GetImageCacheTag(user);
 16504            sessionInfo.RemoteEndPoint = remoteEndPoint;
 505
 16506            if (string.IsNullOrEmpty(deviceName))
 507            {
 0508                deviceName = "Network Device";
 509            }
 510
 16511            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
 16512            if (string.IsNullOrEmpty(deviceOptions.CustomName))
 513            {
 16514                sessionInfo.DeviceName = deviceName;
 515            }
 516            else
 517            {
 0518                sessionInfo.DeviceName = deviceOptions.CustomName;
 0519                sessionInfo.HasCustomDeviceName = true;
 520            }
 521
 16522            OnSessionStarted(sessionInfo);
 16523            return sessionInfo;
 524        }
 525
 526        private List<User> GetUsers(SessionInfo session)
 527        {
 0528            var users = new List<User>();
 529
 0530            if (session.UserId.IsEmpty())
 531            {
 0532                return users;
 533            }
 534
 0535            var user = _userManager.GetUserById(session.UserId);
 536
 0537            if (user is null)
 538            {
 0539                throw new InvalidOperationException("User not found");
 540            }
 541
 0542            users.Add(user);
 543
 0544            users.AddRange(session.AdditionalUsers
 0545                .Select(i => _userManager.GetUserById(i.UserId))
 0546                .Where(i => i is not null));
 547
 0548            return users;
 549        }
 550
 551        private void StartCheckTimers()
 552        {
 0553            _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
 554
 0555            if (_config.Configuration.InactiveSessionThreshold > 0)
 556            {
 0557                _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes
 558            }
 559            else
 560            {
 0561                StopInactiveCheckTimer();
 562            }
 0563        }
 564
 565        private void StopIdleCheckTimer()
 566        {
 0567            if (_idleTimer is not null)
 568            {
 0569                _idleTimer.Dispose();
 0570                _idleTimer = null;
 571            }
 0572        }
 573
 574        private void StopInactiveCheckTimer()
 575        {
 0576            if (_inactiveTimer is not null)
 577            {
 0578                _inactiveTimer.Dispose();
 0579                _inactiveTimer = null;
 580            }
 0581        }
 582
 583        private async void CheckForIdlePlayback(object state)
 584        {
 585            var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
 586                .ToList();
 587
 588            if (playingSessions.Count > 0)
 589            {
 590                var idle = playingSessions
 591                    .Where(i => (DateTime.UtcNow - i.LastPlaybackCheckIn).TotalMinutes > 5)
 592                    .ToList();
 593
 594                foreach (var session in idle)
 595                {
 596                    _logger.LogDebug("Session {0} has gone idle while playing", session.Id);
 597
 598                    try
 599                    {
 600                        await OnPlaybackStopped(new PlaybackStopInfo
 601                        {
 602                            Item = session.NowPlayingItem,
 603                            ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
 604                            SessionId = session.Id,
 605                            MediaSourceId = session.PlayState?.MediaSourceId,
 606                            PositionTicks = session.PlayState?.PositionTicks
 607                        }).ConfigureAwait(false);
 608                    }
 609                    catch (Exception ex)
 610                    {
 611                        _logger.LogDebug(ex, "Error calling OnPlaybackStopped");
 612                    }
 613                }
 614            }
 615            else
 616            {
 617                StopIdleCheckTimer();
 618            }
 619        }
 620
 621        private async void CheckForInactiveSteams(object state)
 622        {
 623            var inactiveSessions = Sessions.Where(i =>
 624                    i.NowPlayingItem is not null
 625                    && i.PlayState.IsPaused
 626                    && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionTh
 627
 628            foreach (var session in inactiveSessions)
 629            {
 630                _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session
 631
 632                try
 633                {
 634                    await SendPlaystateCommand(
 635                        session.Id,
 636                        session.Id,
 637                        new PlaystateRequest()
 638                        {
 639                            Command = PlaystateCommand.Stop,
 640                            ControllingUserId = session.UserId.ToString(),
 641                            SeekPositionTicks = session.PlayState?.PositionTicks
 642                        },
 643                        CancellationToken.None).ConfigureAwait(true);
 644                }
 645                catch (Exception ex)
 646                {
 647                    _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", 
 648                }
 649            }
 650
 651            bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
 652
 653            if (!playingSessions)
 654            {
 655                StopInactiveCheckTimer();
 656            }
 657        }
 658
 659        private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
 660        {
 0661            var item = session.FullNowPlayingItem;
 0662            if (item is not null && item.Id.Equals(itemId))
 663            {
 0664                return item;
 665            }
 666
 0667            item = _libraryManager.GetItemById(itemId);
 668
 0669            session.FullNowPlayingItem = item;
 670
 0671            return item;
 672        }
 673
 674        /// <summary>
 675        /// Used to report that playback has started for an item.
 676        /// </summary>
 677        /// <param name="info">The info.</param>
 678        /// <returns>Task.</returns>
 679        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 680        public async Task OnPlaybackStart(PlaybackStartInfo info)
 681        {
 682            CheckDisposed();
 683
 684            ArgumentNullException.ThrowIfNull(info);
 685
 686            var session = GetSession(info.SessionId);
 687
 688            var libraryItem = info.ItemId.IsEmpty()
 689                ? null
 690                : GetNowPlayingItem(session, info.ItemId);
 691
 692            await UpdateNowPlayingItem(session, info, libraryItem, true).ConfigureAwait(false);
 693
 694            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 695            {
 696                ClearTranscodingInfo(session.DeviceId);
 697            }
 698
 699            session.StartAutomaticProgress(info);
 700
 701            var users = GetUsers(session);
 702
 703            if (libraryItem is not null)
 704            {
 705                foreach (var user in users)
 706                {
 707                    OnPlaybackStart(user, libraryItem);
 708                }
 709            }
 710
 711            var eventArgs = new PlaybackStartEventArgs
 712            {
 713                Item = libraryItem,
 714                Users = users,
 715                MediaSourceId = info.MediaSourceId,
 716                MediaInfo = info.Item,
 717                DeviceName = session.DeviceName,
 718                ClientName = session.Client,
 719                DeviceId = session.DeviceId,
 720                Session = session,
 721                PlaybackPositionTicks = info.PositionTicks,
 722                PlaySessionId = info.PlaySessionId
 723            };
 724
 725            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 726
 727            // Nothing to save here
 728            // Fire events to inform plugins
 729            EventHelper.QueueEventIfNotNull(
 730                PlaybackStart,
 731                this,
 732                eventArgs,
 733                _logger);
 734
 735            StartCheckTimers();
 736        }
 737
 738        /// <summary>
 739        /// Called when [playback start].
 740        /// </summary>
 741        /// <param name="user">The user object.</param>
 742        /// <param name="item">The item.</param>
 743        private void OnPlaybackStart(User user, BaseItem item)
 744        {
 0745            var data = _userDataManager.GetUserData(user, item);
 746
 0747            data.PlayCount++;
 0748            data.LastPlayedDate = DateTime.UtcNow;
 749
 0750            if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume)
 751            {
 0752                data.Played = true;
 753            }
 754            else
 755            {
 0756                data.Played = false;
 757            }
 758
 0759            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
 0760        }
 761
 762        /// <inheritdoc />
 763        public Task OnPlaybackProgress(PlaybackProgressInfo info)
 764        {
 0765            return OnPlaybackProgress(info, false);
 766        }
 767
 768        /// <summary>
 769        /// Used to report playback progress for an item.
 770        /// </summary>
 771        /// <param name="info">The playback progress info.</param>
 772        /// <param name="isAutomated">Whether this is an automated update.</param>
 773        /// <returns>Task.</returns>
 774        public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
 775        {
 776            CheckDisposed();
 777
 778            ArgumentNullException.ThrowIfNull(info);
 779
 780            var session = GetSession(info.SessionId);
 781
 782            var libraryItem = info.ItemId.IsEmpty()
 783                ? null
 784                : GetNowPlayingItem(session, info.ItemId);
 785
 786            await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
 787
 788            if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
 789            {
 790                ClearTranscodingInfo(session.DeviceId);
 791            }
 792
 793            var users = GetUsers(session);
 794
 795            // only update saved user data on actual check-ins, not automated ones
 796            if (libraryItem is not null && !isAutomated)
 797            {
 798                foreach (var user in users)
 799                {
 800                    OnPlaybackProgress(user, libraryItem, info);
 801                }
 802            }
 803
 804            var eventArgs = new PlaybackProgressEventArgs
 805            {
 806                Item = libraryItem,
 807                Users = users,
 808                PlaybackPositionTicks = session.PlayState.PositionTicks,
 809                MediaSourceId = session.PlayState.MediaSourceId,
 810                MediaInfo = info.Item,
 811                DeviceName = session.DeviceName,
 812                ClientName = session.Client,
 813                DeviceId = session.DeviceId,
 814                IsPaused = info.IsPaused,
 815                PlaySessionId = info.PlaySessionId,
 816                IsAutomated = isAutomated,
 817                Session = session
 818            };
 819
 820            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 821
 822            PlaybackProgress?.Invoke(this, eventArgs);
 823
 824            if (!isAutomated)
 825            {
 826                session.StartAutomaticProgress(info);
 827            }
 828
 829            StartCheckTimers();
 830        }
 831
 832        private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
 833        {
 0834            var data = _userDataManager.GetUserData(user, item);
 835
 0836            var positionTicks = info.PositionTicks;
 837
 0838            var changed = false;
 839
 0840            if (positionTicks.HasValue)
 841            {
 0842                _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 0843                changed = true;
 844            }
 845
 0846            var tracksChanged = UpdatePlaybackSettings(user, info, data);
 0847            if (!tracksChanged)
 848            {
 0849                changed = true;
 850            }
 851
 0852            if (changed)
 853            {
 0854                _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.N
 855            }
 0856        }
 857
 858        private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
 859        {
 0860            var changed = false;
 861
 0862            if (user.RememberAudioSelections)
 863            {
 0864                if (data.AudioStreamIndex != info.AudioStreamIndex)
 865                {
 0866                    data.AudioStreamIndex = info.AudioStreamIndex;
 0867                    changed = true;
 868                }
 869            }
 870            else
 871            {
 0872                if (data.AudioStreamIndex.HasValue)
 873                {
 0874                    data.AudioStreamIndex = null;
 0875                    changed = true;
 876                }
 877            }
 878
 0879            if (user.RememberSubtitleSelections)
 880            {
 0881                if (data.SubtitleStreamIndex != info.SubtitleStreamIndex)
 882                {
 0883                    data.SubtitleStreamIndex = info.SubtitleStreamIndex;
 0884                    changed = true;
 885                }
 886            }
 887            else
 888            {
 0889                if (data.SubtitleStreamIndex.HasValue)
 890                {
 0891                    data.SubtitleStreamIndex = null;
 0892                    changed = true;
 893                }
 894            }
 895
 0896            return changed;
 897        }
 898
 899        /// <summary>
 900        /// Used to report that playback has ended for an item.
 901        /// </summary>
 902        /// <param name="info">The info.</param>
 903        /// <returns>Task.</returns>
 904        /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
 905        /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</excepti
 906        public async Task OnPlaybackStopped(PlaybackStopInfo info)
 907        {
 908            CheckDisposed();
 909
 910            ArgumentNullException.ThrowIfNull(info);
 911
 912            if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
 913            {
 914                throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."
 915            }
 916
 917            var session = GetSession(info.SessionId);
 918
 919            session.StopAutomaticProgress();
 920
 921            var libraryItem = info.ItemId.IsEmpty()
 922                ? null
 923                : GetNowPlayingItem(session, info.ItemId);
 924
 925            // Normalize
 926            if (string.IsNullOrEmpty(info.MediaSourceId))
 927            {
 928                info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
 929            }
 930
 931            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
 932            {
 933                var current = session.NowPlayingItem;
 934
 935                if (current is null || !info.ItemId.Equals(current.Id))
 936                {
 937                    MediaSourceInfo mediaSource = null;
 938
 939                    if (libraryItem is IHasMediaSources)
 940                    {
 941                        mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).Configure
 942                    }
 943
 944                    info.Item = GetItemInfo(libraryItem, mediaSource);
 945                }
 946                else
 947                {
 948                    info.Item = current;
 949                }
 950            }
 951
 952            if (info.Item is not null)
 953            {
 954                var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.Inv
 955
 956                _logger.LogInformation(
 957                    "Playback stopped reported by app {0} {1} playing {2}. Stopped at {3} ms",
 958                    session.Client,
 959                    session.ApplicationVersion,
 960                    info.Item.Name,
 961                    msString);
 962            }
 963
 964            if (info.NowPlayingQueue is not null)
 965            {
 966                session.NowPlayingQueue = info.NowPlayingQueue;
 967            }
 968
 969            session.PlaylistItemId = info.PlaylistItemId;
 970
 971            RemoveNowPlayingItem(session);
 972
 973            var users = GetUsers(session);
 974            var playedToCompletion = false;
 975
 976            if (libraryItem is not null)
 977            {
 978                foreach (var user in users)
 979                {
 980                    playedToCompletion = OnPlaybackStopped(user, libraryItem, info.PositionTicks, info.Failed);
 981                }
 982            }
 983
 984            if (!string.IsNullOrEmpty(info.LiveStreamId))
 985            {
 986                try
 987                {
 988                    await _mediaSourceManager.CloseLiveStream(info.LiveStreamId).ConfigureAwait(false);
 989                }
 990                catch (Exception ex)
 991                {
 992                    _logger.LogError(ex, "Error closing live stream");
 993                }
 994            }
 995
 996            var eventArgs = new PlaybackStopEventArgs
 997            {
 998                Item = libraryItem,
 999                Users = users,
 1000                PlaybackPositionTicks = info.PositionTicks,
 1001                PlayedToCompletion = playedToCompletion,
 1002                MediaSourceId = info.MediaSourceId,
 1003                MediaInfo = info.Item,
 1004                DeviceName = session.DeviceName,
 1005                ClientName = session.Client,
 1006                DeviceId = session.DeviceId,
 1007                Session = session,
 1008                PlaySessionId = info.PlaySessionId
 1009            };
 1010
 1011            await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
 1012
 1013            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
 1014        }
 1015
 1016        private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
 1017        {
 01018            if (playbackFailed)
 1019            {
 01020                return false;
 1021            }
 1022
 01023            var data = _userDataManager.GetUserData(user, item);
 1024            bool playedToCompletion;
 01025            if (positionTicks.HasValue)
 1026            {
 01027                playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
 1028            }
 1029            else
 1030            {
 1031                // If the client isn't able to report this, then we'll just have to make an assumption
 01032                data.PlayCount++;
 01033                data.Played = item.SupportsPlayedStatus;
 01034                data.PlaybackPositionTicks = 0;
 01035                playedToCompletion = true;
 1036            }
 1037
 01038            _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None)
 1039
 01040            return playedToCompletion;
 1041        }
 1042
 1043        /// <summary>
 1044        /// Gets the session.
 1045        /// </summary>
 1046        /// <param name="sessionId">The session identifier.</param>
 1047        /// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
 1048        /// <returns>SessionInfo.</returns>
 1049        /// <exception cref="ResourceNotFoundException">
 1050        /// No session with an Id equal to <c>sessionId</c> was found
 1051        /// and <c>throwOnMissing</c> is <c>true</c>.
 1052        /// </exception>
 1053        private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
 1054        {
 01055            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 01056            if (session is null && throwOnMissing)
 1057            {
 01058                throw new ResourceNotFoundException(
 01059                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1060            }
 1061
 01062            return session;
 1063        }
 1064
 1065        private SessionInfo GetSessionToRemoteControl(string sessionId)
 1066        {
 1067            // Accept either device id or session id
 01068            var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
 1069
 01070            if (session is null)
 1071            {
 01072                throw new ResourceNotFoundException(
 01073                    string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
 1074            }
 1075
 01076            return session;
 1077        }
 1078
 1079        /// <inheritdoc />
 1080        public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, Cancellati
 1081        {
 01082            CheckDisposed();
 1083
 01084            var generalCommand = new GeneralCommand
 01085            {
 01086                Name = GeneralCommandType.DisplayMessage
 01087            };
 1088
 01089            generalCommand.Arguments["Header"] = command.Header;
 01090            generalCommand.Arguments["Text"] = command.Text;
 1091
 01092            if (command.TimeoutMs.HasValue)
 1093            {
 01094                generalCommand.Arguments["TimeoutMs"] = command.TimeoutMs.Value.ToString(CultureInfo.InvariantCulture);
 1095            }
 1096
 01097            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1098        }
 1099
 1100        /// <inheritdoc />
 1101        public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, Cancellati
 1102        {
 01103            CheckDisposed();
 1104
 01105            var session = GetSessionToRemoteControl(sessionId);
 1106
 01107            if (!string.IsNullOrEmpty(controllingSessionId))
 1108            {
 01109                var controllingSession = GetSession(controllingSessionId);
 01110                AssertCanControl(session, controllingSession);
 1111            }
 1112
 01113            return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
 1114        }
 1115
 1116        private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, Cancella
 1117        {
 1118            var controllers = session.SessionControllers;
 1119            var messageId = Guid.NewGuid();
 1120
 1121            foreach (var controller in controllers)
 1122            {
 1123                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
 1124            }
 1125        }
 1126
 1127        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data,
 1128        {
 1129            IEnumerable<Task> GetTasks()
 1130            {
 1131                var messageId = Guid.NewGuid();
 1132                foreach (var session in sessions)
 1133                {
 1134                    var controllers = session.SessionControllers;
 1135                    foreach (var controller in controllers)
 1136                    {
 1137                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
 1138                    }
 1139                }
 1140            }
 1141
 221142            return Task.WhenAll(GetTasks());
 1143        }
 1144
 1145        /// <inheritdoc />
 1146        public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, Cancellati
 1147        {
 1148            CheckDisposed();
 1149
 1150            var session = GetSessionToRemoteControl(sessionId);
 1151
 1152            var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
 1153
 1154            List<BaseItem> items;
 1155
 1156            if (command.PlayCommand == PlayCommand.PlayInstantMix)
 1157            {
 1158                items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user))
 1159                    .ToList();
 1160
 1161                command.PlayCommand = PlayCommand.PlayNow;
 1162            }
 1163            else
 1164            {
 1165                var list = new List<BaseItem>();
 1166                foreach (var itemId in command.ItemIds)
 1167                {
 1168                    var subItems = TranslateItemForPlayback(itemId, user);
 1169                    list.AddRange(subItems);
 1170                }
 1171
 1172                items = list;
 1173            }
 1174
 1175            if (command.PlayCommand == PlayCommand.PlayShuffle)
 1176            {
 1177                items.Shuffle();
 1178                command.PlayCommand = PlayCommand.PlayNow;
 1179            }
 1180
 1181            command.ItemIds = items.Select(i => i.Id).ToArray();
 1182
 1183            if (user is not null)
 1184            {
 1185                if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
 1186                {
 1187                    throw new ArgumentException(
 1188                        string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username))
 1189                }
 1190            }
 1191
 1192            if (user is not null
 1193                && command.ItemIds.Length == 1
 1194                && user.EnableNextEpisodeAutoPlay
 1195                && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
 1196            {
 1197                var series = episode.Series;
 1198                if (series is not null)
 1199                {
 1200                    var episodes = series.GetEpisodes(
 1201                            user,
 1202                            new DtoOptions(false)
 1203                            {
 1204                                EnableImages = false
 1205                            },
 1206                            user.DisplayMissingEpisodes)
 1207                        .Where(i => !i.IsVirtualItem)
 1208                        .SkipWhile(i => !i.Id.Equals(episode.Id))
 1209                        .ToList();
 1210
 1211                    if (episodes.Count > 0)
 1212                    {
 1213                        command.ItemIds = episodes.Select(i => i.Id).ToArray();
 1214                    }
 1215                }
 1216            }
 1217
 1218            if (!string.IsNullOrEmpty(controllingSessionId))
 1219            {
 1220                var controllingSession = GetSession(controllingSessionId);
 1221                AssertCanControl(session, controllingSession);
 1222                if (!controllingSession.UserId.IsEmpty())
 1223                {
 1224                    command.ControllingUserId = controllingSession.UserId;
 1225                }
 1226            }
 1227
 1228            await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(fals
 1229        }
 1230
 1231        /// <inheritdoc />
 1232        public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken
 1233        {
 1234            CheckDisposed();
 1235            var session = GetSession(sessionId);
 1236            await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).Configur
 1237        }
 1238
 1239        /// <inheritdoc />
 1240        public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancell
 1241        {
 1242            CheckDisposed();
 1243            var session = GetSession(sessionId);
 1244            await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).Conf
 1245        }
 1246
 1247        private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
 1248        {
 01249            var item = _libraryManager.GetItemById(id);
 1250
 01251            if (item is null)
 1252            {
 01253                _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForPlayback", id);
 01254                return Array.Empty<BaseItem>();
 1255            }
 1256
 01257            if (item is IItemByName byName)
 1258            {
 01259                return byName.GetTaggedItems(new InternalItemsQuery(user)
 01260                {
 01261                    IsFolder = false,
 01262                    Recursive = true,
 01263                    DtoOptions = new DtoOptions(false)
 01264                    {
 01265                        EnableImages = false,
 01266                        Fields = new[]
 01267                        {
 01268                            ItemFields.SortName
 01269                        }
 01270                    },
 01271                    IsVirtualItem = false,
 01272                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01273                });
 1274            }
 1275
 01276            if (item.IsFolder)
 1277            {
 01278                var folder = (Folder)item;
 1279
 01280                return folder.GetItemList(new InternalItemsQuery(user)
 01281                {
 01282                    Recursive = true,
 01283                    IsFolder = false,
 01284                    DtoOptions = new DtoOptions(false)
 01285                    {
 01286                        EnableImages = false,
 01287                        Fields = new ItemFields[]
 01288                        {
 01289                            ItemFields.SortName
 01290                        }
 01291                    },
 01292                    IsVirtualItem = false,
 01293                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
 01294                });
 1295            }
 1296
 01297            return new[] { item };
 1298        }
 1299
 1300        private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
 1301        {
 01302            var item = _libraryManager.GetItemById(id);
 1303
 01304            if (item is null)
 1305            {
 01306                _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForInstantMix", id);
 01307                return new List<BaseItem>();
 1308            }
 1309
 01310            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToLis
 1311        }
 1312
 1313        /// <inheritdoc />
 1314        public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, Cancellation
 1315        {
 01316            var generalCommand = new GeneralCommand
 01317            {
 01318                Name = GeneralCommandType.DisplayContent,
 01319                Arguments =
 01320                {
 01321                    ["ItemId"] = command.ItemId,
 01322                    ["ItemName"] = command.ItemName,
 01323                    ["ItemType"] = command.ItemType.ToString()
 01324                }
 01325            };
 1326
 01327            return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
 1328        }
 1329
 1330        /// <inheritdoc />
 1331        public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, Cancel
 1332        {
 01333            CheckDisposed();
 1334
 01335            var session = GetSessionToRemoteControl(sessionId);
 1336
 01337            if (!string.IsNullOrEmpty(controllingSessionId))
 1338            {
 01339                var controllingSession = GetSession(controllingSessionId);
 01340                AssertCanControl(session, controllingSession);
 01341                if (!controllingSession.UserId.IsEmpty())
 1342                {
 01343                    command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
 1344                }
 1345            }
 1346
 01347            return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
 1348        }
 1349
 1350        private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
 1351        {
 01352            ArgumentNullException.ThrowIfNull(session);
 1353
 01354            ArgumentNullException.ThrowIfNull(controllingSession);
 01355        }
 1356
 1357        /// <summary>
 1358        /// Sends the restart required message.
 1359        /// </summary>
 1360        /// <param name="cancellationToken">The cancellation token.</param>
 1361        /// <returns>Task.</returns>
 1362        public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
 1363        {
 01364            CheckDisposed();
 1365
 01366            return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
 1367        }
 1368
 1369        /// <summary>
 1370        /// Adds the additional user.
 1371        /// </summary>
 1372        /// <param name="sessionId">The session identifier.</param>
 1373        /// <param name="userId">The user identifier.</param>
 1374        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1375        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1376        public void AddAdditionalUser(string sessionId, Guid userId)
 1377        {
 01378            CheckDisposed();
 1379
 01380            var session = GetSession(sessionId);
 1381
 01382            if (session.UserId.Equals(userId))
 1383            {
 01384                throw new ArgumentException("The requested user is already the primary user of the session.");
 1385            }
 1386
 01387            if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
 1388            {
 01389                var user = _userManager.GetUserById(userId);
 01390                var newUser = new SessionUserInfo
 01391                {
 01392                    UserId = userId,
 01393                    UserName = user.Username
 01394                };
 1395
 01396                session.AdditionalUsers = [..session.AdditionalUsers, newUser];
 1397            }
 01398        }
 1399
 1400        /// <summary>
 1401        /// Removes the additional user.
 1402        /// </summary>
 1403        /// <param name="sessionId">The session identifier.</param>
 1404        /// <param name="userId">The user identifier.</param>
 1405        /// <exception cref="UnauthorizedAccessException">Cannot modify additional users without authenticating first.</
 1406        /// <exception cref="ArgumentException">The requested user is already the primary user of the session.</exceptio
 1407        public void RemoveAdditionalUser(string sessionId, Guid userId)
 1408        {
 01409            CheckDisposed();
 1410
 01411            var session = GetSession(sessionId);
 1412
 01413            if (session.UserId.Equals(userId))
 1414            {
 01415                throw new ArgumentException("The requested user is already the primary user of the session.");
 1416            }
 1417
 01418            var user = session.AdditionalUsers.FirstOrDefault(i => i.UserId.Equals(userId));
 1419
 01420            if (user is not null)
 1421            {
 01422                var list = session.AdditionalUsers.ToList();
 01423                list.Remove(user);
 1424
 01425                session.AdditionalUsers = list.ToArray();
 1426            }
 01427        }
 1428
 1429        /// <summary>
 1430        /// Authenticates the new session.
 1431        /// </summary>
 1432        /// <param name="request">The authenticationrequest.</param>
 1433        /// <returns>The authentication result.</returns>
 1434        public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
 1435        {
 161436            return AuthenticateNewSessionInternal(request, true);
 1437        }
 1438
 1439        /// <summary>
 1440        /// Directly authenticates the session without enforcing password.
 1441        /// </summary>
 1442        /// <param name="request">The authentication request.</param>
 1443        /// <returns>The authentication result.</returns>
 1444        public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
 1445        {
 01446            return AuthenticateNewSessionInternal(request, false);
 1447        }
 1448
 1449        internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enf
 1450        {
 1451            CheckDisposed();
 1452
 1453            ArgumentException.ThrowIfNullOrEmpty(request.App);
 1454            ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
 1455            ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
 1456            ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
 1457
 1458            User user = null;
 1459            if (!request.UserId.IsEmpty())
 1460            {
 1461                user = _userManager.GetUserById(request.UserId);
 1462            }
 1463
 1464            user ??= _userManager.GetUserByName(request.Username);
 1465
 1466            if (enforcePassword)
 1467            {
 1468                user = await _userManager.AuthenticateUser(
 1469                    request.Username,
 1470                    request.Password,
 1471                    request.RemoteEndPoint,
 1472                    true).ConfigureAwait(false);
 1473            }
 1474
 1475            if (user is null)
 1476            {
 1477                await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
 1478                throw new AuthenticationException("Invalid username or password entered.");
 1479            }
 1480
 1481            if (!string.IsNullOrEmpty(request.DeviceId)
 1482                && !_deviceManager.CanAccessDevice(user, request.DeviceId))
 1483            {
 1484                throw new SecurityException("User is not allowed access from this device.");
 1485            }
 1486
 1487            int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
 1488            int maxActiveSessions = user.MaxActiveSessions;
 1489            _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCoun
 1490            if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
 1491            {
 1492                throw new SecurityException("User is at their maximum number of sessions.");
 1493            }
 1494
 1495            var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.Dev
 1496
 1497            var session = await LogSessionActivity(
 1498                request.App,
 1499                request.AppVersion,
 1500                request.DeviceId,
 1501                request.DeviceName,
 1502                request.RemoteEndPoint,
 1503                user).ConfigureAwait(false);
 1504
 1505            var returnResult = new AuthenticationResult
 1506            {
 1507                User = _userManager.GetUserDto(user, request.RemoteEndPoint),
 1508                SessionInfo = session,
 1509                AccessToken = token,
 1510                ServerId = _appHost.SystemId
 1511            };
 1512
 1513            await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
 1514            return returnResult;
 1515        }
 1516
 1517        internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, str
 1518        {
 1519            // This should be validated above, but if it isn't don't delete all tokens.
 1520            ArgumentException.ThrowIfNullOrEmpty(deviceId);
 1521
 1522            var existing = _deviceManager.GetDevices(
 1523                new DeviceQuery
 1524                {
 1525                    DeviceId = deviceId,
 1526                    UserId = user.Id
 1527                }).Items;
 1528
 1529            foreach (var auth in existing)
 1530            {
 1531                try
 1532                {
 1533                    // Logout any existing sessions for the user on this device
 1534                    await Logout(auth).ConfigureAwait(false);
 1535                }
 1536                catch (Exception ex)
 1537                {
 1538                    _logger.LogError(ex, "Error while logging out existing session.");
 1539                }
 1540            }
 1541
 1542            _logger.LogInformation("Creating new access token for user {0}", user.Id);
 1543            var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).C
 1544
 1545            return device.AccessToken;
 1546        }
 1547
 1548        /// <inheritdoc />
 1549        public async Task Logout(string accessToken)
 1550        {
 1551            CheckDisposed();
 1552
 1553            ArgumentException.ThrowIfNullOrEmpty(accessToken);
 1554
 1555            var existing = _deviceManager.GetDevices(
 1556                new DeviceQuery
 1557                {
 1558                    Limit = 1,
 1559                    AccessToken = accessToken
 1560                }).Items;
 1561
 1562            if (existing.Count > 0)
 1563            {
 1564                await Logout(existing[0]).ConfigureAwait(false);
 1565            }
 1566        }
 1567
 1568        /// <inheritdoc />
 1569        public async Task Logout(Device device)
 1570        {
 1571            CheckDisposed();
 1572
 1573            _logger.LogInformation("Logging out access token {0}", device.AccessToken);
 1574
 1575            await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
 1576
 1577            var sessions = Sessions
 1578                .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
 1579                .ToList();
 1580
 1581            foreach (var session in sessions)
 1582            {
 1583                try
 1584                {
 1585                    await ReportSessionEnded(session.Id).ConfigureAwait(false);
 1586                }
 1587                catch (Exception ex)
 1588                {
 1589                    _logger.LogError(ex, "Error reporting session ended");
 1590                }
 1591            }
 1592        }
 1593
 1594        /// <inheritdoc />
 1595        public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
 1596        {
 1597            CheckDisposed();
 1598
 1599            var existing = _deviceManager.GetDevices(new DeviceQuery
 1600            {
 1601                UserId = userId
 1602            });
 1603
 1604            foreach (var info in existing.Items)
 1605            {
 1606                if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
 1607                {
 1608                    await Logout(info).ConfigureAwait(false);
 1609                }
 1610            }
 1611        }
 1612
 1613        /// <summary>
 1614        /// Reports the capabilities.
 1615        /// </summary>
 1616        /// <param name="sessionId">The session identifier.</param>
 1617        /// <param name="capabilities">The capabilities.</param>
 1618        public void ReportCapabilities(string sessionId, ClientCapabilities capabilities)
 1619        {
 01620            CheckDisposed();
 1621
 01622            var session = GetSession(sessionId);
 1623
 01624            ReportCapabilities(session, capabilities, true);
 01625        }
 1626
 1627        private void ReportCapabilities(
 1628            SessionInfo session,
 1629            ClientCapabilities capabilities,
 1630            bool saveCapabilities)
 1631        {
 161632            session.Capabilities = capabilities;
 1633
 161634            if (saveCapabilities)
 1635            {
 01636                CapabilitiesChanged?.Invoke(
 01637                    this,
 01638                    new SessionEventArgs
 01639                    {
 01640                        SessionInfo = session
 01641                    });
 1642
 01643                _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
 1644            }
 161645        }
 1646
 1647        /// <summary>
 1648        /// Converts a BaseItem to a BaseItemInfo.
 1649        /// </summary>
 1650        private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
 1651        {
 01652            ArgumentNullException.ThrowIfNull(item);
 1653
 01654            var dtoOptions = _itemInfoDtoOptions;
 1655
 01656            if (_itemInfoDtoOptions is null)
 1657            {
 01658                dtoOptions = new DtoOptions
 01659                {
 01660                    AddProgramRecordingInfo = false
 01661                };
 1662
 01663                var fields = dtoOptions.Fields.ToList();
 1664
 01665                fields.Remove(ItemFields.CanDelete);
 01666                fields.Remove(ItemFields.CanDownload);
 01667                fields.Remove(ItemFields.ChildCount);
 01668                fields.Remove(ItemFields.CustomRating);
 01669                fields.Remove(ItemFields.DateLastMediaAdded);
 01670                fields.Remove(ItemFields.DateLastRefreshed);
 01671                fields.Remove(ItemFields.DateLastSaved);
 01672                fields.Remove(ItemFields.DisplayPreferencesId);
 01673                fields.Remove(ItemFields.Etag);
 01674                fields.Remove(ItemFields.InheritedParentalRatingValue);
 01675                fields.Remove(ItemFields.ItemCounts);
 01676                fields.Remove(ItemFields.MediaSourceCount);
 01677                fields.Remove(ItemFields.MediaStreams);
 01678                fields.Remove(ItemFields.MediaSources);
 01679                fields.Remove(ItemFields.People);
 01680                fields.Remove(ItemFields.PlayAccess);
 01681                fields.Remove(ItemFields.People);
 01682                fields.Remove(ItemFields.ProductionLocations);
 01683                fields.Remove(ItemFields.RecursiveItemCount);
 01684                fields.Remove(ItemFields.RemoteTrailers);
 01685                fields.Remove(ItemFields.SeasonUserData);
 01686                fields.Remove(ItemFields.Settings);
 01687                fields.Remove(ItemFields.SortName);
 01688                fields.Remove(ItemFields.Tags);
 01689                fields.Remove(ItemFields.ExtraIds);
 1690
 01691                dtoOptions.Fields = fields.ToArray();
 1692
 01693                _itemInfoDtoOptions = dtoOptions;
 1694            }
 1695
 01696            var info = _dtoService.GetBaseItemDto(item, dtoOptions);
 1697
 01698            if (mediaSource is not null)
 1699            {
 01700                info.MediaStreams = mediaSource.MediaStreams.ToArray();
 1701            }
 1702
 01703            return info;
 1704        }
 1705
 1706        private string GetImageCacheTag(User user)
 1707        {
 1708            try
 1709            {
 01710                return _imageProcessor.GetImageCacheTag(user);
 1711            }
 01712            catch (Exception e)
 1713            {
 01714                _logger.LogError(e, "Error getting image information for profile image");
 01715                return null;
 1716            }
 01717        }
 1718
 1719        /// <inheritdoc />
 1720        public void ReportNowViewingItem(string sessionId, string itemId)
 1721        {
 01722            ArgumentException.ThrowIfNullOrEmpty(itemId);
 1723
 01724            var item = _libraryManager.GetItemById(new Guid(itemId));
 01725            var session = GetSession(sessionId);
 1726
 01727            session.NowViewingItem = GetItemInfo(item, null);
 01728        }
 1729
 1730        /// <inheritdoc />
 1731        public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
 1732        {
 01733            var session = Sessions.FirstOrDefault(i =>
 01734                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1735
 01736            if (session is not null)
 1737            {
 01738                session.TranscodingInfo = info;
 1739            }
 01740        }
 1741
 1742        /// <inheritdoc />
 1743        public void ClearTranscodingInfo(string deviceId)
 1744        {
 01745            ReportTranscodingInfo(deviceId, null);
 01746        }
 1747
 1748        /// <inheritdoc />
 1749        public SessionInfo GetSession(string deviceId, string client, string version)
 1750        {
 01751            return Sessions.FirstOrDefault(i =>
 01752                string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
 01753                    && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
 1754        }
 1755
 1756        /// <inheritdoc />
 1757        public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, st
 1758        {
 01759            ArgumentNullException.ThrowIfNull(info);
 1760
 01761            var user = info.UserId.IsEmpty()
 01762                ? null
 01763                : _userManager.GetUserById(info.UserId);
 1764
 01765            appVersion = string.IsNullOrEmpty(appVersion)
 01766                ? info.AppVersion
 01767                : appVersion;
 1768
 01769            var deviceName = info.DeviceName;
 01770            var appName = info.AppName;
 1771
 01772            if (string.IsNullOrEmpty(deviceId))
 1773            {
 01774                deviceId = info.DeviceId;
 1775            }
 1776
 1777            // Prevent argument exception
 01778            if (string.IsNullOrEmpty(appVersion))
 1779            {
 01780                appVersion = "1";
 1781            }
 1782
 01783            return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
 1784        }
 1785
 1786        /// <inheritdoc />
 1787        public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpo
 1788        {
 1789            var items = _deviceManager.GetDevices(new DeviceQuery
 1790            {
 1791                AccessToken = token,
 1792                Limit = 1
 1793            }).Items;
 1794
 1795            if (items.Count == 0)
 1796            {
 1797                return null;
 1798            }
 1799
 1800            return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false)
 1801        }
 1802
 1803        /// <inheritdoc />
 1804        public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
 1805        {
 01806            CheckDisposed();
 1807
 01808            var adminUserIds = _userManager.Users
 01809                .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
 01810                .Select(i => i.Id)
 01811                .ToList();
 1812
 01813            return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
 1814        }
 1815
 1816        /// <inheritdoc />
 1817        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, Cancellati
 1818        {
 01819            CheckDisposed();
 1820
 01821            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
 1822
 01823            if (sessions.Count == 0)
 1824            {
 01825                return Task.CompletedTask;
 1826            }
 1827
 01828            return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
 1829        }
 1830
 1831        /// <inheritdoc />
 1832        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken 
 1833        {
 01834            CheckDisposed();
 1835
 01836            var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
 01837            return SendMessageToSessions(sessions, name, data, cancellationToken);
 1838        }
 1839
 1840        /// <inheritdoc />
 1841        public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationTok
 1842        {
 01843            CheckDisposed();
 1844
 01845            var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
 1846
 01847            return SendMessageToSessions(sessions, name, data, cancellationToken);
 1848        }
 1849
 1850        /// <inheritdoc />
 1851        public async ValueTask DisposeAsync()
 1852        {
 1853            if (_disposed)
 1854            {
 1855                return;
 1856            }
 1857
 1858            foreach (var session in _activeConnections.Values)
 1859            {
 1860                await session.DisposeAsync().ConfigureAwait(false);
 1861            }
 1862
 1863            if (_idleTimer is not null)
 1864            {
 1865                await _idleTimer.DisposeAsync().ConfigureAwait(false);
 1866                _idleTimer = null;
 1867            }
 1868
 1869            if (_inactiveTimer is not null)
 1870            {
 1871                await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
 1872                _inactiveTimer = null;
 1873            }
 1874
 1875            await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
 1876
 1877            _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
 1878            _disposed = true;
 1879        }
 1880
 1881        private async void OnApplicationStopping()
 1882        {
 1883            _logger.LogInformation("Sending shutdown notifications");
 1884            try
 1885            {
 1886                var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.Serv
 1887
 1888                await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(
 1889            }
 1890            catch (Exception ex)
 1891            {
 1892                _logger.LogError(ex, "Error sending server shutdown notifications");
 1893            }
 1894
 1895            // Close open websockets to allow Kestrel to shut down cleanly
 1896            foreach (var session in _activeConnections.Values)
 1897            {
 1898                await session.DisposeAsync().ConfigureAwait(false);
 1899            }
 1900
 1901            _activeConnections.Clear();
 1902        }
 1903    }
 1904}

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.Data.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.Data.Entities.User)
CreateSession(System.String,System.String,System.String,System.String,System.String,System.String,Jellyfin.Data.Entities.User)
GetUsers(MediaBrowser.Controller.Session.SessionInfo)
StartCheckTimers()
StopIdleCheckTimer()
StopInactiveCheckTimer()
GetNowPlayingItem(MediaBrowser.Controller.Session.SessionInfo,System.Guid)
OnPlaybackStart(Jellyfin.Data.Entities.User,MediaBrowser.Controller.Entities.BaseItem)
OnPlaybackProgress(MediaBrowser.Model.Session.PlaybackProgressInfo)
OnPlaybackProgress(Jellyfin.Data.Entities.User,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Session.PlaybackProgressInfo)
UpdatePlaybackSettings(Jellyfin.Data.Entities.User,MediaBrowser.Model.Session.PlaybackProgressInfo,MediaBrowser.Controller.Entities.UserItemData)
OnPlaybackStopped(Jellyfin.Data.Entities.User,MediaBrowser.Controller.Entities.BaseItem,System.Nullable`1<System.Int64>,System.Boolean)
GetSession(System.String,System.Boolean)
GetSessionToRemoteControl(System.String)
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.Data.Entities.User)
TranslateItemForInstantMix(System.Guid,Jellyfin.Data.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.Data.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.Data.Entities.Security.Device,System.String,System.String,System.String)
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)