diff options
Diffstat (limited to 'Jellyfin.Server.Implementations')
29 files changed, 277 insertions, 164 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/Library/LyricDownloadFailureLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs index 5f4864e953..cfd1cbe05b 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs @@ -37,7 +37,7 @@ public class LyricDownloadFailureLogger : IEventConsumer<LyricDownloadFailureEve await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("LyricDownloadFailureFromForItem"), + _localizationManager.GetServerLocalizedString("LyricDownloadFailureFromForItem"), eventArgs.Provider, GetItemName(eventArgs.Item)), "LyricDownloadFailure", diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs index 8fe380e4f4..24146210c6 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs @@ -37,7 +37,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Library await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("SubtitleDownloadFailureFromForItem"), + _localizationManager.GetServerLocalizedString("SubtitleDownloadFailureFromForItem"), eventArgs.Provider, GetItemName(eventArgs.Item)), "SubtitleDownloadFailure", diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs index 1a8931a6dc..df526977a2 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs @@ -35,7 +35,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("FailedLoginAttemptWithUserName"), + _localizationManager.GetServerLocalizedString("FailedLoginAttemptWithUserName"), eventArgs.Username), "AuthenticationFailed", Guid.Empty) @@ -43,7 +43,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security LogSeverity = LogLevel.Error, ShortOverview = string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("LabelIpAddressValue"), + _localizationManager.GetServerLocalizedString("LabelIpAddressValue"), eventArgs.RemoteEndPoint), }).ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs index 584d559e44..fa9ce21170 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs @@ -33,14 +33,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"), + _localizationManager.GetServerLocalizedString("AuthenticationSucceededWithUserName"), eventArgs.User.Name), "AuthenticationSucceeded", eventArgs.User.Id) { ShortOverview = string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("LabelIpAddressValue"), + _localizationManager.GetServerLocalizedString("LabelIpAddressValue"), eventArgs.SessionInfo?.RemoteEndPoint), }).ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs index 73323acb37..8f71966b83 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs @@ -61,7 +61,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"), + _localizationManager.GetServerLocalizedString("UserStartedPlayingItemWithValues"), user.Username, GetItemName(eventArgs.MediaInfo), eventArgs.DeviceName), diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs index b75567539c..a88904c727 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -69,15 +69,15 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("UserStoppedPlayingItemWithValues"), + _localizationManager.GetServerLocalizedString("UserStoppedPlayingItemWithValues"), user.Username, GetItemName(item), 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/Events/Consumers/Session/SessionEndedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs index b90708a2f2..74dfeebba6 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs @@ -38,7 +38,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("UserOfflineFromDevice"), + _localizationManager.GetServerLocalizedString("UserOfflineFromDevice"), eventArgs.Argument.UserName, eventArgs.Argument.DeviceName), "SessionEnded", @@ -46,7 +46,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session { ShortOverview = string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("LabelIpAddressValue"), + _localizationManager.GetServerLocalizedString("LabelIpAddressValue"), eventArgs.Argument.RemoteEndPoint), }).ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs index 139c2e2acb..4028522838 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs @@ -38,7 +38,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("UserOnlineFromDevice"), + _localizationManager.GetServerLocalizedString("UserOnlineFromDevice"), eventArgs.Argument.UserName, eventArgs.Argument.DeviceName), "SessionStarted", @@ -46,7 +46,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session { ShortOverview = string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("LabelIpAddressValue"), + _localizationManager.GetServerLocalizedString("LabelIpAddressValue"), eventArgs.Argument.RemoteEndPoint) }).ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs index da82a3b30f..1e3dc7c92e 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs @@ -47,7 +47,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System var time = result.EndTimeUtc - result.StartTimeUtc; var runningTime = string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("LabelRunningTimeValue"), + _localizationManager.GetServerLocalizedString("LabelRunningTimeValue"), ToUserFriendlyString(time)); if (result.Status == TaskCompletionStatus.Failed) @@ -65,7 +65,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System } await _activityManager.CreateAsync(new ActivityLog( - string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), + string.Format(CultureInfo.InvariantCulture, _localizationManager.GetServerLocalizedString("ScheduledTaskFailedWithName"), task.Name), NotificationType.TaskFailed.ToString(), Guid.Empty) { diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs index 632f30c7ad..9fb007aca7 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs @@ -35,14 +35,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("NameInstallFailed"), + _localizationManager.GetServerLocalizedString("NameInstallFailed"), eventArgs.InstallationInfo.Name), NotificationType.InstallationFailed.ToString(), Guid.Empty) { ShortOverview = string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("VersionNumber"), + _localizationManager.GetServerLocalizedString("VersionNumber"), eventArgs.InstallationInfo.Version), Overview = eventArgs.Exception.Message }).ConfigureAwait(false); diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs index 4b49b714cf..2aa738c153 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs @@ -35,14 +35,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("PluginInstalledWithName"), + _localizationManager.GetServerLocalizedString("PluginInstalledWithName"), eventArgs.Argument.Name), NotificationType.PluginInstalled.ToString(), Guid.Empty) { ShortOverview = string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("VersionNumber"), + _localizationManager.GetServerLocalizedString("VersionNumber"), eventArgs.Argument.Version) }).ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs index 2d24de7fc6..f7e651173d 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs @@ -35,7 +35,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("PluginUninstalledWithName"), + _localizationManager.GetServerLocalizedString("PluginUninstalledWithName"), eventArgs.Argument.Name), NotificationType.PluginUninstalled.ToString(), Guid.Empty)) diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs index e892d3dd9a..bca9662839 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs @@ -35,14 +35,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("PluginUpdatedWithName"), + _localizationManager.GetServerLocalizedString("PluginUpdatedWithName"), eventArgs.Argument.Name), NotificationType.PluginUpdateInstalled.ToString(), Guid.Empty) { ShortOverview = string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("VersionNumber"), + _localizationManager.GetServerLocalizedString("VersionNumber"), eventArgs.Argument.Version), Overview = eventArgs.Argument.Changelog }).ConfigureAwait(false); diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs index 4f063f6a1b..cf5c81b981 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs @@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("UserCreatedWithName"), + _localizationManager.GetServerLocalizedString("UserCreatedWithName"), eventArgs.Argument.Username), "UserCreated", eventArgs.Argument.Id)) diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs index ba4a072e84..720480c28f 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs @@ -34,7 +34,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("UserDeletedWithName"), + _localizationManager.GetServerLocalizedString("UserDeletedWithName"), eventArgs.Argument.Username), "UserDeleted", Guid.Empty)) diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs index bbc00567d1..efaf19397f 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs @@ -35,7 +35,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("UserLockedOutWithName"), + _localizationManager.GetServerLocalizedString("UserLockedOutWithName"), eventArgs.Argument.Username), NotificationType.UserLockedOut.ToString(), eventArgs.Argument.Id) diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs index 7219704ec6..cc9efa7061 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs @@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users await _activityManager.CreateAsync(new ActivityLog( string.Format( CultureInfo.InvariantCulture, - _localizationManager.GetLocalizedString("UserPasswordChangedWithName"), + _localizationManager.GetServerLocalizedString("UserPasswordChangedWithName"), eventArgs.Argument.Username), "UserPasswordChanged", eventArgs.Argument.Id)) 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.ByName.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs index 380c6e582c..c5b5fbf6d8 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; -using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -170,92 +169,50 @@ public sealed partial class BaseItemRepository ExcludeItemIds = filter.ExcludeItemIds }; - // Build the master query and collapse rows that share a PresentationUniqueKey - // (e.g. alternate versions) by picking the lowest Id per group. + // Collapse rows that share a PresentationUniqueKey (e.g. alternate versions) by picking + // the lowest Id per group. For MusicArtist, prefer the entity from a library the user + // can actually access,since the same artist can have a folder in multiple libraries. + // Keep as an IQueryable sub-select so paging is applied AFTER + // ApplyOrder runs the caller's actual sort. var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter); - - var orderedMasterQuery = ApplyOrder(masterQuery, filter, context) - .GroupBy(e => e.PresentationUniqueKey) - .Select(g => g.Min(e => e.Id)); + var isMusicArtist = returnType == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]; + var representativeIds = isMusicArtist + ? masterQuery + .GroupBy(e => e.PresentationUniqueKey) + .Select(g => g + .OrderBy(e => filter.TopParentIds.Contains(e.TopParentId ?? Guid.Empty) ? 0 : 1) + .ThenBy(e => e.Id) + .First().Id) + : masterQuery + .GroupBy(e => e.PresentationUniqueKey) + .Select(g => g.Min(e => e.Id)); var result = new QueryResult<(BaseItemDto, ItemCounts?)>(); if (filter.EnableTotalRecordCount) { - result.TotalRecordCount = orderedMasterQuery.Count(); + result.TotalRecordCount = representativeIds.Count(); } + var query = ApplyNavigations( + context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => representativeIds.Contains(e.Id)), + filter); + + query = ApplyOrder(query, filter, context); + if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0) { - orderedMasterQuery = orderedMasterQuery.Skip(filter.StartIndex.Value); + query = query.Skip(filter.StartIndex.Value); } if (filter.Limit.HasValue) { - orderedMasterQuery = orderedMasterQuery.Take(filter.Limit.Value); + query = query.Take(filter.Limit.Value); } - var masterIds = orderedMasterQuery.ToList(); - - var query = ApplyNavigations( - context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => masterIds.Contains(e.Id)), - filter); - - query = ApplyOrder(query, filter, context); - + result.StartIndex = filter.StartIndex ?? 0; if (filter.IncludeItemTypes.Length > 0) { - var typeSubQuery = new InternalItemsQuery(filter.User) - { - ExcludeItemTypes = filter.ExcludeItemTypes, - IncludeItemTypes = filter.IncludeItemTypes, - MediaTypes = filter.MediaTypes, - AncestorIds = filter.AncestorIds, - ExcludeItemIds = filter.ExcludeItemIds, - ItemIds = filter.ItemIds, - TopParentIds = filter.TopParentIds, - ParentId = filter.ParentId, - IsPlayed = filter.IsPlayed - }; - - var itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, typeSubQuery) - .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type))); - - var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; - var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; - var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]; - var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]; - var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]; - var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; - var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer]; - var itemIds = itemCountQuery.Select(e => e.Id); - - // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite) - // Instead, start from ItemValueMaps and join with BaseItems - var countsByCleanName = context.ItemValuesMap - .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type)) - .Where(ivm => itemIds.Contains(ivm.ItemId)) - .Join( - context.BaseItems, - ivm => ivm.ItemId, - e => e.Id, - (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type }) - .GroupBy(x => new { x.CleanName, x.Type }) - .Select(g => new { g.Key.CleanName, g.Key.Type, Count = g.Count() }) - .GroupBy(x => x.CleanName) - .ToDictionary( - g => g.Key, - g => new ItemCounts - { - SeriesCount = g.Where(x => x.Type == seriesTypeName).Sum(x => x.Count), - EpisodeCount = g.Where(x => x.Type == episodeTypeName).Sum(x => x.Count), - MovieCount = g.Where(x => x.Type == movieTypeName).Sum(x => x.Count), - AlbumCount = g.Where(x => x.Type == musicAlbumTypeName).Sum(x => x.Count), - ArtistCount = g.Where(x => x.Type == musicArtistTypeName).Sum(x => x.Count), - SongCount = g.Where(x => x.Type == audioTypeName).Sum(x => x.Count), - TrailerCount = g.Where(x => x.Type == trailerTypeName).Sum(x => x.Count), - }); - - result.StartIndex = filter.StartIndex ?? 0; + var countsByCleanName = BuildItemCountsByCleanName(context, filter, itemValueTypes); result.Items = [ .. query @@ -273,7 +230,6 @@ public sealed partial class BaseItemRepository } else { - result.StartIndex = filter.StartIndex ?? 0; result.Items = [ .. query @@ -287,4 +243,61 @@ public sealed partial class BaseItemRepository return result; } + + private Dictionary<string, ItemCounts> BuildItemCountsByCleanName( + Database.Implementations.JellyfinDbContext context, + InternalItemsQuery filter, + IReadOnlyList<ItemValueType> itemValueTypes) + { + var typeSubQuery = new InternalItemsQuery(filter.User) + { + ExcludeItemTypes = filter.ExcludeItemTypes, + IncludeItemTypes = filter.IncludeItemTypes, + MediaTypes = filter.MediaTypes, + AncestorIds = filter.AncestorIds, + ExcludeItemIds = filter.ExcludeItemIds, + ItemIds = filter.ItemIds, + TopParentIds = filter.TopParentIds, + ParentId = filter.ParentId, + IsPlayed = filter.IsPlayed + }; + + var itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, typeSubQuery) + .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type))); + + var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; + var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; + var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]; + var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]; + var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]; + var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; + var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer]; + var itemIds = itemCountQuery.Select(e => e.Id); + + // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite) + // Instead, start from ItemValueMaps and join with BaseItems + return context.ItemValuesMap + .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type)) + .Where(ivm => itemIds.Contains(ivm.ItemId)) + .Join( + context.BaseItems, + ivm => ivm.ItemId, + e => e.Id, + (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type }) + .GroupBy(x => new { x.CleanName, x.Type }) + .Select(g => new { g.Key.CleanName, g.Key.Type, Count = g.Count() }) + .GroupBy(x => x.CleanName) + .ToDictionary( + g => g.Key, + g => new ItemCounts + { + SeriesCount = g.Where(x => x.Type == seriesTypeName).Sum(x => x.Count), + EpisodeCount = g.Where(x => x.Type == episodeTypeName).Sum(x => x.Count), + MovieCount = g.Where(x => x.Type == movieTypeName).Sum(x => x.Count), + AlbumCount = g.Where(x => x.Type == musicAlbumTypeName).Sum(x => x.Count), + ArtistCount = g.Where(x => x.Type == musicArtistTypeName).Sum(x => x.Count), + SongCount = g.Where(x => x.Type == audioTypeName).Sum(x => x.Count), + TrailerCount = g.Where(x => x.Type == trailerTypeName).Sum(x => x.Count), + }); + } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs index 0abe981af8..f33a65a703 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs @@ -390,7 +390,8 @@ public sealed partial class BaseItemRepository { if (filter.UseRawName == true) { - baseQuery = baseQuery.Where(e => e.Name == filter.Name); + var nameLower = filter.Name.ToLowerInvariant(); + baseQuery = baseQuery.Where(e => e.Name!.ToLower() == nameLower); } else { @@ -823,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; @@ -952,6 +973,17 @@ public sealed partial class BaseItemRepository } } + if (filter.HasAnyProviderIds is not null && filter.HasAnyProviderIds.Count > 0) + { + var includeAny = filter.HasAnyProviderIds + .SelectMany(kvp => kvp.Value.Select(v => $"{kvp.Key}:{v}")) + .ToArray(); + if (includeAny.Length > 0) + { + baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => includeAny.Contains(f))); + } + } + if (filter.HasImdbId.HasValue) { baseQuery = filter.HasImdbId.Value @@ -995,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 @@ -1056,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/LinkedChildrenService.cs b/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs index 415510b2f4..5e5ce320a5 100644 --- a/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs +++ b/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs @@ -1,4 +1,6 @@ #pragma warning disable RS0030 // Do not use banned APIs +#pragma warning disable CA1304 // Specify CultureInfo +#pragma warning disable CA1311 // Specify a culture or use an invariant version using System; using System.Collections.Generic; @@ -62,17 +64,19 @@ public class LinkedChildrenService : ILinkedChildrenService { using var dbContext = _dbProvider.CreateDbContext(); + var lowerNames = artistNames.Select(n => n.ToLowerInvariant()).ToArray(); var artists = dbContext.BaseItems .AsNoTracking() .Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!) - .Where(e => artistNames.Contains(e.Name)) + .Where(e => lowerNames.Contains(e.Name!.ToLower())) .ToArray(); var lookup = artists - .GroupBy(e => e.Name!) + .GroupBy(e => e.Name!, StringComparer.OrdinalIgnoreCase) .ToDictionary( g => g.Key, - g => g.Select(f => _queryHelpers.DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray()); + g => g.Select(f => _queryHelpers.DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray(), + StringComparer.OrdinalIgnoreCase); var result = new Dictionary<string, MusicArtist[]>(artistNames.Count); foreach (var name in artistNames) @@ -87,14 +91,25 @@ public class LinkedChildrenService : ILinkedChildrenService } /// <inheritdoc/> - public IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId) + public IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId, BaseItemKind? parentType = null) { using var context = _dbProvider.CreateDbContext(); - return context.LinkedChildren - .Where(lc => lc.ChildId == childId && lc.ChildType == DbLinkedChildType.Manual) - .Select(lc => lc.ParentId) - .Distinct() - .ToList(); + + var query = context.LinkedChildren + .Where(lc => lc.ChildId == childId && lc.ChildType == DbLinkedChildType.Manual); + + if (parentType.HasValue) + { + var parentTypeName = _itemTypeLookup.BaseItemKindNames[parentType.Value]; + query = query.Join( + context.BaseItems + .Where(item => item.Type == parentTypeName), + lc => lc.ParentId, + item => item.Id, + (lc, _) => lc); + } + + return query.Select(lc => lc.ParentId).Distinct().ToList(); } /// <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/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs index ada86c8b87..d327b218a9 100644 --- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -48,9 +48,9 @@ public static class OrderMapper (ItemSortBy.SeriesSortName, _) => e => e.SeriesName, (ItemSortBy.Album, _) => e => e.Album, (ItemSortBy.DateCreated, _) => e => e.DateCreated, - (ItemSortBy.PremiereDate, _) => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)), + (ItemSortBy.PremiereDate, _) => e => e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null), (ItemSortBy.StartDate, _) => e => e.StartDate, - (ItemSortBy.Name, _) => e => e.CleanName, + (ItemSortBy.Name, _) => e => e.SortName, (ItemSortBy.CommunityRating, _) => e => e.CommunityRating, (ItemSortBy.ProductionYear, _) => e => e.ProductionYear, (ItemSortBy.CriticRating, _) => e => e.CriticRating, diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 6cc9729bbe..eb87b525fe 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -46,9 +46,10 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I { // The Peoples table has one row per (Name, PersonType), so the same person can // appear multiple times (e.g. as Actor and GuestStar). Collapse to one row per - // name so /Persons doesn't return the same BaseItem id repeatedly. + // name so /Persons doesn't return the same BaseItem id repeatedly. Lowercase the + // grouping key so case-only duplicates collapse together. var representativeIds = dbQuery - .GroupBy(e => e.Name) + .GroupBy(e => e.Name.ToLower()) .Select(g => g.Min(e => e.Id)); dbQuery = context.Peoples.AsNoTracking() .Where(p => representativeIds.Contains(p.Id)) @@ -102,24 +103,23 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I person.Role = person.Role?.Trim() ?? string.Empty; } - // multiple metadata providers can provide the _same_ person - people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray(); - var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray(); + // multiple metadata providers can provide the _same_ person; dedupe case-insensitively. + people = people.DistinctBy(e => e.Name.ToLowerInvariant() + "-" + e.Type).ToArray(); + var personKeys = people.Select(e => e.Name.ToLowerInvariant() + "-" + e.Type).ToArray(); using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); var existingPersons = context.Peoples.Select(e => new - { - item = e, - SelectionKey = e.Name + "-" + e.PersonType - }) + { + item = e, + SelectionKey = e.Name.ToLower() + "-" + e.PersonType + }) .Where(p => personKeys.Contains(p.SelectionKey)) .Select(f => f.item) .ToArray(); var toAdd = people - .Where(e => e.Type is not PersonKind.Artist && e.Type is not PersonKind.AlbumArtist) - .Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString())) + .Where(e => !existingPersons.Any(f => string.Equals(f.Name, e.Name, StringComparison.OrdinalIgnoreCase) && f.PersonType == e.Type.ToString())) .Select(Map); context.Peoples.AddRange(toAdd); context.SaveChanges(); @@ -132,13 +132,8 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I foreach (var person in people) { - if (person.Type == PersonKind.Artist || person.Type == PersonKind.AlbumArtist) - { - continue; - } - - var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString()); - var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == person.Type.ToString() && e.Role == person.Role); + var entityPerson = personsEntities.First(e => string.Equals(e.Name, person.Name, StringComparison.OrdinalIgnoreCase) && e.PersonType == person.Type.ToString()); + var existingMap = existingMaps.FirstOrDefault(e => string.Equals(e.People.Name, person.Name, StringComparison.OrdinalIgnoreCase) && e.People.PersonType == person.Type.ToString() && e.Role == person.Role); if (existingMap is null) { context.PeopleBaseItemMap.Add(new PeopleBaseItemMap() @@ -170,6 +165,42 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I transaction.Commit(); } + /// <inheritdoc/> + public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes) + { + using var context = _dbProvider.CreateDbContext(); + var query = context.PeopleBaseItemMap + .AsNoTracking() + .Where(m => itemIds.Contains(m.ItemId)); + + if (personTypes.Count > 0) + { + query = query.Where(m => personTypes.Contains(m.People.PersonType)); + } + + var rows = query + .OrderBy(m => m.ListOrder) + .Select(m => new { m.ItemId, m.People.Name }) + .ToList(); + + var result = new Dictionary<Guid, IReadOnlyList<string>>(); + foreach (var group in rows.GroupBy(r => r.ItemId)) + { + var names = group + .Select(r => r.Name) + .Where(name => !string.IsNullOrEmpty(name)) + .Distinct() + .ToArray(); + + if (names.Length > 0) + { + result[group.Key] = names; + } + } + + return result; + } + private PersonInfo Map(People people) { var mapping = people.BaseItems?.FirstOrDefault(); @@ -244,7 +275,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I if (filter.MaxListOrder.HasValue && !filter.ItemId.IsEmpty()) { - query = query.Where(e => e.BaseItems!.Where(w => w.ItemId == filter.ItemId).OrderBy(w => w.ListOrder).First().ListOrder <= filter.MaxListOrder.Value); + query = query.Where(e => e.BaseItems!.Any(w => w.ItemId == filter.ItemId && w.ListOrder <= filter.MaxListOrder.Value)); } if (!string.IsNullOrWhiteSpace(filter.NameContains)) diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index c514735688..be98f93dab 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -54,7 +54,7 @@ public class MediaSegmentManager : IMediaSegmentManager public async Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool forceOverwrite, CancellationToken cancellationToken) { var providers = _segmentProviders - .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name))) + .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(e.Name, StringComparer.OrdinalIgnoreCase)) .OrderBy(i => { var index = libraryOptions.MediaSegmentProviderOrder.IndexOf(i.Name); @@ -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); @@ -224,7 +235,7 @@ public class MediaSegmentManager : IMediaSegmentManager if (filterByProvider) { var providerIds = _segmentProviders - .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name))) + .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(e.Name, StringComparer.OrdinalIgnoreCase)) .Select(f => GetProviderId(f.Name)) .ToArray(); if (providerIds.Length == 0) 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); |
