diff options
Diffstat (limited to 'Emby.Server.Implementations')
8 files changed, 200 insertions, 37 deletions
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index cc57d183b6..94e2468719 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -203,6 +203,39 @@ namespace Emby.Server.Implementations.Dto } } + // Batch-fetch MusicArtist lookups across all items to avoid N+1 queries. + IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null; + var artistNames = new HashSet<string>(StringComparer.Ordinal); + foreach (var item in accessibleItems) + { + if (item is IHasArtist hasArtist) + { + foreach (var name in hasArtist.Artists) + { + if (!string.IsNullOrWhiteSpace(name)) + { + artistNames.Add(name); + } + } + } + + if (item is IHasAlbumArtist hasAlbumArtist) + { + foreach (var name in hasAlbumArtist.AlbumArtists) + { + if (!string.IsNullOrWhiteSpace(name)) + { + artistNames.Add(name); + } + } + } + } + + if (artistNames.Count > 0) + { + artistsBatch = _libraryManager.GetArtists(artistNames.ToArray()); + } + for (int index = 0; index < accessibleItems.Count; index++) { var item = accessibleItems[index]; @@ -214,7 +247,8 @@ namespace Emby.Server.Implementations.Dto userDataBatch?.GetValueOrDefault(item.Id), allCollectionFolders, childCountBatch, - playedCountBatch); + playedCountBatch, + artistsBatch); if (item is LiveTvChannel tvChannel) { @@ -274,7 +308,8 @@ namespace Emby.Server.Implementations.Dto UserItemData? userData = null, List<Folder>? allCollectionFolders = null, Dictionary<Guid, int>? childCountBatch = null, - Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null) + Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null, + IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null) { var dto = new BaseItemDto { @@ -334,7 +369,7 @@ namespace Emby.Server.Implementations.Dto AttachStudios(dto, item); } - AttachBasicFields(dto, item, owner, options); + AttachBasicFields(dto, item, owner, options, artistsBatch); if (options.ContainsField(ItemFields.CanDelete)) { @@ -907,7 +942,8 @@ namespace Emby.Server.Implementations.Dto /// <param name="item">The item.</param> /// <param name="owner">The owner.</param> /// <param name="options">The options.</param> - private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options) + /// <param name="artistsBatch">Optional pre-fetched artist lookup shared across a batch of items.</param> + private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null) { if (options.ContainsField(ItemFields.DateCreated)) { @@ -1152,7 +1188,8 @@ namespace Emby.Server.Implementations.Dto // Include artists that are not in the database yet, e.g., just added via metadata editor // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); - var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]); + var artistsLookup = artistsBatch + ?? _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]); dto.ArtistItems = hasArtist.Artists .Where(name => !string.IsNullOrWhiteSpace(name)) @@ -1186,7 +1223,8 @@ namespace Emby.Server.Implementations.Dto // }) // .ToList(); - var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]); + var albumArtistsLookup = artistsBatch + ?? _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]); dto.AlbumArtists = hasAlbumArtist.AlbumArtists .Where(name => !string.IsNullOrWhiteSpace(name)) diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 5fd23c9f50..85bf20cc2a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -1,8 +1,10 @@ #nullable disable using System; +using System.IO; using System.Linq; using Emby.Naming.Common; +using Emby.Server.Implementations.Library; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -81,10 +83,34 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV episode.ParentIndexNumber = 1; } + SetProviderIdFromPath(episode, args.Path); + return episode; } return null; } + + /// <summary> + /// Sets provider ids from the episode file name. + /// </summary> + /// <param name="item">The episode.</param> + /// <param name="path">The episode file path.</param> + private static void SetProviderIdFromPath(Episode item, string path) + { + var justName = Path.GetFileNameWithoutExtension(path.AsSpan()); + + var imdbId = justName.GetAttributeValue("imdbid"); + item.TrySetProviderId(MetadataProvider.Imdb, imdbId); + + var tvdbId = justName.GetAttributeValue("tvdbid"); + item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId); + + var tvmazeId = justName.GetAttributeValue("tvmazeid"); + item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId); + + var tmdbId = justName.GetAttributeValue("tmdbid"); + item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId); + } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 6cb63a28a2..6e9a38fd34 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -1,10 +1,15 @@ #nullable disable +using System; using System.Globalization; +using System.IO; +using System.Linq; using Emby.Naming.Common; using Emby.Naming.TV; +using Emby.Server.Implementations.Library; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using Microsoft.Extensions.Logging; @@ -77,6 +82,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV return null; } + + var hasAnyVideo = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories) + .Any(file => _namingOptions.VideoFileExtensions.Contains(Path.GetExtension(file))); + + if (!hasAnyVideo) + { + return null; + } } if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name)) @@ -91,10 +104,31 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV args.LibraryOptions.PreferredMetadataLanguage); } + SetProviderIdFromPath(season, path); + return season; } return null; } + + /// <summary> + /// Sets provider ids from the season folder name. + /// </summary> + /// <param name="item">The season.</param> + /// <param name="path">The season folder path.</param> + private static void SetProviderIdFromPath(Season item, string path) + { + var justName = Path.GetFileName(path.AsSpan()); + + var tvdbId = justName.GetAttributeValue("tvdbid"); + item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId); + + var tvmazeId = justName.GetAttributeValue("tvmazeid"); + item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId); + + var tmdbId = justName.GetAttributeValue("tmdbid"); + item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId); + } } } diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json index 7c6b08fb36..0e52e32c1b 100644 --- a/Emby.Server.Implementations/Localization/Core/ne.json +++ b/Emby.Server.Implementations/Localization/Core/ne.json @@ -45,7 +45,7 @@ "Genres": "विधाहरू", "Folders": "फोल्डरहरू", "Favorites": "मनपर्ने", - "FailedLoginAttemptWithUserName": "{0}को लग इन प्रयास असफल", + "FailedLoginAttemptWithUserName": "असफल लग इन प्रयास {0} देखि", "DeviceOnlineWithName": "{0}को साथ जडित", "DeviceOfflineWithName": "{0}बाट विच्छेदन भयो", "Collections": "संग्रह", diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 6fca5bc1ba..d8797e612b 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -320,6 +320,14 @@ namespace Emby.Server.Implementations.Localization { return value; } + + if (ratingsDictionary is not null && rating.Length > countryCode.Length + && rating.StartsWith(countryCode, StringComparison.OrdinalIgnoreCase) + && (rating[countryCode.Length] == '-' || rating[countryCode.Length] == ':') + && ratingsDictionary.TryGetValue(rating[(countryCode.Length + 1)..].Trim(), out var normalizedValue)) + { + return normalizedValue; + } } else { @@ -345,33 +353,68 @@ namespace Emby.Server.Implementations.Localization } } - // Try splitting by : to handle "Germany: FSK-18" - if (rating.Contains(':', StringComparison.OrdinalIgnoreCase)) + // Try splitting by country prefix separator to handle "US:PG-13", "Germany: FSK-18", "DE-FSK-18" + if (TryGetRatingScoreBySeparator(rating, ':', out var result) + || TryGetRatingScoreBySeparator(rating, '-', out result)) { - var ratingLevelRightPart = rating.AsSpan().RightPart(':'); - if (ratingLevelRightPart.Length != 0) - { - return GetRatingScore(ratingLevelRightPart.ToString()); - } + return result; } - // Handle prefix country code to handle "DE-18" - if (rating.Contains('-', StringComparison.OrdinalIgnoreCase)) + return null; + } + + private bool TryGetRatingScoreBySeparator(string rating, char separator, out ParentalRatingScore? result) + { + result = null; + + if (rating.IndexOf(separator, StringComparison.Ordinal) < 0) { - var ratingSpan = rating.AsSpan(); + return false; + } - // Extract culture from country prefix - var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString()); + var ratingSpan = rating.AsSpan(); + var countryPart = ratingSpan.LeftPart(separator).Trim().ToString(); + var ratingPart = ratingSpan.RightPart(separator).Trim().ToString(); + if (ratingPart.Length == 0) + { + return false; + } - var ratingLevelRightPart = ratingSpan.RightPart('-'); - if (ratingLevelRightPart.Length != 0) + string? resolvedCountryCode = null; + + if (_allParentalRatings.ContainsKey(countryPart)) + { + resolvedCountryCode = countryPart; + } + else + { + var culture = FindLanguageInfo(countryPart); + if (culture is not null) { - // Check rating system of culture - return GetRatingScore(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName); + resolvedCountryCode = culture.TwoLetterISOLanguageName; } } - return null; + if (resolvedCountryCode is not null + && _allParentalRatings.TryGetValue(resolvedCountryCode, out var countryRatings)) + { + if (countryRatings.TryGetValue(ratingPart, out result)) + { + return true; + } + + _logger.LogWarning( + "Rating '{Rating}' not found in the '{CountryCode}' rating system, treating as unrated", + rating, + resolvedCountryCode); + + return true; + } + + // Country not identified or no rating data available, try recursive lookup + result = GetRatingScore(ratingPart, resolvedCountryCode); + + return true; } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.json b/Emby.Server.Implementations/Localization/Ratings/ca.json index fa43a8f2b7..76550b64c3 100644 --- a/Emby.Server.Implementations/Localization/Ratings/ca.json +++ b/Emby.Server.Implementations/Localization/Ratings/ca.json @@ -3,7 +3,7 @@ "supportsSubScores": true, "ratings": [ { - "ratingStrings": ["E", "G", "TV-Y", "TV-G"], + "ratingStrings": ["C", "E", "G", "TV-Y", "TV-G"], "ratingScore": { "score": 0, "subScore": 0 @@ -24,13 +24,20 @@ } }, { - "ratingStrings": ["PG", "TV-PG"], + "ratingStrings": ["C8"], "ratingScore": { - "score": 9, + "score": 8, "subScore": 0 } }, { + "ratingStrings": ["PG", "TV-PG"], + "ratingScore": { + "score": 8, + "subScore": 1 + } + }, + { "ratingStrings": ["14A"], "ratingScore": { "score": 14, @@ -38,7 +45,7 @@ } }, { - "ratingStrings": ["TV-14"], + "ratingStrings": ["14+", "TV-14"], "ratingScore": { "score": 14, "subScore": 1 diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs index aa5fbbdf73..5c9a94cd36 100644 --- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs +++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs @@ -85,9 +85,17 @@ namespace Emby.Server.Implementations.Serialization /// <returns>System.Object.</returns> public object? DeserializeFromFile(Type type, string file) { - using (var stream = File.OpenRead(file)) + try { - return DeserializeFromStream(type, stream); + using (var stream = File.OpenRead(file)) + { + return DeserializeFromStream(type, stream); + } + } + catch (Exception ex) + { + ex.Data.Add("Filename", file); + throw; } } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index e2ddf86c7a..1782b53e10 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -973,7 +973,7 @@ namespace Emby.Server.Implementations.Session if (user.RememberAudioSelections) { - if (data.AudioStreamIndex != info.AudioStreamIndex) + if (info.AudioStreamIndex.HasValue && data.AudioStreamIndex != info.AudioStreamIndex) { data.AudioStreamIndex = info.AudioStreamIndex; changed = true; @@ -990,7 +990,7 @@ namespace Emby.Server.Implementations.Session if (user.RememberSubtitleSelections) { - if (data.SubtitleStreamIndex != info.SubtitleStreamIndex) + if (info.SubtitleStreamIndex.HasValue && data.SubtitleStreamIndex != info.SubtitleStreamIndex) { data.SubtitleStreamIndex = info.SubtitleStreamIndex; changed = true; @@ -1021,15 +1021,22 @@ namespace Emby.Server.Implementations.Session ArgumentNullException.ThrowIfNull(info); + var session = GetSession(info.SessionId); + + session.StopAutomaticProgress(); + if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0) { + // Ensure live stream is cleaned up before throwing, to prevent tuner + // resource leaks when stalled clients report a negative PositionTicks. + if (!string.IsNullOrEmpty(info.LiveStreamId)) + { + await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false); + } + throw new ArgumentOutOfRangeException(nameof(info), "The PlaybackStopInfo's PositionTicks was negative."); } - var session = GetSession(info.SessionId); - - session.StopAutomaticProgress(); - var libraryItem = info.ItemId.IsEmpty() ? null : GetNowPlayingItem(session, info.ItemId); @@ -2049,7 +2056,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - var adminUserIds = _userManager.Users + var adminUserIds = _userManager.GetUsers() .Where(i => i.HasPermission(PermissionKind.IsAdministrator)) .Select(i => i.Id) .ToList(); |
