aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs85
-rw-r--r--Jellyfin.Api/Constants/Policies.cs5
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs6
-rw-r--r--Jellyfin.Data/Entities/User.cs1
-rw-r--r--Jellyfin.Data/Enums/PermissionKind.cs7
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs2
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs1
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs6
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs7
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs8
10 files changed, 119 insertions, 9 deletions
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<SessionManager> _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<SessionManager> 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();
}
/// <summary>
@@ -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/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index 53841b0c4..02fdef150 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -84,4 +84,9 @@ public static class Policies
/// Policy name for managing LiveTV.
/// </summary>
public const string LiveTvManagement = "LiveTvManagement";
+
+ /// <summary>
+ /// Policy name for accessing subtitles management.
+ /// </summary>
+ public const string SubtitleManagement = "SubtitleManagement";
}
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index fb89e9610..c9e256af3 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -115,7 +115,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="200">Subtitles retrieved.</response>
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
[HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
- [Authorize]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
[FromRoute, Required] Guid itemId,
@@ -135,7 +135,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="204">Subtitle downloaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
- [Authorize]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DownloadRemoteSubtitles(
[FromRoute, Required] Guid itemId,
@@ -399,7 +399,7 @@ public class SubtitleController : BaseJellyfinApiController
/// <response code="204">Subtitle uploaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Videos/{itemId}/Subtitles")]
- [Authorize(Policy = Policies.RequiresElevation)]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UploadSubtitle(
[FromRoute, Required] Guid itemId,
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 5c3e0338d..ea0de3016 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -505,6 +505,7 @@ namespace Jellyfin.Data.Entities
Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false));
+ Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false));
}
/// <summary>
diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs
index 40280b95e..6644f0151 100644
--- a/Jellyfin.Data/Enums/PermissionKind.cs
+++ b/Jellyfin.Data/Enums/PermissionKind.cs
@@ -113,6 +113,11 @@ namespace Jellyfin.Data.Enums
/// <summary>
/// Whether the user can create, modify and delete collections.
/// </summary>
- EnableCollectionManagement = 21
+ EnableCollectionManagement = 21,
+
+ /// <summary>
+ /// Whether the user can edit subtitles.
+ /// </summary>
+ EnableSubtitleManagement = 22
}
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index b2cb589f7..edae4cfc5 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -359,6 +359,7 @@ namespace Jellyfin.Server.Implementations.Users
ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
EnableCollectionManagement = user.HasPermission(PermissionKind.EnableCollectionManagement),
+ EnableSubtitleManagement = user.HasPermission(PermissionKind.EnableSubtitleManagement),
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
AllowedTags = user.GetPreference(PreferenceKind.AllowedTags),
@@ -683,6 +684,7 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
+ user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index cb1680558..b7e71a81d 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -82,6 +82,7 @@ namespace Jellyfin.Server.Extensions
options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
+ options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtitleManagement));
options.AddPolicy(
Policies.RequiresElevation,
policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
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
@@ -110,6 +110,12 @@ namespace MediaBrowser.Controller.Session
public DateTime LastPlaybackCheckIn { get; set; }
/// <summary>
+ /// Gets or sets the last paused date.
+ /// </summary>
+ /// <value>The last paused date.</value>
+ public DateTime? LastPausedDate { get; set; }
+
+ /// <summary>
/// Gets or sets the name of the device.
/// </summary>
/// <value>The name of the device.</value>
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
@@ -159,6 +159,13 @@ public class ServerConfiguration : BaseApplicationConfiguration
public int MaxAudiobookResume { get; set; } = 5;
/// <summary>
+ /// 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.
+ /// </summary>
+ /// <value>The close inactive session threshold in minutes. 0 to disable.</value>
+ public int InactiveSessionThreshold { get; set; } = 10;
+
+ /// <summary>
/// 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
/// different directories and files.
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 8354c60ef..f5aff07db 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -15,6 +15,7 @@ namespace MediaBrowser.Model.Users
{
IsHidden = true;
EnableCollectionManagement = false;
+ EnableSubtitleManagement = false;
EnableContentDeletion = false;
EnableContentDeletionFromFolders = Array.Empty<string>();
@@ -84,6 +85,13 @@ namespace MediaBrowser.Model.Users
public bool EnableCollectionManagement { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether this instance can manage subtitles.
+ /// </summary>
+ /// <value><c>true</c> if this instance is allowed; otherwise, <c>false</c>.</value>
+ [DefaultValue(false)]
+ public bool EnableSubtitleManagement { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating whether this instance is disabled.
/// </summary>
/// <value><c>true</c> if this instance is disabled; otherwise, <c>false</c>.</value>