diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index e935f7e5e..dc59a4523 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -19,6 +19,7 @@ using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; @@ -48,6 +49,7 @@ namespace Emby.Server.Implementations.Session public sealed class SessionManager : ISessionManager, IAsyncDisposable { private readonly IUserDataManager _userDataManager; + private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly IEventManager _eventManager; private readonly ILibraryManager _libraryManager; @@ -63,6 +65,7 @@ namespace Emby.Server.Implementations.Session = new(StringComparer.OrdinalIgnoreCase); private Timer _idleTimer; + private Timer _inactiveTimer; private DtoOptions _itemInfoDtoOptions; private bool _disposed = false; @@ -71,6 +74,7 @@ namespace Emby.Server.Implementations.Session ILogger logger, IEventManager eventManager, IUserDataManager userDataManager, + IServerConfigurationManager config, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, @@ -84,6 +88,7 @@ namespace Emby.Server.Implementations.Session _logger = logger; _eventManager = eventManager; _userDataManager = userDataManager; + _config = config; _libraryManager = libraryManager; _userManager = userManager; _musicManager = musicManager; @@ -369,6 +374,15 @@ namespace Emby.Server.Implementations.Session session.LastPlaybackCheckIn = DateTime.UtcNow; } + if (info.IsPaused && session.LastPausedDate is null) + { + session.LastPausedDate = DateTime.UtcNow; + } + else if (!info.IsPaused) + { + session.LastPausedDate = null; + } + session.PlayState.IsPaused = info.IsPaused; session.PlayState.PositionTicks = info.PositionTicks; session.PlayState.MediaSourceId = info.MediaSourceId; @@ -536,9 +550,18 @@ namespace Emby.Server.Implementations.Session return users; } - private void StartIdleCheckTimer() + private void StartCheckTimers() { _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); + + if (_config.Configuration.InactiveSessionThreshold > 0) + { + _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + } + else + { + StopInactiveCheckTimer(); + } } private void StopIdleCheckTimer() @@ -550,6 +573,15 @@ namespace Emby.Server.Implementations.Session } } + private void StopInactiveCheckTimer() + { + if (_inactiveTimer is not null) + { + _inactiveTimer.Dispose(); + _inactiveTimer = null; + } + } + private async void CheckForIdlePlayback(object state) { var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null) @@ -585,13 +617,50 @@ namespace Emby.Server.Implementations.Session playingSessions = Sessions.Where(i => i.NowPlayingItem is not null) .ToList(); } - - if (playingSessions.Count == 0) + else { StopIdleCheckTimer(); } } + private async void CheckForInactiveSteams(object state) + { + var inactiveSessions = Sessions.Where(i => + i.NowPlayingItem is not null + && i.PlayState.IsPaused + && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionThreshold); + + foreach (var session in inactiveSessions) + { + _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session.Id, _config.Configuration.InactiveSessionThreshold); + + try + { + await SendPlaystateCommand( + session.Id, + session.Id, + new PlaystateRequest() + { + Command = PlaystateCommand.Stop, + ControllingUserId = session.UserId.ToString(), + SeekPositionTicks = session.PlayState?.PositionTicks + }, + CancellationToken.None).ConfigureAwait(true); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", session.Id); + } + } + + bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null); + + if (!playingSessions) + { + StopInactiveCheckTimer(); + } + } + private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId) { var item = session.FullNowPlayingItem; @@ -668,7 +737,7 @@ namespace Emby.Server.Implementations.Session eventArgs, _logger); - StartIdleCheckTimer(); + StartCheckTimers(); } /// @@ -762,7 +831,7 @@ namespace Emby.Server.Implementations.Session session.StartAutomaticProgress(info); } - StartIdleCheckTimer(); + StartCheckTimers(); } private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info) @@ -1798,6 +1867,12 @@ namespace Emby.Server.Implementations.Session _idleTimer = null; } + if (_inactiveTimer is not null) + { + await _inactiveTimer.DisposeAsync().ConfigureAwait(false); + _inactiveTimer = null; + } + await _shutdownCallback.DisposeAsync().ConfigureAwait(false); _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated; diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 25bf23d61..172d79a59 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -109,6 +109,12 @@ namespace MediaBrowser.Controller.Session /// The last playback check in. public DateTime LastPlaybackCheckIn { get; set; } + /// + /// Gets or sets the last paused date. + /// + /// The last paused date. + public DateTime? LastPausedDate { get; set; } + /// /// Gets or sets the name of the device. /// diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 1c9cc6c01..fe92251e9 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -158,6 +158,13 @@ public class ServerConfiguration : BaseApplicationConfiguration /// The remaining time in minutes. public int MaxAudiobookResume { get; set; } = 5; + /// + /// Gets or sets the threshold in minutes after a inactive session gets closed automatically. + /// If set to 0 the check for inactive sessions gets disabled. + /// + /// The close inactive session threshold in minutes. 0 to disable. + public int InactiveSessionThreshold { get; set; } = 10; + /// /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several