aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server.Implementations')
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs6
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs6
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemMapper.cs2
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs35
-rw-r--r--Jellyfin.Server.Implementations/Item/ChapterRepository.cs19
-rw-r--r--Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs11
-rw-r--r--Jellyfin.Server.Implementations/Item/PeopleRepository.cs8
-rw-r--r--Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs11
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs2
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs20
10 files changed, 81 insertions, 39 deletions
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index fe987b9d86..ba24dc3864 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -58,9 +58,9 @@ public class ActivityManager : IActivityManager
{
// TODO switch to LeftJoin in .NET 10.
var entries = from a in dbContext.ActivityLogs
- join u in dbContext.Users on a.UserId equals u.Id into ugj
- from u in ugj.DefaultIfEmpty()
- select new ExpandedActivityLog { ActivityLog = a, Username = u.Username };
+ join u in dbContext.Users on a.UserId equals u.Id into ugj
+ from u in ugj.DefaultIfEmpty()
+ select new ExpandedActivityLog { ActivityLog = a, Username = u.Username };
if (query.HasUserId is not null)
{
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
index 09d68e4451..a88904c727 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
@@ -75,9 +75,9 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
eventArgs.DeviceName),
notificationType,
user.Id)
- {
- ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture),
- })
+ {
+ ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture),
+ })
.ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs
index 736388e9eb..c64e6ac068 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs
@@ -26,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Item;
/// <summary>
/// Handles mapping between BaseItemEntity (database) and BaseItemDto (domain) objects.
/// </summary>
-internal static class BaseItemMapper
+public static class BaseItemMapper
{
/// <summary>
/// This holds all the types in the running assemblies
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
index 2e0b982152..f33a65a703 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
@@ -824,6 +824,26 @@ public sealed partial class BaseItemRepository
}
}
+ if (filter.SubtitleLanguages.Count > 0)
+ {
+ var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, filter.SubtitleLanguages));
+ baseQuery = baseQuery
+ .Where(e =>
+ (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle
+ && (filter.SubtitleLanguages.Contains(f.Language) || (filter.SubtitleLanguages.Contains("und") && string.IsNullOrEmpty(f.Language)))))
+ || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
+ }
+
+ if (filter.AudioLanguages.Count > 0)
+ {
+ var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Audio, filter.AudioLanguages));
+ baseQuery = baseQuery
+ .Where(e =>
+ (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio
+ && (filter.AudioLanguages.Contains(f.Language) || (filter.AudioLanguages.Contains("und") && string.IsNullOrEmpty(f.Language)))))
+ || (e.IsFolder && foldersWithAudio.Contains(e.Id)));
+ }
+
if (filter.HasChapterImages.HasValue)
{
var hasChapterImages = filter.HasChapterImages.Value;
@@ -1007,6 +1027,15 @@ public sealed partial class BaseItemRepository
baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter));
}
+ if (filter.LinkedChildAncestorIds.Length > 0)
+ {
+ // Keep folder-like items (BoxSets, Playlists) whose linked children descend from any of the requested ancestor ids.
+ var linkedChildAncestorIds = filter.LinkedChildAncestorIds;
+ baseQuery = baseQuery.Where(e => context.LinkedChildren.Any(lc =>
+ lc.ParentId == e.Id
+ && lc.Child!.Parents!.Any(a => linkedChildAncestorIds.Contains(a.ParentItemId))));
+ }
+
if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
{
baseQuery = baseQuery
@@ -1068,8 +1097,12 @@ public sealed partial class BaseItemRepository
if (filter.VideoTypes.Length > 0)
{
+ // Dvds and Blu-rays can either be stored in a folder structure or as an iso file
+ // => to find all matches we need to check both: VideoType and IsoType
+ // alternatively, we could provide specific IsoType filters
var videoTypeBs = filter.VideoTypes.Select(vt => $"\"VideoType\":\"{vt}\"").ToArray();
- Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f));
+ var isoTypeBs = filter.VideoTypes.Select(vt => $"\"IsoType\":\"{vt}\"").ToArray();
+ Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f)) || isoTypeBs.Any(f => e.Data!.Contains(f));
baseQuery = baseQuery.WhereItemOrDescendantMatches(context, hasVideoType);
}
diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs
index 98700f3224..f7d76517e1 100644
--- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs
@@ -55,6 +55,7 @@ public class ChapterRepository : IChapterRepository
{
using var context = _dbProvider.CreateDbContext();
return context.Chapters.AsNoTracking().Where(e => e.ItemId.Equals(baseItemId))
+ .OrderBy(e => e.StartPositionTicks)
.Select(e => new
{
chapter = e,
@@ -69,18 +70,16 @@ public class ChapterRepository : IChapterRepository
public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters)
{
using var context = _dbProvider.CreateDbContext();
- using (var transaction = context.Database.BeginTransaction())
+ using var transaction = context.Database.BeginTransaction();
+ context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete();
+ for (var i = 0; i < chapters.Count; i++)
{
- context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete();
- for (var i = 0; i < chapters.Count; i++)
- {
- var chapter = chapters[i];
- context.Chapters.Add(Map(chapter, i, itemId));
- }
-
- context.SaveChanges();
- transaction.Commit();
+ var chapter = chapters[i];
+ context.Chapters.Add(Map(chapter, i, itemId));
}
+
+ context.SaveChanges();
+ transaction.Commit();
}
/// <inheritdoc />
diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
index dd0446f49a..7fa33c8639 100644
--- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
@@ -55,6 +55,17 @@ public class MediaStreamRepository : IMediaStreamRepository
return TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter).AsEnumerable().Select(Map).ToArray();
}
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ return context.MediaStreamInfos
+ .Where(e => e.StreamType == (MediaStreamTypeEntity)mediaStreamType)
+ .Select(s => string.IsNullOrEmpty(s.Language) ? "und" : s.Language) // und = undetermined
+ .Distinct()
+ .ToArray();
+ }
+
private string? GetPathToSave(string? path)
{
if (path is null)
diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
index 88bf6fa1bb..6062aaca2f 100644
--- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
@@ -110,10 +110,10 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
var existingPersons = context.Peoples.Select(e => new
- {
- item = e,
- SelectionKey = e.Name.ToLower() + "-" + e.PersonType
- })
+ {
+ item = e,
+ SelectionKey = e.Name.ToLower() + "-" + e.PersonType
+ })
.Where(p => personKeys.Contains(p.SelectionKey))
.Select(f => f.item)
.ToArray();
diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
index 249df476a0..be98f93dab 100644
--- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
+++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
@@ -81,6 +81,8 @@ public class MediaSegmentManager : IMediaSegmentManager
foreach (var provider in providers)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
if (!await provider.Supports(baseItem).ConfigureAwait(false))
{
_logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path);
@@ -146,6 +148,15 @@ public class MediaSegmentManager : IMediaSegmentManager
await CreateSegmentAsync(segment, providerId).ConfigureAwait(false);
}
}
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
+ catch (Exception ex) when (cancellationToken.IsCancellationRequested)
+ {
+ _logger.LogDebug(ex, "Provider {ProviderName} aborted segment extraction for {MediaPath} due to shutdown", provider.Name, baseItem.Path);
+ break;
+ }
catch (Exception ex)
{
_logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path);
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index e3fe517c49..8657cb7dbb 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -302,7 +302,7 @@ namespace Jellyfin.Server.Implementations.Security
}
else if (!escaped && token == '=')
{
- key = authorizationHeader[start.. i].Trim().ToString();
+ key = authorizationHeader[start..i].Trim().ToString();
start = i + 1;
}
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 8c0cbbd448..37c4106496 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -1,4 +1,3 @@
-#pragma warning disable CA1307
#pragma warning disable RS0030 // Do not use banned APIs
using System;
@@ -161,12 +160,8 @@ namespace Jellyfin.Server.Implementations.Users
using var dbContext = _dbProvider.CreateDbContext();
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
-#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
-#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
return UserQuery(dbContext)
- .FirstOrDefault(u => u.Username.ToUpper() == name.ToUpper());
-#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
-#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
+ .FirstOrDefault(u => u.NormalizedUsername == name.ToUpperInvariant());
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
}
@@ -187,10 +182,8 @@ namespace Jellyfin.Server.Implementations.Users
await using (dbContext.ConfigureAwait(false))
{
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
-#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
-#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
if (await dbContext.Users
- .AnyAsync(u => u.Username.ToUpper() == newName.ToUpper() && u.Id != userId)
+ .AnyAsync(u => u.NormalizedUsername == newName.ToUpperInvariant() && u.Id != userId)
.ConfigureAwait(false))
{
throw new ArgumentException(string.Format(
@@ -198,8 +191,6 @@ namespace Jellyfin.Server.Implementations.Users
"A user with the name '{0}' already exists.",
newName));
}
-#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
-#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
user = await UserQuery(dbContext)
@@ -208,6 +199,7 @@ namespace Jellyfin.Server.Implementations.Users
.ConfigureAwait(false)
?? throw new ResourceNotFoundException(nameof(userId));
user.Username = newName;
+ user.NormalizedUsername = newName.ToUpperInvariant();
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
}
}
@@ -257,10 +249,8 @@ namespace Jellyfin.Server.Implementations.Users
await using (dbContext.ConfigureAwait(false))
{
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
-#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
-#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
if (await dbContext.Users
- .AnyAsync(u => u.Username.ToUpper() == name.ToUpper())
+ .AnyAsync(u => u.NormalizedUsername == name.ToUpperInvariant())
.ConfigureAwait(false))
{
throw new ArgumentException(string.Format(
@@ -268,8 +258,6 @@ namespace Jellyfin.Server.Implementations.Users
"A user with the name '{0}' already exists.",
name));
}
-#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
-#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);