aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs50
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs26
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs34
-rw-r--r--Emby.Server.Implementations/Localization/Core/ne.json2
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs77
-rw-r--r--Emby.Server.Implementations/Localization/Ratings/ca.json15
-rw-r--r--Emby.Server.Implementations/Serialization/MyXmlSerializer.cs12
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs21
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();