From 20f7ddbf8fe9e06e366ad80484dd386542245c61 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 5 Sep 2025 22:39:15 +0200 Subject: Refactor Display preference manager (#14056) --- MediaBrowser.Controller/IDisplayPreferencesManager.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'MediaBrowser.Controller') diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs index a97096eaee..7e235ed26c 100644 --- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs +++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs @@ -60,8 +60,15 @@ namespace MediaBrowser.Controller void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary customPreferences); /// - /// Saves changes made to the database. + /// Updates or Creates the display preferences. /// - void SaveChanges(); + /// The entity to update or create. + void UpdateDisplayPreferences(DisplayPreferences displayPreferences); + + /// + /// Updates or Creates the display preferences for the given item. + /// + /// The entity to update or create. + void UpdateItemDisplayPreferences(ItemDisplayPreferences itemDisplayPreferences); } } -- cgit v1.2.3 From c02a24e32a7c4ffc4f00620d53911d044b26c9cc Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 12 Sep 2025 21:58:16 +0200 Subject: Fix several Stackoverflows (#14783) --- MediaBrowser.Controller/Entities/BaseItem.cs | 34 +++++++++++++++++----------- MediaBrowser.Controller/Entities/Folder.cs | 2 +- MediaBrowser.Controller/Entities/Video.cs | 32 ++++++++++++++++++-------- 3 files changed, 44 insertions(+), 24 deletions(-) (limited to 'MediaBrowser.Controller') diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index bb0b26b8e2..275fdac2eb 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -701,19 +701,7 @@ namespace MediaBrowser.Controller.Entities { get { - var customRating = CustomRating; - if (!string.IsNullOrEmpty(customRating)) - { - return customRating; - } - - var parent = DisplayParent; - if (parent is not null) - { - return parent.CustomRatingForComparison; - } - - return null; + return GetCustomRatingForComparision(); } } @@ -791,6 +779,26 @@ namespace MediaBrowser.Controller.Entities /// The remote trailers. public IReadOnlyList RemoteTrailers { get; set; } + private string GetCustomRatingForComparision(HashSet callstack = null) + { + callstack ??= new(); + var customRating = CustomRating; + if (!string.IsNullOrEmpty(customRating)) + { + return customRating; + } + + callstack.Add(Id); + + var parent = DisplayParent; + if (parent is not null && !callstack.Contains(parent.Id)) + { + return parent.GetCustomRatingForComparision(callstack); + } + + return null; + } + public virtual double GetDefaultPrimaryImageAspectRatio() { return 0; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 06cbcc2e18..082cf39fac 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -568,7 +568,7 @@ namespace MediaBrowser.Controller.Entities if (recursive && child is Folder folder) { - await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false); + await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false); } } } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 04f47b729d..1043029c6e 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -152,16 +152,7 @@ namespace MediaBrowser.Controller.Entities { get { - if (!string.IsNullOrEmpty(PrimaryVersionId)) - { - var item = LibraryManager.GetItemById(PrimaryVersionId); - if (item is Video video) - { - return video.MediaSourceCount; - } - } - - return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1; + return GetMediaSourceCount(); } } @@ -259,6 +250,27 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public override MediaType MediaType => MediaType.Video; + private int GetMediaSourceCount(HashSet callstack = null) + { + callstack ??= new(); + if (!string.IsNullOrEmpty(PrimaryVersionId)) + { + var item = LibraryManager.GetItemById(PrimaryVersionId); + if (item is Video video) + { + if (callstack.Contains(video.Id)) + { + return video.LinkedAlternateVersions.Length + video.LocalAlternateVersions.Length + 1; + } + + callstack.Add(video.Id); + return video.GetMediaSourceCount(callstack); + } + } + + return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1; + } + public override List GetUserDataKeys() { var list = base.GetUserDataKeys(); -- cgit v1.2.3 From 6796b3435d893fde8c1ae7551f52b1fbb1bc489c Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 12 Sep 2025 21:58:28 +0200 Subject: Avoid constant arrays as arguments (#14784) --- .editorconfig | 3 +++ MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs | 8 ++++---- MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs | 15 ++++++++------- .../Probing/ProbeResultNormalizer.cs | 14 ++++++++------ src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs | 7 +++---- 5 files changed, 26 insertions(+), 21 deletions(-) (limited to 'MediaBrowser.Controller') diff --git a/.editorconfig b/.editorconfig index ab5d3d9dd1..313b02563d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -294,6 +294,9 @@ dotnet_diagnostic.CA1854.severity = error # error on CA1860: Avoid using 'Enumerable.Any()' extension method dotnet_diagnostic.CA1860.severity = error +# error on CA1861: Avoid constant arrays as arguments +dotnet_diagnostic.CA1861.severity = error + # error on CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons dotnet_diagnostic.CA1862.severity = error diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs index 2742f21e36..b53210b0b1 100644 --- a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs @@ -167,12 +167,12 @@ public static class XmlReaderExtensions // Only split by comma if there is no pipe in the string // We have to be careful to not split names like Matthew, Jr. - var separator = !value.Contains('|', StringComparison.Ordinal) + ReadOnlySpan separator = !value.Contains('|', StringComparison.Ordinal) && !value.Contains(';', StringComparison.Ordinal) - ? new[] { ',' } - : new[] { '|', ';' }; + ? stackalloc[] { ',' } + : stackalloc[] { '|', ';' }; - foreach (var part in value.Trim().Trim(separator).Split(separator)) + foreach (var part in value.AsSpan().Trim().Trim(separator).ToString().Split(separator)) { if (!string.IsNullOrWhiteSpace(part)) { diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 8d6211051b..ef912f42c3 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Globalization; using System.Linq; using Jellyfin.Data.Enums; @@ -22,6 +21,8 @@ namespace MediaBrowser.Controller.MediaEncoding // For now, a common base class until the API and MediaEncoding classes are unified public class EncodingJobInfo { + private static readonly char[] _separators = ['|', ',']; + public int? OutputAudioBitrate; public int? OutputAudioChannels; @@ -586,7 +587,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (!string.IsNullOrEmpty(BaseRequest.Profile)) { - return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return BaseRequest.Profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } if (!string.IsNullOrEmpty(codec)) @@ -595,7 +596,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(profile)) { - return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } } @@ -606,7 +607,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType)) { - return BaseRequest.VideoRangeType.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return BaseRequest.VideoRangeType.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } if (!string.IsNullOrEmpty(codec)) @@ -615,7 +616,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(rangetype)) { - return rangetype.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return rangetype.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } } @@ -626,7 +627,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (!string.IsNullOrEmpty(BaseRequest.CodecTag)) { - return BaseRequest.CodecTag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return BaseRequest.CodecTag.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } if (!string.IsNullOrEmpty(codec)) @@ -635,7 +636,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codectag)) { - return codectag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries); + return codectag.Split(_separators, StringSplitOptions.RemoveEmptyEntries); } } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 18646ec5dc..00a9ae797d 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -30,9 +30,11 @@ namespace MediaBrowser.MediaEncoding.Probing private const string ArtistReplaceValue = " | "; - private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' }; - private readonly string[] _webmVideoCodecs = { "av1", "vp8", "vp9" }; - private readonly string[] _webmAudioCodecs = { "opus", "vorbis" }; + private static readonly char[] _basicDelimiters = ['/', ';']; + private static readonly char[] _nameDelimiters = [.. _basicDelimiters, '|', '\\']; + private static readonly char[] _genreDelimiters = [.. _basicDelimiters, ',']; + private static readonly string[] _webmVideoCodecs = ["av1", "vp8", "vp9"]; + private static readonly string[] _webmAudioCodecs = ["opus", "vorbis"]; private readonly ILogger _logger; private readonly ILocalizationManager _localization; @@ -174,7 +176,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (tags.TryGetValue("artists", out var artists) && !string.IsNullOrWhiteSpace(artists)) { - info.Artists = SplitDistinctArtists(artists, new[] { '/', ';' }, false).ToArray(); + info.Artists = SplitDistinctArtists(artists, _basicDelimiters, false).ToArray(); } else { @@ -1552,7 +1554,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (tags.TryGetValue("WM/Genre", out var genres) && !string.IsNullOrWhiteSpace(genres)) { - var genreList = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var genreList = genres.Split(_genreDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); // If this is empty then don't overwrite genres that might have been fetched earlier if (genreList.Length > 0) @@ -1569,7 +1571,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (tags.TryGetValue("WM/MediaCredits", out var people) && !string.IsNullOrEmpty(people)) { video.People = Array.ConvertAll( - people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), + people.Split(_basicDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), i => new BaseItemPerson { Name = i, Type = PersonKind.Actor }); } diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs index e3afe15131..2270758454 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs @@ -200,8 +200,7 @@ namespace Jellyfin.LiveTv.TunerHosts var numberIndex = nameInExtInf.IndexOf(' '); if (numberIndex > 0) { - var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' }); - + var numberPart = nameInExtInf[..numberIndex].Trim(stackalloc[] { ' ', '.' }); if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) { numberString = numberPart.ToString(); @@ -273,12 +272,12 @@ namespace Jellyfin.LiveTv.TunerHosts var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal); if (numberIndex > 0) { - var numberPart = nameInExtInf.AsSpan(0, numberIndex).Trim(new[] { ' ', '.' }); + var numberPart = nameInExtInf.AsSpan(0, numberIndex).Trim(stackalloc[] { ' ', '.' }); if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) { // channel.Number = number.ToString(); - nameInExtInf = nameInExtInf.AsSpan(numberIndex + 1).Trim(new[] { ' ', '-' }).ToString(); + nameInExtInf = nameInExtInf.AsSpan(numberIndex + 1).Trim(stackalloc[] { ' ', '-' }).ToString(); } } } -- cgit v1.2.3 From 7c6cedd90ac26d6b18fd518f25421b2b71c74993 Mon Sep 17 00:00:00 2001 From: KGT1 Date: Fri, 12 Sep 2025 22:15:00 +0200 Subject: Allow non-admin users to subscribe to their own Sessions (#13767) --- .../SessionInfoWebSocketListener.cs | 23 ++++++++++++++---- .../Net/BasePeriodicWebSocketListener.cs | 27 +++++++++++++++------- 2 files changed, 38 insertions(+), 12 deletions(-) (limited to 'MediaBrowser.Controller') diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index 9d149cc85a..143d82bac6 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Jellyfin.Data; using Jellyfin.Database.Implementations.Enums; @@ -56,6 +57,21 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener + protected override Task> GetDataToSendForConnection(IWebSocketConnection connection) + { + // For non-admin users, filter the sessions to only include their own sessions + if (connection.AuthorizationInfo?.User is not null && + !connection.AuthorizationInfo.IsApiKey && + !connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) + { + var userId = connection.AuthorizationInfo.User.Id; + return Task.FromResult(_sessionManager.Sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId))); + } + + return Task.FromResult(_sessionManager.Sessions); + } + /// protected override async ValueTask DisposeAsyncCore() { @@ -80,11 +96,10 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListenerThe message. protected override void Start(WebSocketMessageInfo message) { - if (!message.Connection.AuthorizationInfo.IsApiKey - && (message.Connection.AuthorizationInfo.User is null - || !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))) + // Allow all authenticated users to subscribe to session information + if (message.Connection.AuthorizationInfo.User is null && !message.Connection.AuthorizationInfo.IsApiKey) { - throw new AuthenticationException("Only admin users can subscribe to session information."); + throw new AuthenticationException("User must be authenticated to subscribe to session Information."); } base.Start(message); diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index 4757bfa303..1e0d77fe51 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -80,6 +80,16 @@ namespace MediaBrowser.Controller.Net /// Task{`1}. protected abstract Task GetDataToSend(); + /// + /// Gets the data to send for a specific connection. + /// + /// The connection. + /// Task{`1}. + protected virtual Task GetDataToSendForConnection(IWebSocketConnection connection) + { + return GetDataToSend(); + } + /// /// Processes the message. /// @@ -174,17 +184,11 @@ namespace MediaBrowser.Controller.Net continue; } - var data = await GetDataToSend().ConfigureAwait(false); - if (data is null) - { - continue; - } - IEnumerable GetTasks() { foreach (var tuple in tuples) { - yield return SendDataInternal(data, tuple); + yield return SendDataForConnectionAsync(tuple); } } @@ -198,12 +202,19 @@ namespace MediaBrowser.Controller.Net } } - private async Task SendDataInternal(TReturnDataType data, (IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) tuple) + private async Task SendDataForConnectionAsync((IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State) tuple) { try { var (connection, cts, state) = tuple; var cancellationToken = cts.Token; + + var data = await GetDataToSendForConnection(connection).ConfigureAwait(false); + if (data is null) + { + return; + } + await connection.SendAsync( new OutboundWebSocketMessage { MessageType = Type, Data = data }, cancellationToken).ConfigureAwait(false); -- cgit v1.2.3 From 4d36bd635d3dd0ff5652c1807dce7a1a1dff8873 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sun, 14 Sep 2025 11:18:21 -0600 Subject: Revert IsPlayed optimization, pass UserItemData to IsPlayed when available (#14786) --- .../Sorting/IsFavoriteOrLikeComparer.cs | 3 +-- .../Sorting/IsPlayedComparer.cs | 3 +-- .../Sorting/IsUnplayedComparer.cs | 3 +-- MediaBrowser.Controller/Entities/BaseItem.cs | 18 +++++++++--------- MediaBrowser.Controller/Entities/Folder.cs | 8 ++++---- MediaBrowser.Controller/Entities/UserViewBuilder.cs | 2 +- 6 files changed, 17 insertions(+), 20 deletions(-) (limited to 'MediaBrowser.Controller') diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs index 01c1e596f9..86d08ed27b 100644 --- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs @@ -6,7 +6,6 @@ using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { @@ -54,7 +53,7 @@ namespace Emby.Server.Implementations.Sorting /// DateTime. private int GetValue(BaseItem x) { - return x.IsFavoriteOrLiked(User) ? 0 : 1; + return x.IsFavoriteOrLiked(User, userItemData: null) ? 0 : 1; } } } diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs index 6f206c8772..9faa02f1fd 100644 --- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs @@ -7,7 +7,6 @@ using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { @@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.Sorting /// DateTime. private int GetValue(BaseItem x) { - return x.IsPlayed(User) ? 0 : 1; + return x.IsPlayed(User, userItemData: null) ? 0 : 1; } } } diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs index fd1326327b..6f177c4637 100644 --- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs @@ -7,7 +7,6 @@ using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { @@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.Sorting /// DateTime. private int GetValue(BaseItem x) { - return x.IsUnplayed(User) ? 0 : 1; + return x.IsUnplayed(User, userItemData: null) ? 0 : 1; } } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 275fdac2eb..67675e7560 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2315,27 +2315,27 @@ namespace MediaBrowser.Controller.Entities return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None); } - public virtual bool IsPlayed(User user) + public virtual bool IsPlayed(User user, UserItemData userItemData) { - var userdata = UserDataManager.GetUserData(user, this); + userItemData ??= UserDataManager.GetUserData(user, this); - return userdata is not null && userdata.Played; + return userItemData is not null && userItemData.Played; } - public bool IsFavoriteOrLiked(User user) + public bool IsFavoriteOrLiked(User user, UserItemData userItemData) { - var userdata = UserDataManager.GetUserData(user, this); + userItemData ??= UserDataManager.GetUserData(user, this); - return userdata is not null && (userdata.IsFavorite || (userdata.Likes ?? false)); + return userItemData is not null && (userItemData.IsFavorite || (userItemData.Likes ?? false)); } - public virtual bool IsUnplayed(User user) + public virtual bool IsUnplayed(User user, UserItemData userItemData) { ArgumentNullException.ThrowIfNull(user); - var userdata = UserDataManager.GetUserData(user, this); + userItemData ??= UserDataManager.GetUserData(user, this); - return userdata is null || !userdata.Played; + return userItemData is null || !userItemData.Played; } ItemLookupInfo IHasLookupInfo.GetLookupInfo() diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 082cf39fac..b889e73e3a 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1666,7 +1666,7 @@ namespace MediaBrowser.Controller.Entities } } - public override bool IsPlayed(User user) + public override bool IsPlayed(User user, UserItemData userItemData) { var itemsResult = GetItemList(new InternalItemsQuery(user) { @@ -1677,12 +1677,12 @@ namespace MediaBrowser.Controller.Entities }); return itemsResult - .All(i => i.IsPlayed(user)); + .All(i => i.IsPlayed(user, userItemData: null)); } - public override bool IsUnplayed(User user) + public override bool IsUnplayed(User user, UserItemData userItemData) { - return !IsPlayed(user); + return !IsPlayed(user, userItemData); } public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields) diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 0cd3399d4a..62eb43aa52 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -542,7 +542,7 @@ namespace MediaBrowser.Controller.Entities if (query.IsPlayed.HasValue) { userData ??= userDataManager.GetUserData(user, item); - if (userData.Played != query.IsPlayed.Value) + if (item.IsPlayed(user, userData) != query.IsPlayed.Value) { return false; } -- cgit v1.2.3 From a0b3e2b071509f440db10768f6f8984c7ea382d6 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Tue, 16 Sep 2025 21:08:04 +0200 Subject: Optimize internal querying of UserData, other fixes (#14795) --- .../Library/LibraryManager.cs | 10 +- .../Library/MusicManager.cs | 13 +- .../Library/UserDataManager.cs | 8 +- Jellyfin.Api/Controllers/YearsController.cs | 5 +- .../Item/BaseItemRepository.cs | 104 +- .../Item/PeopleRepository.cs | 6 +- MediaBrowser.Controller/Entities/BaseItem.cs | 7 + MediaBrowser.Controller/Entities/Folder.cs | 122 +- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 4 +- MediaBrowser.Controller/Entities/TV/Season.cs | 2 +- MediaBrowser.Controller/Entities/TV/Series.cs | 1 + MediaBrowser.Controller/Entities/UserView.cs | 10 +- .../Persistence/IItemRepository.cs | 10 + MediaBrowser.Controller/Playlists/Playlist.cs | 6 +- .../Entities/BaseItemEntity.cs | 4 + .../ModelConfiguration/BaseItemConfiguration.cs | 1 + ...entChildRelationBaseItemWithCascade.Designer.cs | 1721 ++++++++++++++++++++ ...ProperParentChildRelationBaseItemWithCascade.cs | 30 + .../Migrations/JellyfinDbModelSnapshot.cs | 14 +- .../Controllers/LibraryStructureControllerTests.cs | 2 + 20 files changed, 1988 insertions(+), 92 deletions(-) create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.Designer.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs (limited to 'MediaBrowser.Controller') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 58a971f62a..0074df80a0 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1090,6 +1090,7 @@ namespace Emby.Server.Implementations.Library public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false) { + RootFolder.Children = null; await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); // Start by just validating the children of the root, but go no further @@ -1100,9 +1101,12 @@ namespace Emby.Server.Implementations.Library allowRemoveRoot: removeRoot, cancellationToken: cancellationToken).ConfigureAwait(false); - await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); + var rootFolder = GetUserRootFolder(); + rootFolder.Children = null; - await GetUserRootFolder().ValidateChildren( + await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); + + await rootFolder.ValidateChildren( new Progress(), new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false, @@ -1110,7 +1114,7 @@ namespace Emby.Server.Implementations.Library cancellationToken: cancellationToken).ConfigureAwait(false); // Quickly scan CollectionFolders for changes - foreach (var child in GetUserRootFolder().Children.OfType()) + foreach (var child in rootFolder.Children!.OfType()) { // If the user has somehow deleted the collection directory, remove the metadata from the database. if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path)) diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 28cf695007..e0c8ae371b 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -45,11 +45,14 @@ namespace Emby.Server.Implementations.Library public IReadOnlyList GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions) { var genres = item - .GetRecursiveChildren(user, new InternalItemsQuery(user) - { - IncludeItemTypes = [BaseItemKind.Audio], - DtoOptions = dtoOptions - }) + .GetRecursiveChildren( + user, + new InternalItemsQuery(user) + { + IncludeItemTypes = [BaseItemKind.Audio], + DtoOptions = dtoOptions + }, + out _) .Cast