diff options
| -rw-r--r-- | Directory.Packages.props | 6 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Session/SessionManager.cs | 85 | ||||
| -rw-r--r-- | Jellyfin.Api/Constants/Policies.cs | 5 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/SubtitleController.cs | 6 | ||||
| -rw-r--r-- | Jellyfin.Data/Entities/User.cs | 1 | ||||
| -rw-r--r-- | Jellyfin.Data/Enums/PermissionKind.cs | 7 | ||||
| -rw-r--r-- | Jellyfin.Server.Implementations/Users/UserManager.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 1 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Session/SessionInfo.cs | 6 | ||||
| -rw-r--r-- | MediaBrowser.Model/Configuration/ServerConfiguration.cs | 7 | ||||
| -rw-r--r-- | MediaBrowser.Model/Users/UserPolicy.cs | 8 | ||||
| -rw-r--r-- | src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj | 4 | ||||
| -rw-r--r-- | src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 19 |
13 files changed, 132 insertions, 25 deletions
diff --git a/Directory.Packages.props b/Directory.Packages.props index 0619c4ef9..d4d2e8455 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,9 +2,7 @@ <PropertyGroup> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> </PropertyGroup> - <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> - <ItemGroup Label="Package Dependencies"> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" /> @@ -72,9 +70,9 @@ <PackageVersion Include="SkiaSharp" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> - <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> + <PackageVersion Include="Svg.Skia" Version="1.0.0.2" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="System.Globalization" Version="4.3.0" /> @@ -90,4 +88,4 @@ <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> <PackageVersion Include="xunit" Version="2.5.3" /> </ItemGroup> -</Project> +</Project>
\ No newline at end of file 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> diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index c465c4ad0..09e103c6b 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -18,11 +18,11 @@ <ItemGroup> <PackageReference Include="BlurHashSharp" /> <PackageReference Include="BlurHashSharp.SkiaSharp" /> + <PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" /> <PackageReference Include="SkiaSharp" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" /> - <PackageReference Include="SkiaSharp.Svg" /> <PackageReference Include="SkiaSharp.HarfBuzz" /> - <PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" /> + <PackageReference Include="Svg.Skia" /> </ItemGroup> <ItemGroup> diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index b8290c5fc..5721e2882 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -2,19 +2,15 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; -using System.Security.Cryptography.Xml; using BlurHashSharp.SkiaSharp; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Drawing; using Microsoft.Extensions.Logging; using SkiaSharp; -using static System.Net.Mime.MediaTypeNames; -using SKSvg = SkiaSharp.Extended.Svg.SKSvg; +using Svg.Skia; namespace Jellyfin.Drawing.Skia; @@ -147,10 +143,16 @@ public class SkiaEncoder : IImageEncoder var extension = Path.GetExtension(path.AsSpan()); if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase)) { - var svg = new SKSvg(); + using var svg = new SKSvg(); try { using var picture = svg.Load(path); + if (picture is null) + { + _logger.LogError("Unable to determine image dimensions for {FilePath}", path); + return default; + } + return new ImageDimensions(Convert.ToInt32(picture.CullRect.Width), Convert.ToInt32(picture.CullRect.Height)); } catch (FormatException skiaColorException) @@ -313,10 +315,7 @@ public class SkiaEncoder : IImageEncoder private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) { - var needsFlip = origin == SKEncodedOrigin.LeftBottom - || origin == SKEncodedOrigin.LeftTop - || origin == SKEncodedOrigin.RightBottom - || origin == SKEncodedOrigin.RightTop; + var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom or SKEncodedOrigin.RightTop; var rotated = needsFlip ? new SKBitmap(bitmap.Height, bitmap.Width) : new SKBitmap(bitmap.Width, bitmap.Height); |
