diff options
Diffstat (limited to 'Jellyfin.Server.Implementations')
4 files changed, 148 insertions, 136 deletions
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 0ed1578c7..7c4155bfc 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -26,6 +26,7 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="AsyncKeyedLock" /> <PackageReference Include="EFCoreSecondLevelCacheInterceptor" /> <PackageReference Include="System.Linq.Async" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" /> diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index b960feb7f..095bc9ed3 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using AsyncKeyedLock; using Jellyfin.Data.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; @@ -37,7 +38,7 @@ public class TrickplayManager : ITrickplayManager private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private readonly IApplicationPaths _appPaths; - private static readonly SemaphoreSlim _resourcePool = new(1, 1); + private static readonly AsyncNonKeyedLocker _resourcePool = new(1); private static readonly string[] _trickplayImgExtensions = { ".jpg" }; /// <summary> @@ -107,93 +108,92 @@ public class TrickplayManager : ITrickplayManager var imgTempDir = string.Empty; var outputDir = GetTrickplayDirectory(video, width); - await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - try + using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false)) { - if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width)) - { - _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); - return; - } - - // Extract images - // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. - var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); - - if (mediaSource is null) + try { - _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id); - return; - } + if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width)) + { + _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); + return; + } - var mediaPath = mediaSource.Path; - var mediaStream = mediaSource.VideoStream; - var container = mediaSource.Container; + // Extract images + // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. + var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); - _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); - imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( - mediaPath, - container, - mediaSource, - mediaStream, - width, - TimeSpan.FromMilliseconds(options.Interval), - options.EnableHwAcceleration, - options.ProcessThreads, - options.Qscale, - options.ProcessPriority, - _encodingHelper, - cancellationToken).ConfigureAwait(false); + if (mediaSource is null) + { + _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id); + return; + } - if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir)) - { - throw new InvalidOperationException("Null or invalid directory from media encoder."); - } + var mediaPath = mediaSource.Path; + var mediaStream = mediaSource.VideoStream; + var container = mediaSource.Container; + + _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); + imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( + mediaPath, + container, + mediaSource, + mediaStream, + width, + TimeSpan.FromMilliseconds(options.Interval), + options.EnableHwAcceleration, + options.ProcessThreads, + options.Qscale, + options.ProcessPriority, + _encodingHelper, + cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir)) + { + throw new InvalidOperationException("Null or invalid directory from media encoder."); + } - var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false) - .Select(i => i.FullName) - .OrderBy(i => i) - .ToList(); + var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false) + .Select(i => i.FullName) + .OrderBy(i => i) + .ToList(); - // Create tiles - var trickplayInfo = CreateTiles(images, width, options, outputDir); + // Create tiles + var trickplayInfo = CreateTiles(images, width, options, outputDir); - // Save tiles info - try - { - if (trickplayInfo is not null) + // Save tiles info + try { - trickplayInfo.ItemId = video.Id; - await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false); + if (trickplayInfo is not null) + { + trickplayInfo.ItemId = video.Id; + await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false); - _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); + _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); + } + else + { + throw new InvalidOperationException("Null trickplay tiles info from CreateTiles."); + } } - else + catch (Exception ex) { - throw new InvalidOperationException("Null trickplay tiles info from CreateTiles."); + _logger.LogError(ex, "Error while saving trickplay tiles info."); + + // Make sure no files stay in metadata folders on failure + // if tiles info wasn't saved. + Directory.Delete(outputDir, true); } } catch (Exception ex) { - _logger.LogError(ex, "Error while saving trickplay tiles info."); - - // Make sure no files stay in metadata folders on failure - // if tiles info wasn't saved. - Directory.Delete(outputDir, true); + _logger.LogError(ex, "Error creating trickplay images."); } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating trickplay images."); - } - finally - { - _resourcePool.Release(); - - if (!string.IsNullOrEmpty(imgTempDir)) + finally { - Directory.Delete(imgTempDir, true); + if (!string.IsNullOrEmpty(imgTempDir)) + { + Directory.Delete(imgTempDir, true); + } } } } @@ -382,7 +382,7 @@ public class TrickplayManager : ITrickplayManager if (trickplayInfo.ThumbnailCount > 0) { - const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}"; + const string urlFormat = "{0}.jpg?MediaSourceId={1}&api_key={2}"; const string decimalFormat = "{0:0.###}"; var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}"; @@ -431,7 +431,6 @@ public class TrickplayManager : ITrickplayManager .AppendFormat( CultureInfo.InvariantCulture, urlFormat, - width.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture), itemId.ToString("N"), apiKey) diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs deleted file mode 100644 index a471ea1d5..000000000 --- a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs +++ /dev/null @@ -1,64 +0,0 @@ -#pragma warning disable CS1591 - -using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Events; -using Jellyfin.Data.Queries; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; - -namespace Jellyfin.Server.Implementations.Users -{ - public sealed class DeviceAccessEntryPoint : IServerEntryPoint - { - private readonly IUserManager _userManager; - private readonly IDeviceManager _deviceManager; - private readonly ISessionManager _sessionManager; - - public DeviceAccessEntryPoint(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager) - { - _userManager = userManager; - _deviceManager = deviceManager; - _sessionManager = sessionManager; - } - - public Task RunAsync() - { - _userManager.OnUserUpdated += OnUserUpdated; - - return Task.CompletedTask; - } - - public void Dispose() - { - } - - private async void OnUserUpdated(object? sender, GenericEventArgs<User> e) - { - var user = e.Argument; - if (!user.HasPermission(PermissionKind.EnableAllDevices)) - { - await UpdateDeviceAccess(user).ConfigureAwait(false); - } - } - - private async Task UpdateDeviceAccess(User user) - { - var existing = (await _deviceManager.GetDevices(new DeviceQuery - { - UserId = user.Id - }).ConfigureAwait(false)).Items; - - foreach (var device in existing) - { - if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId)) - { - await _sessionManager.Logout(device).ConfigureAwait(false); - } - } - } - } -} diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs new file mode 100644 index 000000000..e40b541a3 --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs @@ -0,0 +1,76 @@ +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; +using Jellyfin.Data.Queries; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; +using Microsoft.Extensions.Hosting; + +namespace Jellyfin.Server.Implementations.Users; + +/// <summary> +/// <see cref="IHostedService"/> responsible for managing user device permissions. +/// </summary> +public sealed class DeviceAccessHost : IHostedService +{ + private readonly IUserManager _userManager; + private readonly IDeviceManager _deviceManager; + private readonly ISessionManager _sessionManager; + + /// <summary> + /// Initializes a new instance of the <see cref="DeviceAccessHost"/> class. + /// </summary> + /// <param name="userManager">The <see cref="IUserManager"/>.</param> + /// <param name="deviceManager">The <see cref="IDeviceManager"/>.</param> + /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param> + public DeviceAccessHost(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager) + { + _userManager = userManager; + _deviceManager = deviceManager; + _sessionManager = sessionManager; + } + + /// <inheritdoc /> + public Task StartAsync(CancellationToken cancellationToken) + { + _userManager.OnUserUpdated += OnUserUpdated; + + return Task.CompletedTask; + } + + /// <inheritdoc /> + public Task StopAsync(CancellationToken cancellationToken) + { + _userManager.OnUserUpdated -= OnUserUpdated; + + return Task.CompletedTask; + } + + private async void OnUserUpdated(object? sender, GenericEventArgs<User> e) + { + var user = e.Argument; + if (!user.HasPermission(PermissionKind.EnableAllDevices)) + { + await UpdateDeviceAccess(user).ConfigureAwait(false); + } + } + + private async Task UpdateDeviceAccess(User user) + { + var existing = (await _deviceManager.GetDevices(new DeviceQuery + { + UserId = user.Id + }).ConfigureAwait(false)).Items; + + foreach (var device in existing) + { + if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId)) + { + await _sessionManager.Logout(device).ConfigureAwait(false); + } + } + } +} |
