diff options
Diffstat (limited to 'Emby.Server.Implementations')
30 files changed, 875 insertions, 151 deletions
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index ef5fa8bef9..aa19948e36 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.AppBase } else { - _configurationFactories = [.._configurationFactories, factory]; + _configurationFactories = [.. _configurationFactories, factory]; } _configurationStores = _configurationFactories diff --git a/Emby.Server.Implementations/Chapters/ChapterManager.cs b/Emby.Server.Implementations/Chapters/ChapterManager.cs index 8a4721ce62..69cbe533c6 100644 --- a/Emby.Server.Implementations/Chapters/ChapterManager.cs +++ b/Emby.Server.Implementations/Chapters/ChapterManager.cs @@ -240,15 +240,15 @@ public class ChapterManager : IChapterManager public void SaveChapters(BaseItem item, IReadOnlyList<ChapterInfo> chapters) { if (!Supports(item)) - { - _logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id); - return; - } + { + _logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id); + return; + } // Remove any chapters that are outside of the runtime of the item var validChapters = chapters.Where(c => c.StartPositionTicks < item.RunTimeTicks).ToList(); _chapterRepository.SaveChapters(item.Id, validChapters); -} + } /// <inheritdoc /> public ChapterInfo? GetChapter(Guid baseItemId, int index) diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 0ede5665f9..295efd456c 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -4,12 +4,15 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; @@ -29,6 +32,7 @@ namespace Emby.Server.Implementations.Collections private readonly ILibraryMonitor _iLibraryMonitor; private readonly ILogger<CollectionManager> _logger; private readonly IProviderManager _providerManager; + private readonly ILinkedChildrenService _linkedChildrenService; private readonly ILocalizationManager _localizationManager; private readonly IApplicationPaths _appPaths; @@ -42,6 +46,7 @@ namespace Emby.Server.Implementations.Collections /// <param name="iLibraryMonitor">The library monitor.</param> /// <param name="loggerFactory">The logger factory.</param> /// <param name="providerManager">The provider manager.</param> + /// <param name="linkedChildrenService">The linked children service.</param> public CollectionManager( ILibraryManager libraryManager, IApplicationPaths appPaths, @@ -49,13 +54,15 @@ namespace Emby.Server.Implementations.Collections IFileSystem fileSystem, ILibraryMonitor iLibraryMonitor, ILoggerFactory loggerFactory, - IProviderManager providerManager) + IProviderManager providerManager, + ILinkedChildrenService linkedChildrenService) { _libraryManager = libraryManager; _fileSystem = fileSystem; _iLibraryMonitor = iLibraryMonitor; _logger = loggerFactory.CreateLogger<CollectionManager>(); _providerManager = providerManager; + _linkedChildrenService = linkedChildrenService; _localizationManager = localizationManager; _appPaths = appPaths; } @@ -120,6 +127,22 @@ namespace Emby.Server.Implementations.Collections return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded); } + /// <inheritdoc /> + public IEnumerable<BoxSet> GetCollectionsContainingItem(User user, Guid itemId) + { + ArgumentNullException.ThrowIfNull(user); + + if (itemId.IsEmpty()) + { + return Enumerable.Empty<BoxSet>(); + } + + return _linkedChildrenService + .GetManualLinkedParentIds(itemId, BaseItemKind.BoxSet) + .Select(parentId => _libraryManager.GetItemById<BoxSet>(parentId, user)) + .OfType<BoxSet>(); + } + private IEnumerable<BoxSet> GetCollections(User user) { var folder = GetCollectionsFolder(false).GetAwaiter().GetResult(); diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 321c7da1c4..f53328c7dd 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1366,6 +1366,41 @@ namespace Emby.Server.Implementations.Dto } } + if (options.PreferEpisodeParentPoster) + { + var episodeSeason = episode.Season; + var seasonPrimaryTag = episodeSeason is not null + ? GetTagAndFillBlurhash(dto, episodeSeason, ImageType.Primary) + : null; + + BaseItem? posterParent = null; + if (seasonPrimaryTag is not null) + { + dto.ParentPrimaryImageItemId = episodeSeason!.Id; + dto.ParentPrimaryImageTag = seasonPrimaryTag; + posterParent = episodeSeason; + } + else if (episodeSeries is not null && dto.SeriesPrimaryImageTag is not null) + { + dto.ParentPrimaryImageItemId = episodeSeries.Id; + dto.ParentPrimaryImageTag = dto.SeriesPrimaryImageTag; + posterParent = episodeSeries; + } + + if (posterParent is not null) + { + if (dto.ImageTags is not null && dto.ImageTags.Remove(ImageType.Primary, out var ownPrimaryTag)) + { + // Only drop the episode's own primary blurhash; keep the poster parent's. + dto.ImageBlurHashes?.GetValueOrDefault(ImageType.Primary)?.Remove(ownPrimaryTag); + } + + dto.SeriesPrimaryImageTag = null; + dto.PrimaryImageAspectRatio = null; + AttachPrimaryImageAspectRatio(dto, posterParent); + } + } + if (options.ContainsField(ItemFields.SeriesStudio)) { episodeSeries ??= episode.Series; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 30ff1bd333..a826db090f 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -3394,6 +3394,12 @@ namespace Emby.Server.Implementations.Library return _peopleRepository.GetPeopleNames(query); } + /// <inheritdoc/> + public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes) + { + return _peopleRepository.GetPeopleNamesByItems(itemIds, personTypes); + } + public void UpdatePeople(BaseItem item, List<PersonInfo> people) { UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult(); diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index fdb4c7328b..9ccfefa86e 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -24,6 +24,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; @@ -127,6 +128,11 @@ namespace Emby.Server.Implementations.Library return true; } + if (stream.IsVobSubSubtitleStream) + { + return true; + } + return false; } @@ -171,6 +177,7 @@ namespace Emby.Server.Implementations.Library public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken) { var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); + ResolveSymlinkPaths(mediaSources, enablePathSubstitution); // If file is strm or main media stream is missing, force a metadata refresh with remote probing if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder @@ -187,6 +194,7 @@ namespace Emby.Server.Implementations.Library cancellationToken).ConfigureAwait(false); mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); + ResolveSymlinkPaths(mediaSources, enablePathSubstitution); } var dynamicMediaSources = await GetDynamicMediaSources(item, cancellationToken).ConfigureAwait(false); @@ -319,6 +327,28 @@ namespace Emby.Server.Implementations.Library } } + /// <summary> + /// Resolves symlinked file paths on the supplied sources to the real on-disk target. + /// Skipped when <paramref name="enablePathSubstitution"/> is set because the path may + /// already have been rewritten to a UNC/URL meant for the client to consume directly. + /// </summary> + private static void ResolveSymlinkPaths(IReadOnlyList<MediaSourceInfo> sources, bool enablePathSubstitution) + { + if (enablePathSubstitution) + { + return; + } + + foreach (var source in sources) + { + if (source.Protocol == MediaProtocol.File + && FileSystemHelper.ResolveLinkTarget(source.Path, returnFinalTarget: true) is { Exists: true } target) + { + source.Path = target.FullName; + } + } + } + private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource) { var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter; @@ -440,10 +470,6 @@ namespace Emby.Server.Implementations.Library if (string.Equals(user.AudioLanguagePreference, "OriginalLanguage", StringComparison.OrdinalIgnoreCase)) { - originalLanguage = !string.IsNullOrWhiteSpace(originalLanguage) - ? originalLanguage.Split(',').FirstOrDefault() - : null; - if (user.PlayDefaultAudioTrack) { source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex( @@ -498,17 +524,7 @@ namespace Emby.Server.Implementations.Library var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections; - var originalLanguage = item?.OriginalLanguage ?? item switch - { - Episode episode => episode.Series.OriginalLanguage, - Video video => video.GetOwner() switch - { - Episode ownerEpisode => ownerEpisode.OriginalLanguage ?? ownerEpisode.Series.OriginalLanguage, - BaseItem owner => owner.OriginalLanguage, - null => null - }, - _ => null - }; + var originalLanguage = item?.GetInheritedOriginalLanguage(); SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection, originalLanguage); SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection); diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index cfa3e7c31d..7591359ea4 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.Library '[' => ']', '(' => ')', '{' => '}', - _ => '\0' + _ => '\0' }; if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-')) { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index c81a0adb89..769d721665 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// </summary> /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions) + public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions) { _logger = logger; _namingOptions = namingOptions; diff --git a/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs index 93aa0574c0..b4ed12a20c 100644 --- a/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs +++ b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs @@ -1,36 +1,72 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; -using Jellyfin.Database.Implementations.Enums; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Configuration; +using Microsoft.EntityFrameworkCore; +using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; namespace Emby.Server.Implementations.Library.SimilarItems; /// <summary> -/// Provides similar items for movies and trailers. +/// Provides similar items for movies and trailers using weighted scoring. /// </summary> -public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie>, ILocalSimilarItemsProvider<Trailer> +public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie>, ILocalSimilarItemsProvider<Trailer>, IBatchLocalSimilarItemsProvider { - private readonly ILibraryManager _libraryManager; + private const int GenreWeight = 10; + private const int TagWeight = 5; + private const int StudioWeight = 5; + private const int DirectorWeight = 50; + private const int ActorWeight = 15; + + // Caps the batch fan-out so downstream IN-list sizes (per-source scores, accessible-id + // load, navigation includes) stay bounded regardless of caller input. + private const int MaxBatchSourceItems = 64; + + private static readonly (ItemValueType Type, int Weight)[] _itemValueDimensions = + [ + (ItemValueType.Genre, GenreWeight), + (ItemValueType.Tags, TagWeight), + (ItemValueType.Studios, StudioWeight) + ]; + + private static readonly Dictionary<string, int> _personTypeWeights = new(StringComparer.Ordinal) + { + [nameof(PersonKind.Director)] = DirectorWeight, + [nameof(PersonKind.Actor)] = ActorWeight, + [nameof(PersonKind.GuestStar)] = ActorWeight, + }; + + private static readonly string[] _scoredPersonTypes = [.. _personTypeWeights.Keys]; + + private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; + private readonly IItemQueryHelpers _queryHelpers; private readonly IServerConfigurationManager _serverConfigurationManager; /// <summary> /// Initializes a new instance of the <see cref="MovieSimilarItemsProvider"/> class. /// </summary> - /// <param name="libraryManager">The library manager.</param> + /// <param name="dbProvider">The database context factory.</param> + /// <param name="queryHelpers">The shared query helpers.</param> /// <param name="serverConfigurationManager">The server configuration manager.</param> public MovieSimilarItemsProvider( - ILibraryManager libraryManager, + IDbContextFactory<JellyfinDbContext> dbProvider, + IItemQueryHelpers queryHelpers, IServerConfigurationManager serverConfigurationManager) { - _libraryManager = libraryManager; + _dbProvider = dbProvider; + _queryHelpers = queryHelpers; _serverConfigurationManager = serverConfigurationManager; } @@ -41,15 +77,17 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider; /// <inheritdoc/> - public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Movie item, SimilarItemsQuery query, CancellationToken cancellationToken) + public async Task<IReadOnlyList<BaseItemDto>> GetSimilarItemsAsync(Movie item, SimilarItemsQuery query, CancellationToken cancellationToken) { - return Task.FromResult(GetSimilarMovieItems(item, query)); + var results = await GetBatchSimilarItemsAsync([item], query, cancellationToken).ConfigureAwait(false); + return results.TryGetValue(item.Id, out var items) ? items : []; } /// <inheritdoc/> - public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Trailer item, SimilarItemsQuery query, CancellationToken cancellationToken) + public async Task<IReadOnlyList<BaseItemDto>> GetSimilarItemsAsync(Trailer item, SimilarItemsQuery query, CancellationToken cancellationToken) { - return Task.FromResult(GetSimilarMovieItems(item, query)); + var results = await GetBatchSimilarItemsAsync([item], query, cancellationToken).ConfigureAwait(false); + return results.TryGetValue(item.Id, out var items) ? items : []; } bool ILocalSimilarItemsProvider.Supports(Type itemType) @@ -63,29 +101,233 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie _ => throw new ArgumentException($"Unsupported item type {item.GetType()}", nameof(item)) }; - private IReadOnlyList<BaseItem> GetSimilarMovieItems(BaseItem item, SimilarItemsQuery query) + /// <inheritdoc/> + public async Task<Dictionary<Guid, IReadOnlyList<BaseItemDto>>> GetBatchSimilarItemsAsync( + IReadOnlyList<BaseItemDto> sourceItems, + SimilarItemsQuery query, + CancellationToken cancellationToken) { var includeItemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; - if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { includeItemTypes.Add(BaseItemKind.Trailer); includeItemTypes.Add(BaseItemKind.LiveTvProgram); } - var internalQuery = new InternalItemsQuery(query.User) + var limit = query.Limit ?? 50; + var dtoOptions = query.DtoOptions ?? new DtoOptions(); + + if (sourceItems.Count > MaxBatchSourceItems) { - Genres = item.Genres, - Tags = item.Tags, - Limit = query.Limit, - DtoOptions = query.DtoOptions ?? new DtoOptions(), - ExcludeItemIds = [.. query.ExcludeItemIds], - IncludeItemTypes = [.. includeItemTypes], - EnableGroupByMetadataKey = true, - EnableTotalRecordCount = false, - OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)] - }; + sourceItems = sourceItems.Take(MaxBatchSourceItems).ToList(); + } + + var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + // Phase 1: Score all candidates per source item + var sourceIds = sourceItems.Select(i => i.Id).ToList(); + var perSourceScores = await ComputeBatchScoresAsync(sourceIds, context, cancellationToken).ConfigureAwait(false); + + var allCandidateIds = new HashSet<Guid>(); + foreach (var (_, scores) in perSourceScores) + { + allCandidateIds.UnionWith( + scores.OrderByDescending(kvp => kvp.Value) + .Take(limit * 3) + .Select(kvp => kvp.Key)); + } + + var result = new Dictionary<Guid, IReadOnlyList<BaseItemDto>>(); + if (allCandidateIds.Count == 0) + { + return result; + } + + // Phase 2: One access filter for all candidates + var filter = new InternalItemsQuery(query.User) + { + IncludeItemTypes = [.. includeItemTypes], + ExcludeItemIds = [.. query.ExcludeItemIds], + DtoOptions = dtoOptions, + EnableGroupByMetadataKey = true, + EnableTotalRecordCount = false, + IsMovie = true, + IsPlayed = false + }; + + _queryHelpers.PrepareFilterQuery(filter); + var baseQuery = _queryHelpers.PrepareItemQuery(context, filter); + baseQuery = _queryHelpers.TranslateQuery(baseQuery, context, filter); + + var allCandidateIdsList = allCandidateIds.ToList(); + var accessibleItems = await baseQuery + .WhereOneOrMany(allCandidateIdsList, e => e.Id) + .Select(e => new { e.Id, e.PresentationUniqueKey }) + .ToListAsync(cancellationToken).ConfigureAwait(false); + + // Phase 3: Pick top IDs per source, dedup by PresentationUniqueKey + var allOrderedIds = new HashSet<Guid>(); + var perSourceOrderedIds = new Dictionary<Guid, List<Guid>>(); + + foreach (var item in sourceItems) + { + if (!perSourceScores.TryGetValue(item.Id, out var scores)) + { + continue; + } + + var orderedIds = accessibleItems + .Where(x => scores.ContainsKey(x.Id)) + .OrderByDescending(x => scores.GetValueOrDefault(x.Id)) + .DistinctBy(x => x.PresentationUniqueKey) + .Take(limit) + .Select(x => x.Id) + .ToList(); + + if (orderedIds.Count > 0) + { + perSourceOrderedIds[item.Id] = orderedIds; + allOrderedIds.UnionWith(orderedIds); + } + } + + if (allOrderedIds.Count == 0) + { + return result; + } + + // Phase 4: One entity load for all results + var allOrderedIdsList = allOrderedIds.ToList(); + var entities = await _queryHelpers.ApplyNavigations( + context.BaseItems.AsNoTracking().WhereOneOrMany(allOrderedIdsList, e => e.Id), + filter) + .AsSplitQuery() + .ToListAsync(cancellationToken).ConfigureAwait(false); + + var entitiesById = entities + .Select(e => _queryHelpers.DeserializeBaseItem(e, filter.SkipDeserialization)) + .Where(dto => dto is not null) + .ToDictionary(i => i!.Id); + + // Phase 5: Split by source, preserving score order + foreach (var (sourceId, orderedIds) in perSourceOrderedIds) + { + var items = orderedIds + .Where(entitiesById.ContainsKey) + .Select(id => entitiesById[id]!) + .ToList(); + + if (items.Count > 0) + { + result[sourceId] = items; + } + } + + return result; + } + } + + private static async Task<Dictionary<Guid, Dictionary<Guid, int>>> ComputeBatchScoresAsync(List<Guid> sourceIds, JellyfinDbContext context, CancellationToken cancellationToken) + { + var result = new Dictionary<Guid, Dictionary<Guid, int>>(); + foreach (var id in sourceIds) + { + result[id] = []; + } + + foreach (var (valueType, weight) in _itemValueDimensions) + { + var sourceRows = await context.ItemValuesMap.AsNoTracking() + .Where(m => sourceIds.Contains(m.ItemId) && m.ItemValue.Type == valueType) + .Select(m => new { m.ItemId, Key = m.ItemValue.CleanValue }) + .ToListAsync(cancellationToken).ConfigureAwait(false); + + var sourceMap = sourceRows.GroupBy(r => r.ItemId).ToDictionary(g => g.Key, g => g.Select(x => x.Key).ToHashSet()); + var allKeys = sourceMap.Values.SelectMany(v => v).Distinct().ToList(); + if (allKeys.Count == 0) + { + continue; + } + + var candidateRows = await context.ItemValuesMap.AsNoTracking() + .Where(m => m.ItemValue.Type == valueType && allKeys.Contains(m.ItemValue.CleanValue)) + .Select(m => new { m.ItemId, Key = m.ItemValue.CleanValue }) + .ToListAsync(cancellationToken).ConfigureAwait(false); + + var keyToCandidates = candidateRows.GroupBy(r => r.Key).ToDictionary(g => g.Key, g => g.Select(x => x.ItemId).ToList()); + ApplyDimensionScores(sourceIds, sourceMap, keyToCandidates, weight, result); + } + + var personSourceRows = await context.PeopleBaseItemMap.AsNoTracking() + .Where(m => sourceIds.Contains(m.ItemId) && _scoredPersonTypes.Contains(m.People.PersonType)) + .Select(m => new { m.ItemId, m.PeopleId, m.People.PersonType }) + .ToListAsync(cancellationToken).ConfigureAwait(false); + + if (personSourceRows.Count > 0) + { + var personCandidateRows = await context.PeopleBaseItemMap.AsNoTracking() + .Where(m => context.PeopleBaseItemMap + .Where(s => sourceIds.Contains(s.ItemId) && _scoredPersonTypes.Contains(s.People.PersonType)) + .Select(s => s.PeopleId) + .Contains(m.PeopleId)) + .Select(m => new { m.ItemId, m.PeopleId }) + .ToListAsync(cancellationToken).ConfigureAwait(false); + + var personToCandidates = personCandidateRows + .GroupBy(r => r.PeopleId) + .ToDictionary(g => g.Key, g => g.Select(x => x.ItemId).ToList()); + + foreach (var weightGroup in personSourceRows.GroupBy(r => _personTypeWeights[r.PersonType!])) + { + var sourceMap = weightGroup + .GroupBy(r => r.ItemId) + .ToDictionary(g => g.Key, g => g.Select(x => x.PeopleId).ToHashSet()); + ApplyDimensionScores(sourceIds, sourceMap, personToCandidates, weightGroup.Key, result); + } + } + + foreach (var sourceId in sourceIds) + { + var scoreMap = result[sourceId]; + scoreMap.Remove(sourceId); + if (scoreMap.Count == 0) + { + result.Remove(sourceId); + } + } - return _libraryManager.GetItemList(internalQuery); + return result; + } + + private static void ApplyDimensionScores<TKey>( + List<Guid> sourceIds, + Dictionary<Guid, HashSet<TKey>> sourceMap, + Dictionary<TKey, List<Guid>> keyToCandidates, + int weight, + Dictionary<Guid, Dictionary<Guid, int>> result) + where TKey : notnull + { + foreach (var sourceId in sourceIds) + { + if (!sourceMap.TryGetValue(sourceId, out var sourceKeys)) + { + continue; + } + + var scoreMap = result[sourceId]; + foreach (var key in sourceKeys) + { + if (!keyToCandidates.TryGetValue(key, out var candidates)) + { + continue; + } + + foreach (var candidateId in candidates) + { + scoreMap[candidateId] = scoreMap.GetValueOrDefault(candidateId) + weight; + } + } + } } } diff --git a/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs index b56779cf3f..d923cff07e 100644 --- a/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs +++ b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs @@ -8,12 +8,16 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions.Json; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; @@ -30,6 +34,7 @@ public class SimilarItemsManager : ISimilarItemsManager private readonly IServerApplicationPaths _appPaths; private readonly ILibraryManager _libraryManager; private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _serverConfigurationManager; private ISimilarItemsProvider[] _similarItemsProviders = []; /// <summary> @@ -39,16 +44,19 @@ public class SimilarItemsManager : ISimilarItemsManager /// <param name="appPaths">The server application paths.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="fileSystem">The file system.</param> + /// <param name="serverConfigurationManager">The server configuration manager.</param> public SimilarItemsManager( ILogger<SimilarItemsManager> logger, IServerApplicationPaths appPaths, ILibraryManager libraryManager, - IFileSystem fileSystem) + IFileSystem fileSystem, + IServerConfigurationManager serverConfigurationManager) { _logger = logger; _appPaths = appPaths; _libraryManager = libraryManager; _fileSystem = fileSystem; + _serverConfigurationManager = serverConfigurationManager; } /// <inheritdoc/> @@ -117,6 +125,7 @@ public class SimilarItemsManager : ISimilarItemsManager var allResults = new List<(BaseItem Item, float Score)>(); var excludeIds = new HashSet<Guid> { item.Id }; + var excludeKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { item.GetPresentationUniqueKey() }; foreach (var (providerOrder, provider) in orderedProviders.Index()) { if (allResults.Count >= requestedLimit || cancellationToken.IsCancellationRequested) @@ -141,7 +150,9 @@ public class SimilarItemsManager : ISimilarItemsManager foreach (var (position, resultItem) in items.Index()) { - if (excludeIds.Add(resultItem.Id)) + var isNewId = excludeIds.Add(resultItem.Id); + var isNewKey = excludeKeys.Add(resultItem.GetPresentationUniqueKey()); + if (isNewId && isNewKey) { var score = CalculateScore(null, providerOrder, position); allResults.Add((resultItem, score)); @@ -155,7 +166,7 @@ public class SimilarItemsManager : ISimilarItemsManager var cachedReferences = await TryReadSimilarItemsCacheAsync(cachePath, cancellationToken).ConfigureAwait(false); if (cachedReferences is not null) { - var resolvedItems = ResolveRemoteReferences(cachedReferences, providerOrder, user, dtoOptions, itemKind, excludeIds); + var resolvedItems = ResolveRemoteReferences(cachedReferences, providerOrder, user, dtoOptions, itemKind, excludeIds, excludeKeys); allResults.AddRange(resolvedItems); continue; } @@ -183,7 +194,7 @@ public class SimilarItemsManager : ISimilarItemsManager if (pendingBatch.Count >= BatchSize) { - var resolvedItems = ResolveRemoteReferences(pendingBatch, providerOrder, user, dtoOptions, itemKind, excludeIds); + var resolvedItems = ResolveRemoteReferences(pendingBatch, providerOrder, user, dtoOptions, itemKind, excludeIds, excludeKeys); allResults.AddRange(resolvedItems); remaining -= resolvedItems.Count; pendingBatch.Clear(); @@ -198,7 +209,7 @@ public class SimilarItemsManager : ISimilarItemsManager // Resolve any remaining references in the last partial batch if (pendingBatch.Count > 0) { - var resolvedItems = ResolveRemoteReferences(pendingBatch, providerOrder, user, dtoOptions, itemKind, excludeIds); + var resolvedItems = ResolveRemoteReferences(pendingBatch, providerOrder, user, dtoOptions, itemKind, excludeIds, excludeKeys); allResults.AddRange(resolvedItems); } @@ -225,20 +236,230 @@ public class SimilarItemsManager : ISimilarItemsManager .ToList(); } + /// <inheritdoc/> + public async Task<IReadOnlyList<SimilarItemsRecommendation>> GetMovieRecommendationsAsync( + User? user, + Guid parentId, + int categoryLimit, + int itemLimit, + DtoOptions dtoOptions, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(dtoOptions); + + var recentlyPlayedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + IncludeItemTypes = [BaseItemKind.Movie], + OrderBy = [(ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending)], + Limit = 7, + ParentId = parentId, + Recursive = true, + IsPlayed = true, + EnableGroupByMetadataKey = true, + DtoOptions = dtoOptions + }); + + var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); + } + + var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + IncludeItemTypes = itemTypes.ToArray(), + IsMovie = true, + OrderBy = [(ItemSortBy.Random, SortOrder.Descending)], + Limit = 10, + IsFavoriteOrLiked = true, + ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(), + EnableGroupByMetadataKey = true, + ParentId = parentId, + Recursive = true, + DtoOptions = dtoOptions + }); + + var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)).ToList(); + var recentDirectors = GetPeopleNames(mostRecentMovies, [PersonType.Director]); + var recentActors = GetPeopleNames(mostRecentMovies, [PersonType.Actor, PersonType.GuestStar]); + + // Cap baseline items to categoryLimit - the round-robin can't use more categories than that. + var recentlyPlayedBaseline = recentlyPlayedMovies.Count > categoryLimit + ? recentlyPlayedMovies.Take(categoryLimit).ToList() + : recentlyPlayedMovies; + var likedBaseline = likedMovies.Count > categoryLimit + ? likedMovies.Take(categoryLimit).ToList() + : likedMovies; + + var batchQuery = new SimilarItemsQuery + { + User = user, + Limit = itemLimit, + DtoOptions = dtoOptions + }; + + var similarToRecentlyPlayed = await GetSimilarItemsRecommendationsAsync( + recentlyPlayedBaseline, + RecommendationType.SimilarToRecentlyPlayed, + batchQuery, + cancellationToken).ConfigureAwait(false); + + var similarToLiked = await GetSimilarItemsRecommendationsAsync( + likedBaseline, + RecommendationType.SimilarToLikedItem, + batchQuery, + cancellationToken).ConfigureAwait(false); + + var hasDirectorFromRecentlyPlayed = GetPersonRecommendations(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed, itemTypes); + var hasActorFromRecentlyPlayed = GetPersonRecommendations(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed, itemTypes); + + // Use a single enumerator per list, listed twice so MoveNext advances it + // twice per round-robin pass (giving these categories double weight). + // IMPORTANT: Declare as IEnumerator<T> to box the List<T>.Enumerator struct once; + // using var would box separately per list insertion, creating independent copies. + IEnumerator<SimilarItemsRecommendation> similarToRecentlyPlayedEnum = similarToRecentlyPlayed.GetEnumerator(); + IEnumerator<SimilarItemsRecommendation> similarToLikedEnum = similarToLiked.GetEnumerator(); + + var categoryTypes = new List<IEnumerator<SimilarItemsRecommendation>> + { + similarToRecentlyPlayedEnum, + similarToRecentlyPlayedEnum, + similarToLikedEnum, + similarToLikedEnum, + hasDirectorFromRecentlyPlayed.GetEnumerator(), + hasActorFromRecentlyPlayed.GetEnumerator() + }; + + var categories = new List<SimilarItemsRecommendation>(); + while (categories.Count < categoryLimit) + { + var allEmpty = true; + foreach (var category in categoryTypes) + { + if (category.MoveNext()) + { + categories.Add(category.Current); + allEmpty = false; + + if (categories.Count >= categoryLimit) + { + break; + } + } + } + + if (allEmpty) + { + break; + } + } + + return [.. categories.OrderBy(i => i.RecommendationType)]; + } + + private async Task<IReadOnlyList<SimilarItemsRecommendation>> GetSimilarItemsRecommendationsAsync( + IReadOnlyList<BaseItem> baselineItems, + RecommendationType recommendationType, + SimilarItemsQuery query, + CancellationToken cancellationToken) + { + var batchProvider = _similarItemsProviders + .OfType<IBatchLocalSimilarItemsProvider>() + .FirstOrDefault(); + + if (batchProvider is null || baselineItems.Count == 0) + { + return []; + } + + var batchResults = await batchProvider.GetBatchSimilarItemsAsync(baselineItems, query, cancellationToken).ConfigureAwait(false); + + var recommendations = new List<SimilarItemsRecommendation>(baselineItems.Count); + foreach (var baseline in baselineItems) + { + if (batchResults.TryGetValue(baseline.Id, out var similar) && similar.Count > 0) + { + recommendations.Add(new SimilarItemsRecommendation + { + BaselineItemName = baseline.Name, + CategoryId = baseline.Id, + RecommendationType = recommendationType, + Items = similar + }); + } + } + + return recommendations; + } + + private IEnumerable<SimilarItemsRecommendation> GetPersonRecommendations( + User? user, + IReadOnlyList<string> names, + int itemLimit, + DtoOptions dtoOptions, + RecommendationType type, + IReadOnlyList<BaseItemKind> itemTypes) + { + var personTypes = type == RecommendationType.HasDirectorFromRecentlyPlayed + ? [PersonType.Director] + : Array.Empty<string>(); + + foreach (var name in names) + { + var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + Person = name, + Limit = itemLimit + 2, + PersonTypes = personTypes, + IncludeItemTypes = itemTypes.ToArray(), + IsMovie = true, + IsPlayed = false, + EnableGroupByMetadataKey = true, + DtoOptions = dtoOptions + }) + .DistinctBy(i => i.GetProviderId(MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) + .Take(itemLimit) + .ToList(); + + if (items.Count > 0) + { + yield return new SimilarItemsRecommendation + { + BaselineItemName = name, + CategoryId = name.GetMD5(), + RecommendationType = type, + Items = items + }; + } + } + } + + private IReadOnlyList<string> GetPeopleNames(IReadOnlyList<BaseItem> items, IReadOnlyList<string> personTypes) + { + var itemIds = items.Select(i => i.Id).ToArray(); + return _libraryManager.GetPeopleNamesByItems(itemIds, personTypes) + .Values + .SelectMany(names => names) + .Distinct() + .ToArray(); + } + private List<(BaseItem Item, float Score)> ResolveRemoteReferences( IReadOnlyList<SimilarItemReference> references, int providerOrder, User? user, DtoOptions dtoOptions, BaseItemKind itemKind, - HashSet<Guid> excludeIds) + HashSet<Guid> excludeIds, + HashSet<string> excludeKeys) { if (references.Count == 0) { return []; } - var resolvedById = new Dictionary<Guid, (BaseItem Item, float Score)>(); + var resolvedByKey = new Dictionary<string, (BaseItem Item, float Score)>(StringComparer.OrdinalIgnoreCase); var providerLookup = new Dictionary<(string ProviderName, string ProviderId), (float? Score, int Position)>(StringTupleComparer.Instance); foreach (var (position, match) in references.Index()) @@ -269,7 +490,13 @@ public class SimilarItemsManager : ISimilarItemsManager foreach (var item in items) { - if (excludeIds.Contains(item.Id) || resolvedById.ContainsKey(item.Id)) + if (excludeIds.Contains(item.Id)) + { + continue; + } + + var presentationKey = item.GetPresentationUniqueKey(); + if (excludeKeys.Contains(presentationKey)) { continue; } @@ -279,10 +506,9 @@ public class SimilarItemsManager : ISimilarItemsManager if (item.TryGetProviderId(providerName, out var itemProviderId) && providerLookup.TryGetValue((providerName, itemProviderId), out var matchInfo)) { var score = CalculateScore(matchInfo.Score, providerOrder, matchInfo.Position); - if (!resolvedById.TryGetValue(item.Id, out var existing) || existing.Score < score) + if (!resolvedByKey.TryGetValue(presentationKey, out var existing) || existing.Score < score) { - excludeIds.Add(item.Id); - resolvedById[item.Id] = (item, score); + resolvedByKey[presentationKey] = (item, score); } break; @@ -290,7 +516,13 @@ public class SimilarItemsManager : ISimilarItemsManager } } - return [.. resolvedById.Values]; + foreach (var (key, entry) in resolvedByKey) + { + excludeIds.Add(entry.Item.Id); + excludeKeys.Add(key); + } + + return [.. resolvedByKey.Values]; } private static float CalculateScore(float? matchScore, int providerOrder, int position) diff --git a/Emby.Server.Implementations/Localization/Core/br.json b/Emby.Server.Implementations/Localization/Core/br.json new file mode 100644 index 0000000000..cedc87e5a6 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/br.json @@ -0,0 +1,63 @@ +{ + "Artists": "Arzourien", + "AuthenticationSucceededWithUserName": "{0} kennasket gant berzh", + "Books": "Levrioù", + "ChapterNameValue": "Pennad {0}", + "Collections": "Dastumadegoù", + "Default": "Dre ziouer", + "External": "Diavaez", + "FailedLoginAttemptWithUserName": "Kennaskañ c'hwitet gant {0}", + "Favorites": "Sinedoù", + "Folders": "Teuliadoù", + "Forced": "Rediet", + "Genres": "Doareoù", + "HeaderContinueWatching": "Kenderc'hel da sellet", + "HeaderFavoriteEpisodes": "Rannoù Karetañ", + "HeaderFavoriteShows": "Heuliadennoù Karetañ", + "HeaderLiveTV": "TV war-eeun", + "HeaderNextUp": "Da c'houde", + "HearingImpaired": "Tud fall o C'hleved", + "HomeVideos": "Videoioù Personel", + "Inherit": "Hêrezhiñ", + "LabelIpAddressValue": "Chomlec'h IP : {0}", + "LabelRunningTimeValue": "Padelezh : {0}", + "Latest": "Diwezhañ", + "AppDeviceValues": "Arload : {0}, Trobarzhell : {1}", + "LyricDownloadFailureFromForItem": "C'hwitet eo pellgargañ ar c'homzoù eus {0} evit {1}", + "MixedContent": "Danvez mesket", + "Movies": "Filmoù", + "Music": "Sonerezh", + "MusicVideos": "Videoioù Sonerezh", + "NameInstallFailed": "{0} c'hwitet war ar staliadur", + "NameSeasonNumber": "Koulzad {0}", + "NameSeasonUnknown": "Koulzad Dianav", + "NewVersionIsAvailable": "Ur stumm Servijer Jellyfin nevez a c'haller pellgargañ.", + "NotificationOptionApplicationUpdateAvailable": "Hizivadur an arload zo da gaout", + "NotificationOptionApplicationUpdateInstalled": "Hizivadur an arload staliet", + "NotificationOptionAudioPlayback": "Lenn aodio lañset", + "NotificationOptionAudioPlaybackStopped": "Lenn aodio ehanet", + "Original": "Orin", + "Photos": "Fotoioù", + "Shows": "Heuliadennoù", + "Undefined": "Dianav", + "TasksMaintenanceCategory": "Trezalc’h", + "TasksLibraryCategory": "Levraoueg", + "TasksApplicationCategory": "Arload", + "NotificationOptionInstallationFailed": "C'hwitet war staliañ", + "NotificationOptionPluginError": "Fazi Askouezh", + "NotificationOptionPluginInstalled": "Askouezh staliet", + "NotificationOptionPluginUninstalled": "Askouezh distaliet", + "ScheduledTaskFailedWithName": "c'hwitadenn war {0}", + "TvShows": "Heuliadennoù TV", + "VersionNumber": "Stumm {0}", + "TasksChannelsCategory": "Chadennoù enlinenn", + "TaskAudioNormalization": "Normalizadur an aodio", + "TaskRefreshPeople": "Freskaat ar gomedianed", + "TaskUpdatePlugins": "Hizivaat an askouezhioù", + "TaskRefreshChannels": "Freskaat ar chadennoù", + "TaskOptimizeDatabase": "Gwellekaat an diaz roadennoù", + "TaskKeyframeExtractor": "Eztenner skeudennoù-alc'hwez", + "NotificationOptionCameraImageUploaded": "Karget eo skeudenn ar benveg", + "NotificationOptionNewLibraryContent": "Danvez nevez ouzhpennet", + "NotificationOptionPluginUpdateInstalled": "Staliet eo hizivadur an askouezh" +} diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 9e3d4456a8..28f0e2df97 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.", "CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.", "CleanupUserDataTask": "Pročistit uživatelská data", - "Original": "Originál" + "Original": "Originál", + "LyricDownloadFailureFromForItem": "Nepodařilo se stáhnout texty pro {1} ze služby {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index e30528b3cd..697d9c090f 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -106,5 +106,7 @@ "TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.", "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment.", "CleanupUserDataTask": "Brugerdata oprydningsopgave", - "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage." + "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage.", + "LyricDownloadFailureFromForItem": "Sangtekster kunne ikke downloades fra {0} til {1}", + "Original": "Original" } diff --git a/Emby.Server.Implementations/Localization/Core/enm.json b/Emby.Server.Implementations/Localization/Core/enm.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/Emby.Server.Implementations/Localization/Core/enm.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index 35efcf74d3..563dce8fe6 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -108,5 +108,5 @@ "CleanupUserDataTask": "Tarea de limpieza de datos del usuario", "CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días.", "Original": "Original", - "LyricDownloadFailureFromForItem": "No se pudieron descargar las letras desde {0} para {1}." + "LyricDownloadFailureFromForItem": "No se pudieron descargar las letras desde {0} para {1}" } diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index d08f652e58..d080eb0236 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -106,5 +106,7 @@ "TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti", "TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan.", "CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä", - "CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään." + "CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään.", + "LyricDownloadFailureFromForItem": "Sanoitusten lataus kohteesta {0} kappaleelle {1} epäonnistui", + "Original": "Alkuperäinen" } diff --git a/Emby.Server.Implementations/Localization/Core/he_IL.json b/Emby.Server.Implementations/Localization/Core/he_IL.json index dedbc56a74..b551608fd0 100644 --- a/Emby.Server.Implementations/Localization/Core/he_IL.json +++ b/Emby.Server.Implementations/Localization/Core/he_IL.json @@ -16,5 +16,97 @@ "HeaderLiveTV": "טלוויזיה בשידור חי", "HeaderNextUp": "הבא", "HearingImpaired": "ללקויי שמיעה", - "HomeVideos": "סרטונים ביתיים" + "HomeVideos": "סרטונים ביתיים", + "AppDeviceValues": "אפליקציה: {0}, מכשיר: {1}", + "AuthenticationSucceededWithUserName": "{0} אומת בהצלחה", + "Default": "בררת מחדל", + "FailedLoginAttemptWithUserName": "התחברות נכשלה מ {0}", + "Forced": "בכוח", + "Inherit": "ירש", + "LabelIpAddressValue": "כתובת IP: {0}", + "LabelRunningTimeValue": "זמן ריצה: {0}", + "Latest": "הכי חדש", + "LyricDownloadFailureFromForItem": "מילות שיר נכשלו לרדת מ{0} בשביל {1}", + "MixedContent": "תוכן מעורב", + "MusicVideos": "סרטוני מוזיקה", + "NameInstallFailed": "{0} התכנות כושלות", + "NameSeasonUnknown": "עונה לא ידוע", + "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.", + "NotificationOptionApplicationUpdateAvailable": "גרסת אפליקציה חדשה זמינה להורדה", + "NotificationOptionApplicationUpdateInstalled": "עדכון אפליקציה הותקן", + "NotificationOptionAudioPlayback": "החלה השמעת אודיו", + "NotificationOptionAudioPlaybackStopped": "ניגון השמע הופסק", + "NotificationOptionCameraImageUploaded": "תמונת מצלמה עודכן", + "NotificationOptionInstallationFailed": "התקנה נכשלה", + "NotificationOptionNewLibraryContent": "תוכן חדש נוסף", + "NotificationOptionPluginError": "תוסף נכשל", + "NotificationOptionPluginInstalled": "תוסף הותקן", + "NotificationOptionPluginUninstalled": "תוסף נמחק", + "NotificationOptionPluginUpdateInstalled": "עידכון לתוסף הותקן", + "NotificationOptionServerRestartRequired": "נדרש התחול מחדש לשרת", + "NotificationOptionTaskFailed": "כשל במשימה מתוכננת", + "NotificationOptionUserLockedOut": "המשתמש ננעל", + "NotificationOptionVideoPlayback": "החלה הפעלת וידאו", + "NotificationOptionVideoPlaybackStopped": "הפעלת הסרטון הופסקה", + "Original": "מקורי", + "Photos": "תמונות", + "PluginInstalledWithName": "{0} הותקן", + "PluginUninstalledWithName": "{0} נמחק", + "PluginUpdatedWithName": "{0} עודכן", + "ScheduledTaskFailedWithName": "{0} נכשל", + "Shows": "סדרות", + "StartupEmbyServerIsLoading": "שרת Jellyfin נטען. אנא נסה שוב בקרוב.", + "SubtitleDownloadFailureFromForItem": "הורדת הכתוביות מ-{0} עבור {1} נכשלה", + "TvShows": "תוכניות טלויזיה", + "Undefined": "לא מוגדר", + "UserCreatedWithName": "המשתמש {0} נוצר", + "UserDeletedWithName": "המשתמש {0} נמחק", + "UserDownloadingItemWithValues": "{0} מוריד את {1}", + "UserLockedOutWithName": "המשתמש {0} ננעל בחוץ", + "UserOfflineFromDevice": "{0} התנתק מ-{1}", + "UserOnlineFromDevice": "{0} מחובר מ-{1}", + "UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}", + "UserStartedPlayingItemWithValues": "{0} מנגן ב-{1} ב-{2}", + "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} ב-{2}", + "VersionNumber": "גרסה {0}", + "TasksMaintenanceCategory": "תחזוקה", + "TasksLibraryCategory": "ספריה", + "TasksApplicationCategory": "אפליקציה", + "TasksChannelsCategory": "ערוצי אינטרנט", + "TaskCleanActivityLog": "נקה יומן פעילות", + "TaskCleanActivityLogDescription": "מוחק רשומות יומן פעילות ישנות יותר מהגיל שהוגדר.", + "TaskCleanCache": "נקה ספריית מטמון", + "TaskCleanCacheDescription": "מוחק קבצי מטמון שאינם נחוצים עוד על ידי המערכת.", + "TaskRefreshChapterImages": "חלץ תמונות פרק", + "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות עבור סרטונים שיש להם פרקים.", + "TaskAudioNormalization": "נורמליזציה של שמע", + "TaskAudioNormalizationDescription": "סורק קבצים לאיתור נתוני נרמול שמע.", + "TaskRefreshLibrary": "סרוק ספריית מדיה", + "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך לאיתור קבצים חדשים ומרענן מטא-דאטה.", + "TaskCleanLogs": "נקה ספריית יומן", + "TaskCleanLogsDescription": "מוחק קבצי יומן שגילם עולה על {0} ימים.", + "TaskRefreshPeople": "רענן אנשים", + "TaskRefreshPeopleDescription": "מעדכן מטא-דאטה עבור שחקנים ובמאים בספריית המדיה שלך.", + "TaskRefreshTrickplayImages": "צור תמונות Trickplay", + "TaskRefreshTrickplayImagesDescription": "יוצר תצוגות מקדימות של trickplay עבור סרטונים בספריות מופעלות.", + "TaskUpdatePlugins": "עדכן פלאגינים", + "TaskUpdatePluginsDescription": "מוריד ומתקין עדכונים עבור תוספים שתצורתם נקבעה לעדכון אוטומטי.", + "TaskCleanTranscode": "נקה ספריית קידוד", + "TaskCleanTranscodeDescription": "תמחוק את קבצי הקידוד בני יותר מיום.", + "TaskRefreshChannels": "רענן ערוצים", + "TaskRefreshChannelsDescription": "מרענן את פרטי ערוץ האינטרנט.", + "TaskDownloadMissingLyrics": "הורד מילות שיר חסרות", + "TaskDownloadMissingLyricsDescription": "הורדות מילים לשירים", + "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות", + "TaskDownloadMissingSubtitlesDescription": "מחפש באינטרנט אחר כתוביות חסרות בהתבסס על תצורת מטא-דאטה.", + "TaskOptimizeDatabase": "בצע אופטימיזציה של מסד הנתונים", + "TaskOptimizeDatabaseDescription": "דוחס את מסד הנתונים וקיצוץ שטח פנוי. הפעלת משימה זו לאחר סריקת הספרייה או ביצוע שינויים אחרים שמשמעותם שינויים בבסיס הנתונים עשויה לשפר את הביצועים.", + "TaskKeyframeExtractor": "מחלץ פריים מרכזי", + "TaskKeyframeExtractorDescription": "מחלץ פריימים מרכזיים מקבצי וידאו כדי ליצור רשימות השמעה HLS מדויקות יותר. משימה זו עשויה להימשך זמן רב.", + "TaskExtractMediaSegments": "סריקת מקטעי מדיה", + "TaskExtractMediaSegmentsDescription": "מחלץ או משיג קטעי מדיה מתוספים התומכים ב-MediaSegment.", + "TaskMoveTrickplayImages": "העברת מיקום תמונת Trickplay", + "TaskMoveTrickplayImagesDescription": "מעביר קבצי trickplay קיימים בהתאם להגדרות הספרייה.", + "CleanupUserDataTask": "משימת ניקוי נתוני משתמש", + "CleanupUserDataTaskDescription": "מנקה את כל נתוני המשתמש (מצב צפייה, סטטוס מועדף וכו') ממדיה שכבר לא הייתה קיימת במשך 90 יום לפחות." } diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json index c0b7a196f7..f7ca19d7f0 100644 --- a/Emby.Server.Implementations/Localization/Core/ka.json +++ b/Emby.Server.Implementations/Localization/Core/ka.json @@ -2,7 +2,7 @@ "Genres": "ჟანრები", "TasksApplicationCategory": "აპლიკაცია", "AppDeviceValues": "აპლიკაცია: {0}, მოწყობილობა: {1}", - "Artists": "არტისტი", + "Artists": "შემსრულებლები", "AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია", "Books": "წიგნები", "Forced": "იძულებითი", @@ -20,9 +20,9 @@ "External": "გარე", "HeaderFavoriteEpisodes": "რჩეული ეპიზოდები", "HearingImpaired": "სმენადაქვეითებული", - "LabelRunningTimeValue": "ხანგრძლივობა: {0}", + "LabelRunningTimeValue": "გაშვების დრო: {0}", "MixedContent": "შერეული შემცველობა", - "MusicVideos": "მუსიკალური ვიდეოები", + "MusicVideos": "მუსიკის ვიდეოები", "NotificationOptionInstallationFailed": "დაყენების შეცდომა", "NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია", "NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია", @@ -31,54 +31,54 @@ "PluginUninstalledWithName": "{0} წაიშალა", "VersionNumber": "ვერსია {0}", "TasksChannelsCategory": "ინტერნეტ-არხები", - "TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.", + "TaskRefreshChannelsDescription": "განაახლებს ინტერნეტ-არხის ინფორმაციას.", "Collections": "კოლექციები", - "Default": "ნაგულისხმები", + "Default": "ნაგულისხმევი", "Favorites": "რჩეულები", "Folders": "საქაღალდეები", "HeaderFavoriteShows": "რჩეული სერიალები", - "HeaderLiveTV": "ლაივ ტელევიზია", + "HeaderLiveTV": "ცოცხალი ტელევიზია", "HeaderNextUp": "შემდეგი", "HomeVideos": "სახლის ვიდეოები", "NameSeasonNumber": "სეზონი {0}", "NameSeasonUnknown": "სეზონი უცნობია", - "NotificationOptionPluginError": "მოდულის შეცდომა", - "NotificationOptionPluginInstalled": "მოდული დაყენებულია", + "NotificationOptionPluginError": "დამატების შეცდომა", + "NotificationOptionPluginInstalled": "დამატება დაყენებულია", "NotificationOptionPluginUninstalled": "მოდული წაიშალა", - "ScheduledTaskFailedWithName": "{0} ვერ შესრულდა", + "ScheduledTaskFailedWithName": "{0} ჩავარდა", "TvShows": "სატელევიზიო სერიალები", "TaskRefreshPeople": "ხალხის განახლება", - "TaskUpdatePlugins": "მოდულების განახლება", + "TaskUpdatePlugins": "დამატებების განახლება", "TaskRefreshChannels": "არხების განახლება", "TaskOptimizeDatabase": "მონაცემთა ბაზის ოპტიმიზაცია", "TaskKeyframeExtractor": "საკვანძო კადრის გამომღები", "LabelIpAddressValue": "IP მისამართი: {0}", - "NameInstallFailed": "{0}-ის დაყენების შეცდომა", + "NameInstallFailed": "{0}-ის დაყენების ჩავარდა", "NotificationOptionApplicationUpdateAvailable": "ხელმისაწვდომია აპლიკაციის განახლება", "NotificationOptionAudioPlaybackStopped": "აუდიოს დაკვრა გაჩერებულია", "NotificationOptionNewLibraryContent": "ახალი შემცველობა დამატებულია", - "NotificationOptionPluginUpdateInstalled": "მოდულიs განახლება დაყენებულია", + "NotificationOptionPluginUpdateInstalled": "დამატების განახლება დაყენებულია", "NotificationOptionServerRestartRequired": "საჭიროა სერვერის გადატვირთვა", - "NotificationOptionTaskFailed": "გეგმიური დავალების შეცდომა", + "NotificationOptionTaskFailed": "დაგეგმილი ამოცანა ჩავარდა", "NotificationOptionUserLockedOut": "მომხმარებელი დაიბლოკა", "NotificationOptionVideoPlayback": "ვიდეოს დაკვრა დაწყებულია", "PluginInstalledWithName": "{0} დაყენებულია", "PluginUpdatedWithName": "{0} განახლდა", "TaskCleanActivityLog": "აქტივობების ჟურნალის გასუფთავება", - "TaskCleanCache": "ქეშის საქაღალდის გასუფთავება", - "TaskRefreshChapterImages": "თავის სურათების გაშლა", + "TaskCleanCache": "კეშის საქაღალდის გასუფთავება", + "TaskRefreshChapterImages": "თავის სურათების ამოღება", "TaskRefreshLibrary": "მედიის ბიბლიოთეკის სკანირება", "TaskCleanLogs": "ჟურნალის საქაღალდის გასუფთავება", "TaskCleanTranscode": "ტრანსკოდირების საქაღალდის გასუფთავება", - "TaskDownloadMissingSubtitles": "მიუწვდომელი სუბტიტრების გადმოწერა", - "UserDownloadingItemWithValues": "{0} -ი {1}-ს იწერს", + "TaskDownloadMissingSubtitles": "ნაკლული სუბტიტრების გადმოწერა", + "UserDownloadingItemWithValues": "{0} იწერს {1}-ს", "FailedLoginAttemptWithUserName": "შესვლის წარუმატებელი მცდელობა {0}-დან", "UserCreatedWithName": "მომხმარებელი {0} შეიქმნა", - "UserDeletedWithName": "მომხმარებელი {0} წაშლილია", - "UserOnlineFromDevice": "{0}-ი დაკავშირდა {1}-დან", - "UserOfflineFromDevice": "{0}-ი {1}-დან გაეთიშა", + "UserDeletedWithName": "მომხმარებელი {0} წაიშალა", + "UserOnlineFromDevice": "{0} ხაზზეა {1}-დან", + "UserOfflineFromDevice": "{0} გაითიშა {1}-დან", "UserLockedOutWithName": "მომხმარებელი {0} დაბლოკილია", - "UserStartedPlayingItemWithValues": "{0} უყურებს {1}-ს {2}-ზე", + "UserStartedPlayingItemWithValues": "{0} უკრავს {1}-ს {2}-ზე", "UserPasswordChangedWithName": "მომხმარებელი {0}-სთვის პაროლი შეიცვალა", "UserStoppedPlayingItemWithValues": "{0}-მა დაასრულა {1}-ის ყურება {2}-ზე", "TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.", @@ -96,15 +96,17 @@ "TaskDownloadMissingSubtitlesDescription": "ეძებს ბიბლიოთეკაში მიუწვდომელ სუბტიტრებს ინტერნეტში მეტამონაცემებზე დაყრდნობით.", "TaskOptimizeDatabaseDescription": "კუმშავს მონაცემთა ბაზას ადგილის გათავისუფლებლად. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.", "TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის დაშვებულ ბიბლიოთეკებში.", - "TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება", - "TaskAudioNormalization": "აუდიოს ნორმალიზება", + "TaskRefreshTrickplayImages": "Trickplay სურათების გენერაცია", + "TaskAudioNormalization": "აუდიოს ნორმალიზაცია", "TaskAudioNormalizationDescription": "აანალიზებს ფაილებს აუდიოს ნორმალიზაციისთვის.", "TaskDownloadMissingLyrics": "მიუწვდომელი ლირიკების ჩამოტვირთვა", - "TaskDownloadMissingLyricsDescription": "ჩამოტვირთავს ამჟამად ბიბლიოთეკაში არარსებულ ლირიკებს სიმღერებისთვის", - "TaskExtractMediaSegments": "მედია სეგმენტების სკანირება", + "TaskDownloadMissingLyricsDescription": "გადმოწერს ლირიკას სიმღერებისთვის", + "TaskExtractMediaSegments": "მედიის სეგმენტების სკანირება", "TaskExtractMediaSegmentsDescription": "მედია სეგმენტების სკანირება მხარდაჭერილი მოდულებისთვის.", - "TaskMoveTrickplayImages": "Trickplay სურათების მიგრაცია", + "TaskMoveTrickplayImages": "Trickplay-ის გამოსახულებების მდებარეობის მიგრაცია", "TaskMoveTrickplayImagesDescription": "გადააქვს trickplay ფაილები ბიბლიოთეკის პარამეტრებზე დაყრდნობით.", - "CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავება", - "CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ." + "CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავების ამოცანა", + "CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ.", + "LyricDownloadFailureFromForItem": "{1}-ისთვის {0}-დან ლირიკის გადმოწერა ჩავარდა", + "Original": "ორიგინალი" } diff --git a/Emby.Server.Implementations/Localization/Core/mi.json b/Emby.Server.Implementations/Localization/Core/mi.json index 000e82ebdf..74fae52dca 100644 --- a/Emby.Server.Implementations/Localization/Core/mi.json +++ b/Emby.Server.Implementations/Localization/Core/mi.json @@ -2,5 +2,14 @@ "AppDeviceValues": "Taupānga: {0}, Pūrere: {1}", "Artists": "Kaiwaiata", "AuthenticationSucceededWithUserName": "{0} has been successfully authenticated", - "Books": "Ngā pukapuka" + "Books": "Ngā pukapuka", + "Default": "Taunoa", + "Collections": "Kohinga", + "External": "Waho", + "Folders": "Kōpaki", + "Forced": "Kaha", + "Music": "Waiata", + "Movies": "Kiriata", + "Latest": "Hou", + "Inherit": "Riro" } diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json index c43a5a7431..dbf2ed4648 100644 --- a/Emby.Server.Implementations/Localization/Core/ml.json +++ b/Emby.Server.Implementations/Localization/Core/ml.json @@ -98,5 +98,10 @@ "TaskAudioNormalization": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുക", "TaskAudioNormalizationDescription": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുന്ന ഡാറ്റയ്ക്കായി ഫയലുകൾ സ്കാൻ ചെയ്യുക.", "TaskRefreshTrickplayImages": "ട്രിക്ക് പ്ലേ ചിത്രങ്ങൾ സൃഷ്ടിക്കുക", - "TaskRefreshTrickplayImagesDescription": "പ്രവർത്തനക്ഷമമാക്കിയ ലൈബ്രറികളിൽ വീഡിയോകൾക്കായി ട്രിക്ക്പ്ലേ പ്രിവ്യൂകൾ സൃഷ്ടിക്കുന്നു." + "TaskRefreshTrickplayImagesDescription": "പ്രവർത്തനക്ഷമമാക്കിയ ലൈബ്രറികളിൽ വീഡിയോകൾക്കായി ട്രിക്ക്പ്ലേ പ്രിവ്യൂകൾ സൃഷ്ടിക്കുന്നു.", + "Original": "ഓറിജിനൽ", + "TaskDownloadMissingLyrics": "ഇല്ലാത്ത വരികൾ ഡൗൺലോഡ് ചെയ്യുക", + "TaskDownloadMissingLyricsDescription": "പാട്ടുകളുടെ വരികൾ ഡൗൺലോഡ് ചെയ്യുന്നു", + "TaskExtractMediaSegments": "മീഡിയ സെഗ്മെന്റ് സ്കാൻ", + "TaskExtractMediaSegmentsDescription": "മീഡിയസെഗ്മെന്റ് പ്രാപ്തമാക്കിയ പ്ലഗിനുകളിൽ നിന്ന് മീഡിയ സെഗ്മെന്റുകൾ എക്സ്ട്രാക്റ്റുചെയ്യുന്നു അല്ലെങ്കിൽ നേടുന്നു." } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 898f5892c9..9aea3adc22 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -8,7 +8,7 @@ "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}", "Favorites": "Favorieten", "Folders": "Mappen", - "HeaderContinueWatching": "Verder kijken", + "HeaderContinueWatching": "Verderkijken", "HeaderFavoriteEpisodes": "Favoriete afleveringen", "HeaderFavoriteShows": "Favoriete series", "HeaderLiveTV": "Live-tv", diff --git a/Emby.Server.Implementations/Localization/Core/oc.json b/Emby.Server.Implementations/Localization/Core/oc.json index 0967ef424b..cad5640763 100644 --- a/Emby.Server.Implementations/Localization/Core/oc.json +++ b/Emby.Server.Implementations/Localization/Core/oc.json @@ -1 +1,3 @@ -{} +{ + "AppDeviceValues": "Aplicacion: {0}, Periferic: {1}" +} diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 15b1543d8e..ce338acf34 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay", "CleanupUserDataTask": "Task de limpeza de dados do usuário", "CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de mídias que não estão presentes há pelo menos 90 dias.", - "Original": "Original" + "Original": "Original", + "LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}" } diff --git a/Emby.Server.Implementations/Localization/Core/uz.json b/Emby.Server.Implementations/Localization/Core/uz.json index 998d799a95..3215733c1a 100644 --- a/Emby.Server.Implementations/Localization/Core/uz.json +++ b/Emby.Server.Implementations/Localization/Core/uz.json @@ -82,5 +82,7 @@ "TaskRefreshChapterImages": "Sahnadan tasvirini chiqarish", "TaskRefreshChapterImagesDescription": "Sahnalarni o'z ichiga olgan videolar uchun eskizlarni yaratadi.", "TaskRefreshLibrary": "Media kutubxonangizni skanerlash", - "TaskCleanLogsDescription": "{0} kundan eski log fayllarni o'chiradi." + "TaskCleanLogsDescription": "{0} kundan eski log fayllarni o'chiradi.", + "Original": "Original", + "LyricDownloadFailureFromForItem": "{0} dan {1} gacha qo'shiq matninin yuklab olishda xatolik ketdi" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 0b0b300d30..843e35afcc 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -318,13 +318,13 @@ namespace Emby.Server.Implementations.Localization // A lot of countries don't explicitly have a separate rating for adult content if (ratings.All(x => x.RatingScore?.Score != 1000)) { - ratings.Add(new ParentalRating("XXX", new(1000, null))); + ratings.Add(new ParentalRating("XXX", new(1000, null))); } // A lot of countries don't explicitly have a separate rating for banned content if (ratings.All(x => x.RatingScore?.Score != 1001)) { - ratings.Add(new ParentalRating("Banned", new(1001, null))); + ratings.Add(new ParentalRating("Banned", new(1001, null))); } return [.. ratings.OrderBy(r => r.RatingScore?.Score).ThenBy(r => r.RatingScore?.SubScore)]; diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 91ccb16ef9..f699c99d85 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -564,7 +564,8 @@ namespace Emby.Server.Implementations.Plugins Id = instance.Id, Status = PluginStatus.Active, Name = instance.Name, - Version = instance.Version.ToString() + Version = instance.Version.ToString(), + ImageResourceName = (instance as IHasEmbeddedImage)?.ImageResourceName }) { Instance = instance diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs index 51920c5b14..5e92808f78 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading; diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 2885b89e3a..18811ef3a9 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -386,7 +386,7 @@ namespace Emby.Server.Implementations.Session { if (session is null) { - return; + return; } if (string.IsNullOrEmpty(info.MediaSourceId)) @@ -453,18 +453,6 @@ namespace Emby.Server.Implementations.Session session.PlayState.RepeatMode = info.RepeatMode; session.PlayState.PlaybackOrder = info.PlaybackOrder; session.PlaylistItemId = info.PlaylistItemId; - - var nowPlayingQueue = info.NowPlayingQueue; - - if (nowPlayingQueue?.Length > 0 && !nowPlayingQueue.SequenceEqual(session.NowPlayingQueue)) - { - session.NowPlayingQueue = nowPlayingQueue; - - var itemIds = Array.ConvertAll(nowPlayingQueue, queue => queue.Id); - session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos( - _libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }), - new DtoOptions(true)); - } } /// <summary> @@ -1217,7 +1205,6 @@ namespace Emby.Server.Implementations.Session SupportsMediaControl = sessionInfo.SupportsMediaControl, SupportsRemoteControl = sessionInfo.SupportsRemoteControl, NowPlayingQueue = sessionInfo.NowPlayingQueue, - NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems, HasCustomDeviceName = sessionInfo.HasCustomDeviceName, PlaylistItemId = sessionInfo.PlaylistItemId, ServerId = sessionInfo.ServerId, diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs index d140426ddf..11a94648f8 100644 --- a/Emby.Server.Implementations/SystemManager.cs +++ b/Emby.Server.Implementations/SystemManager.cs @@ -89,11 +89,11 @@ public class SystemManager : ISystemManager .GetVirtualFolders() .Where(e => !string.IsNullOrWhiteSpace(e.ItemId)) // this should not be null but for some users it is. .Select(e => new LibraryStorageInfo() - { - Id = Guid.Parse(e.ItemId), - Name = e.Name, - Folders = e.Locations.Select(f => StorageHelper.GetFreeSpaceOf(f)).ToArray() - }); + { + Id = Guid.Parse(e.ItemId), + Name = e.Name, + Folders = e.Locations.Select(f => StorageHelper.GetFreeSpaceOf(f)).ToArray() + }); return new SystemStorageInfo() { diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 67b77a112d..ef53e3b326 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -527,42 +527,44 @@ namespace Emby.Server.Implementations.Updates using var response = await _httpClientFactory.CreateClient(NamedClient.Default) .GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - - // CA5351: Do Not Use Broken Cryptographic Algorithms + Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + // CA5351: Do Not Use Broken Cryptographic Algorithms #pragma warning disable CA5351 - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false)); - if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) - { - _logger.LogError( - "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}", - package.Name, - package.Checksum, - hash); - throw new InvalidDataException("The checksum of the received data doesn't match."); - } + var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false)); + if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogError( + "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}", + package.Name, + package.Checksum, + hash); + throw new InvalidDataException("The checksum of the received data doesn't match."); + } - // Version folder as they cannot be overwritten in Windows. - targetDir += "_" + package.Version; + // Version folder as they cannot be overwritten in Windows. + targetDir += "_" + package.Version; - if (Directory.Exists(targetDir)) - { - try + if (Directory.Exists(targetDir)) { - Directory.Delete(targetDir, true); - } + try + { + Directory.Delete(targetDir, true); + } #pragma warning disable CA1031 // Do not catch general exception types - catch + catch #pragma warning restore CA1031 // Do not catch general exception types - { - // Ignore any exceptions. + { + // Ignore any exceptions. + } } - } - stream.Position = 0; - await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true, cancellationToken); + stream.Position = 0; + await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true, cancellationToken).ConfigureAwait(false); + } // Ensure we create one or populate existing ones with missing data. await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); |
