aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server.Implementations')
-rw-r--r--Jellyfin.Server.Implementations/Devices/DeviceManager.cs2
-rw-r--r--Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs27
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs48
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs159
4 files changed, 131 insertions, 105 deletions
diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
index 5e5d52b6b..d8d1b6fa8 100644
--- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
+++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs
@@ -179,7 +179,7 @@ namespace Jellyfin.Server.Implementations.Devices
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
.AsAsyncEnumerable();
- if (userId.HasValue)
+ if (!userId.IsNullOrEmpty())
{
var user = _userManager.GetUserById(userId.Value);
if (user is null)
diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
index bb8d4dd14..a88989840 100644
--- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
@@ -16,23 +16,28 @@ public static class ServiceCollectionExtensions
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
/// </summary>
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
+ /// <param name="disableSecondLevelCache">Whether second level cache disabled..</param>
/// <returns>The updated service collection.</returns>
- public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
+ public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache)
{
- serviceCollection.AddEFSecondLevelCache(options =>
- options.UseMemoryCacheProvider()
- .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
- .DisableLogging(true)
- .UseCacheKeyPrefix("EF_")
- // Don't cache null values. Remove this optional setting if it's not necessary.
- .SkipCachingResults(result =>
- result.Value is null || (result.Value is EFTableRows rows && rows.RowsCount == 0)));
+ if (!disableSecondLevelCache)
+ {
+ serviceCollection.AddEFSecondLevelCache(options =>
+ options.UseMemoryCacheProvider()
+ .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
+ .UseCacheKeyPrefix("EF_")
+ // Don't cache null values. Remove this optional setting if it's not necessary.
+ .SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
+ }
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
- opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}")
- .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
+ var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
+ if (!disableSecondLevelCache)
+ {
+ dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
+ }
});
return serviceCollection;
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index fed5dab69..bb32b7c20 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -81,6 +81,12 @@ public class TrickplayManager : ITrickplayManager
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
var options = _config.Configuration.TrickplayOptions;
+ if (options.Interval < 1000)
+ {
+ _logger.LogWarning("Trickplay image interval {Interval} is too small, reset to the minimum valid value of 1000", options.Interval);
+ options.Interval = 1000;
+ }
+
foreach (var width in options.WidthResolutions)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -106,18 +112,11 @@ public class TrickplayManager : ITrickplayManager
}
var imgTempDir = string.Empty;
- var outputDir = GetTrickplayDirectory(video, width);
using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
try
{
- 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));
@@ -129,22 +128,47 @@ public class TrickplayManager : ITrickplayManager
}
var mediaPath = mediaSource.Path;
+ if (!File.Exists(mediaPath))
+ {
+ _logger.LogWarning("Media not found at {Path} for item {ItemID}", mediaPath, video.Id);
+ return;
+ }
+
+ // The width has to be even, otherwise a lot of filters will not be able to sample it
+ var actualWidth = 2 * (width / 2);
+
+ // Force using the video width when the trickplay setting has a too large width
+ if (mediaSource.VideoStream.Width is not null && mediaSource.VideoStream.Width < width)
+ {
+ _logger.LogWarning("Video width {VideoWidth} is smaller than trickplay setting {TrickPlayWidth}, using video width for thumbnails", mediaSource.VideoStream.Width, width);
+ actualWidth = 2 * ((int)mediaSource.VideoStream.Width / 2);
+ }
+
+ var outputDir = GetTrickplayDirectory(video, actualWidth);
+
+ if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(actualWidth))
+ {
+ _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting", video.Id);
+ return;
+ }
+
var mediaStream = mediaSource.VideoStream;
var container = mediaSource.Container;
- _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
+ _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", actualWidth, mediaPath, video.Id);
imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
mediaPath,
container,
mediaSource,
mediaStream,
- width,
+ actualWidth,
TimeSpan.FromMilliseconds(options.Interval),
options.EnableHwAcceleration,
options.EnableHwEncoding,
options.ProcessThreads,
options.Qscale,
options.ProcessPriority,
+ options.EnableKeyFrameOnlyExtraction,
_encodingHelper,
cancellationToken).ConfigureAwait(false);
@@ -159,7 +183,7 @@ public class TrickplayManager : ITrickplayManager
.ToList();
// Create tiles
- var trickplayInfo = CreateTiles(images, width, options, outputDir);
+ var trickplayInfo = CreateTiles(images, actualWidth, options, outputDir);
// Save tiles info
try
@@ -207,7 +231,7 @@ public class TrickplayManager : ITrickplayManager
throw new ArgumentException("Can't create trickplay from 0 images.");
}
- var workDir = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N"));
+ var workDir = Path.Combine(_appPaths.TempDirectory, "trickplay_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
var trickplayInfo = new TrickplayInfo
@@ -250,7 +274,7 @@ public class TrickplayManager : ITrickplayManager
}
// Update bitrate
- var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000));
+ var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 41f1ac351..5753e75c9 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -1,7 +1,7 @@
#pragma warning disable CA1307
-#pragma warning disable CA1309 // Use ordinal string comparison - EF can't translate this
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -47,6 +47,8 @@ namespace Jellyfin.Server.Implementations.Users
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IDictionary<Guid, User> _users;
+
/// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class.
/// </summary>
@@ -84,30 +86,29 @@ namespace Jellyfin.Server.Implementations.Users
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
+
+ _users = new ConcurrentDictionary<Guid, User>();
+ using var dbContext = _dbProvider.CreateDbContext();
+ foreach (var user in dbContext.Users
+ .AsSplitQuery()
+ .Include(user => user.Permissions)
+ .Include(user => user.Preferences)
+ .Include(user => user.AccessSchedules)
+ .Include(user => user.ProfileImage)
+ .AsEnumerable())
+ {
+ _users.Add(user.Id, user);
+ }
}
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
/// <inheritdoc/>
- public IEnumerable<User> Users
- {
- get
- {
- using var dbContext = _dbProvider.CreateDbContext();
- return GetUsersInternal(dbContext).ToList();
- }
- }
+ public IEnumerable<User> Users => _users.Values;
/// <inheritdoc/>
- public IEnumerable<Guid> UsersIds
- {
- get
- {
- using var dbContext = _dbProvider.CreateDbContext();
- return dbContext.Users.Select(u => u.Id).ToList();
- }
- }
+ public IEnumerable<Guid> UsersIds => _users.Keys;
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
@@ -123,8 +124,8 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- using var dbContext = _dbProvider.CreateDbContext();
- return GetUsersInternal(dbContext).FirstOrDefault(u => u.Id.Equals(id));
+ _users.TryGetValue(id, out var user);
+ return user;
}
/// <inheritdoc/>
@@ -135,9 +136,7 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Invalid username", nameof(name));
}
- using var dbContext = _dbProvider.CreateDbContext();
- return GetUsersInternal(dbContext)
- .FirstOrDefault(u => string.Equals(u.Username, name));
+ return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
}
/// <inheritdoc/>
@@ -202,6 +201,8 @@ namespace Jellyfin.Server.Implementations.Users
user.AddDefaultPermissions();
user.AddDefaultPreferences();
+ _users.Add(user.Id, user);
+
return user;
}
@@ -236,46 +237,40 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/>
public async Task DeleteUserAsync(Guid userId)
{
- var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ if (!_users.TryGetValue(userId, out var user))
+ {
+ throw new ResourceNotFoundException(nameof(userId));
+ }
- await using (dbContext.ConfigureAwait(false))
+ if (_users.Count == 1)
{
- var user = await dbContext.Users
- .AsSingleQuery()
- .Include(u => u.Permissions)
- .FirstOrDefaultAsync(u => u.Id.Equals(userId))
- .ConfigureAwait(false);
- if (user is null)
- {
- throw new ResourceNotFoundException(nameof(userId));
- }
+ throw new InvalidOperationException(string.Format(
+ CultureInfo.InvariantCulture,
+ "The user '{0}' cannot be deleted because there must be at least one user in the system.",
+ user.Username));
+ }
- if (await dbContext.Users.CountAsync().ConfigureAwait(false) == 1)
- {
- throw new InvalidOperationException(string.Format(
+ if (user.HasPermission(PermissionKind.IsAdministrator)
+ && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
+ {
+ throw new ArgumentException(
+ string.Format(
CultureInfo.InvariantCulture,
- "The user '{0}' cannot be deleted because there must be at least one user in the system.",
- user.Username));
- }
-
- if (user.HasPermission(PermissionKind.IsAdministrator)
- && await dbContext.Users
- .CountAsync(u => u.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value))
- .ConfigureAwait(false) == 1)
- {
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
- user.Username),
- nameof(userId));
- }
+ "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
+ user.Username),
+ nameof(userId));
+ }
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
-
- await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
}
+
+ _users.Remove(userId);
+
+ await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
}
/// <inheritdoc/>
@@ -542,23 +537,23 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc />
public async Task InitializeAsync()
{
- var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
+ // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
+ if (_users.Any())
{
- // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
- if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
- {
- return;
- }
+ return;
+ }
- var defaultName = Environment.UserName;
- if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
- {
- defaultName = "MyJellyfinUser";
- }
+ var defaultName = Environment.UserName;
+ if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
+ {
+ defaultName = "MyJellyfinUser";
+ }
- _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
+ _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
@@ -605,9 +600,12 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- var user = await GetUsersInternal(dbContext)
- .FirstOrDefaultAsync(u => u.Id.Equals(userId))
- .ConfigureAwait(false)
+ var user = dbContext.Users
+ .Include(u => u.Permissions)
+ .Include(u => u.Preferences)
+ .Include(u => u.AccessSchedules)
+ .Include(u => u.ProfileImage)
+ .FirstOrDefault(u => u.Id.Equals(userId))
?? throw new ArgumentException("No user exists with given Id!");
user.SubtitleMode = config.SubtitleMode;
@@ -635,6 +633,7 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Update(user);
+ _users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
@@ -645,9 +644,12 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
- var user = await GetUsersInternal(dbContext)
- .FirstOrDefaultAsync(u => u.Id.Equals(userId))
- .ConfigureAwait(false)
+ var user = dbContext.Users
+ .Include(u => u.Permissions)
+ .Include(u => u.Preferences)
+ .Include(u => u.AccessSchedules)
+ .Include(u => u.ProfileImage)
+ .FirstOrDefault(u => u.Id.Equals(userId))
?? throw new ArgumentException("No user exists with given Id!");
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
@@ -708,6 +710,7 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user);
+ _users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
@@ -728,6 +731,7 @@ namespace Jellyfin.Server.Implementations.Users
}
user.ProfileImage = null;
+ _users[user.Id] = user;
}
internal static void ThrowIfInvalidUsername(string name)
@@ -874,15 +878,8 @@ namespace Jellyfin.Server.Implementations.Users
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
{
dbContext.Users.Update(user);
+ _users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
-
- private IQueryable<User> GetUsersInternal(JellyfinDbContext dbContext)
- => dbContext.Users
- .AsSplitQuery()
- .Include(user => user.Permissions)
- .Include(user => user.Preferences)
- .Include(user => user.AccessSchedules)
- .Include(user => user.ProfileImage);
}
}