aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs16
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs58
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs25
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/AudioSimilarItemsProvider.cs55
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/LiveTvProgramSimilarItemsProvider.cs94
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs91
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/MusicAlbumSimilarItemsProvider.cs55
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/MusicArtistSimilarItemsProvider.cs55
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/SeriesSimilarItemsProvider.cs54
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs406
-rw-r--r--Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/et.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/eu.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json3
21 files changed, 925 insertions, 22 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 3e98a5276c..c81829688f 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -14,6 +14,7 @@ using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Emby.Naming.Common;
+using Emby.Naming.Video;
using Emby.Photos;
using Emby.Server.Implementations.Chapters;
using Emby.Server.Implementations.Collections;
@@ -25,6 +26,7 @@ using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
+using Emby.Server.Implementations.Library.SimilarItems;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
@@ -92,7 +94,11 @@ using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager;
+using MediaBrowser.Providers.Plugins.ListenBrainz;
+using MediaBrowser.Providers.Plugins.ListenBrainz.Api;
using MediaBrowser.Providers.Plugins.Tmdb;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+using MediaBrowser.Providers.Plugins.Tmdb.TV;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
@@ -483,6 +489,11 @@ namespace Emby.Server.Implementations
serviceCollection.AddScoped<ISystemManager, SystemManager>();
serviceCollection.AddSingleton<TmdbClientManager>();
+ serviceCollection.AddSingleton<TmdbMovieSimilarProvider>();
+ serviceCollection.AddSingleton<TmdbSeriesSimilarProvider>();
+
+ serviceCollection.AddSingleton<ListenBrainzLabsClient>();
+ serviceCollection.AddSingleton<ListenBrainzSimilarArtistProvider>();
serviceCollection.AddSingleton(NetManager);
@@ -530,12 +541,15 @@ namespace Emby.Server.Implementations
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
serviceCollection.AddSingleton<NamingOptions>();
+ serviceCollection.AddSingleton<VideoListResolver>();
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
serviceCollection.AddSingleton<DotIgnoreIgnoreRule>();
+ serviceCollection.AddSingleton<ISimilarItemsManager, SimilarItemsManager>();
+
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
serviceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
@@ -693,6 +707,8 @@ namespace Emby.Server.Implementations
GetExports<IExternalUrlProvider>());
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
+
+ Resolve<ISimilarItemsManager>().AddParts(GetExports<ISimilarItemsProvider>());
}
/// <summary>
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 11f1496086..30ff1bd333 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -13,6 +13,7 @@ using System.Threading.Tasks;
using BitFaster.Caching.Lru;
using Emby.Naming.Common;
using Emby.Naming.TV;
+using Emby.Naming.Video;
using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
@@ -87,6 +88,7 @@ namespace Emby.Server.Implementations.Library
private readonly IPathManager _pathManager;
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
+ private readonly IMediaStreamRepository _mediaStreamRepository;
/// <summary>
/// The _root folder sync lock.
@@ -129,6 +131,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="peopleRepository">The people repository.</param>
/// <param name="pathManager">The path manager.</param>
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
+ /// <param name="mediaStreamRepository">The media stream repository.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@@ -151,7 +154,8 @@ namespace Emby.Server.Implementations.Library
IDirectoryService directoryService,
IPeopleRepository peopleRepository,
IPathManager pathManager,
- DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
+ DotIgnoreIgnoreRule dotIgnoreIgnoreRule,
+ IMediaStreamRepository mediaStreamRepository)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -181,6 +185,8 @@ namespace Emby.Server.Implementations.Library
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
+ _mediaStreamRepository = mediaStreamRepository;
+
RecordConfigurationValues(_configurationManager.Configuration);
}
@@ -787,6 +793,42 @@ namespace Emby.Server.Implementations.Library
CollectionType? collectionType = null)
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType);
+ private void SetAdditionalPartsFromStack(Video altVideo, string path)
+ {
+ if (altVideo.AdditionalParts is { Length: > 0 })
+ {
+ return;
+ }
+
+ var directory = Path.GetDirectoryName(path);
+ if (string.IsNullOrEmpty(directory))
+ {
+ return;
+ }
+
+ IEnumerable<FileSystemMetadata> siblings;
+ try
+ {
+ siblings = _fileSystem.GetFiles(directory);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to enumerate siblings to detect stack for {Path}", path);
+ return;
+ }
+
+ var stacks = StackResolver.Resolve(siblings, _namingOptions);
+ foreach (var stack in stacks)
+ {
+ if (stack.Files.Count > 1
+ && string.Equals(stack.Files[0], path, StringComparison.OrdinalIgnoreCase))
+ {
+ altVideo.AdditionalParts = stack.Files.Skip(1).ToArray();
+ return;
+ }
+ }
+ }
+
/// <inheritdoc />
public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? collectionType)
{
@@ -2307,6 +2349,10 @@ namespace Emby.Server.Implementations.Library
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
+ // ResolveAlternateVersion only sees the alternate's primary file.
+ // If the alternate is itself a stack (e.g. 1080p part1 + part2),
+ // detect its parts from sibling files so its AdditionalParts persist.
+ SetAdditionalPartsFromStack(altVideo, path);
allItems.Add(altVideo);
}
}
@@ -2510,6 +2556,10 @@ namespace Emby.Server.Implementations.Library
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
+ // ResolveAlternateVersion only sees the alternate's primary file.
+ // If the alternate is itself a stack (e.g. 1080p part1 + part2),
+ // detect its parts from sibling files so its AdditionalParts persist.
+ SetAdditionalPartsFromStack(altVideo, path);
allItems.Add(altVideo);
}
}
@@ -3800,5 +3850,11 @@ namespace Emby.Server.Implementations.Library
SetTopParentOrAncestorIds(query);
return _itemRepository.GetQueryFiltersLegacy(query);
}
+
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
+ {
+ return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType);
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 98e8f5350b..68b66ab7f5 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -28,15 +28,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{
private readonly IImageProcessor _imageProcessor;
+ private readonly VideoListResolver _videoListResolver;
- private static readonly CollectionType[] _validCollectionTypes = new[]
- {
+ private static readonly CollectionType[] _validCollectionTypes =
+ [
CollectionType.movies,
CollectionType.homevideos,
CollectionType.musicvideos,
CollectionType.tvshows,
CollectionType.photos
- };
+ ];
/// <summary>
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
@@ -45,10 +46,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
- public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
+ /// <param name="videoListResolver">The video list resolver.</param>
+ public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService, VideoListResolver videoListResolver)
: base(logger, namingOptions, directoryService)
{
_imageProcessor = imageProcessor;
+ _videoListResolver = videoListResolver;
}
/// <summary>
@@ -228,7 +231,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (collectionType == CollectionType.tvshows)
{
- return ResolveVideos<Episode>(parent, files, false, collectionType, true);
+ return ResolveVideos<Episode>(parent, files, true, collectionType, true);
}
return null;
@@ -274,7 +277,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
.Where(f => f is not null)
.ToList();
- var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
+ var resolverResult = _videoListResolver.Resolve(videoInfos, supportMultiEditions, parseName, parent.ContainingFolderPath, collectionType);
var result = new MultiItemResolverResult
{
@@ -302,7 +305,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
ProductionYear = video.Year,
Name = parseName ? video.Name : firstVideo.Name,
AdditionalParts = additionalParts,
- LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
+ LocalAlternateVersions = video.AlternateVersions.Select(av => av.Files[0].Path).ToArray()
};
SetVideoType(videoItem, firstVideo);
@@ -331,9 +334,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
for (var j = 0; j < current.AlternateVersions.Count; j++)
{
- if (ContainsFile(current.AlternateVersions[j], file))
+ var alternate = current.AlternateVersions[j];
+ for (var k = 0; k < alternate.Files.Count; k++)
{
- return true;
+ if (ContainsFile(alternate.Files[k], file))
+ {
+ return true;
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/AudioSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/AudioSimilarItemsProvider.cs
new file mode 100644
index 0000000000..1cc670b8ee
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/AudioSimilarItemsProvider.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for audio tracks.
+/// </summary>
+public class AudioSimilarItemsProvider : ILocalSimilarItemsProvider<Audio>
+{
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioSimilarItemsProvider"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public AudioSimilarItemsProvider(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Audio item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ var internalQuery = new InternalItemsQuery(query.User)
+ {
+ Genres = item.Genres,
+ Tags = item.Tags,
+ Limit = query.Limit,
+ DtoOptions = query.DtoOptions ?? new DtoOptions(),
+ ExcludeItemIds = [.. query.ExcludeItemIds],
+ ExcludeArtistIds = [.. query.ExcludeArtistIds],
+ IncludeItemTypes = [BaseItemKind.Audio],
+ EnableGroupByMetadataKey = false,
+ EnableTotalRecordCount = true,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
+ };
+
+ return Task.FromResult(_libraryManager.GetItemList(internalQuery));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/LiveTvProgramSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/LiveTvProgramSimilarItemsProvider.cs
new file mode 100644
index 0000000000..7665ee2f79
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/LiveTvProgramSimilarItemsProvider.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for Live TV programs.
+/// </summary>
+public class LiveTvProgramSimilarItemsProvider : ILocalSimilarItemsProvider<LiveTvProgram>
+{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LiveTvProgramSimilarItemsProvider"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="serverConfigurationManager">The server configuration manager.</param>
+ public LiveTvProgramSimilarItemsProvider(
+ ILibraryManager libraryManager,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _libraryManager = libraryManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(LiveTvProgram item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ BaseItemKind[] includeItemTypes;
+ bool enableGroupByMetadataKey;
+ bool enableTotalRecordCount;
+
+ if (item.IsMovie)
+ {
+ // Movie-like program
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
+
+ includeItemTypes = [.. itemTypes];
+ enableGroupByMetadataKey = true;
+ enableTotalRecordCount = false;
+ }
+ else if (item.IsSeries)
+ {
+ // Series-like program
+ includeItemTypes = [BaseItemKind.Series];
+ enableGroupByMetadataKey = false;
+ enableTotalRecordCount = true;
+ }
+ else
+ {
+ // Default - match same type
+ includeItemTypes = [item.GetBaseItemKind()];
+ enableGroupByMetadataKey = false;
+ enableTotalRecordCount = true;
+ }
+
+ var internalQuery = new InternalItemsQuery(query.User)
+ {
+ Genres = item.Genres,
+ Tags = item.Tags,
+ Limit = query.Limit,
+ DtoOptions = query.DtoOptions ?? new DtoOptions(),
+ ExcludeItemIds = [.. query.ExcludeItemIds],
+ IncludeItemTypes = includeItemTypes,
+ EnableGroupByMetadataKey = enableGroupByMetadataKey,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
+ };
+
+ return Task.FromResult(_libraryManager.GetItemList(internalQuery));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
new file mode 100644
index 0000000000..93aa0574c0
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for movies and trailers.
+/// </summary>
+public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie>, ILocalSimilarItemsProvider<Trailer>
+{
+ private readonly ILibraryManager _libraryManager;
+ 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="serverConfigurationManager">The server configuration manager.</param>
+ public MovieSimilarItemsProvider(
+ ILibraryManager libraryManager,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _libraryManager = libraryManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Movie item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(GetSimilarMovieItems(item, query));
+ }
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Trailer item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(GetSimilarMovieItems(item, query));
+ }
+
+ bool ILocalSimilarItemsProvider.Supports(Type itemType)
+ => typeof(Movie).IsAssignableFrom(itemType) || typeof(Trailer).IsAssignableFrom(itemType);
+
+ Task<IReadOnlyList<BaseItem>> ILocalSimilarItemsProvider.GetSimilarItemsAsync(BaseItem item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ => item switch
+ {
+ Movie movie => GetSimilarItemsAsync(movie, query, cancellationToken),
+ Trailer trailer => GetSimilarItemsAsync(trailer, query, cancellationToken),
+ _ => throw new ArgumentException($"Unsupported item type {item.GetType()}", nameof(item))
+ };
+
+ private IReadOnlyList<BaseItem> GetSimilarMovieItems(BaseItem item, SimilarItemsQuery query)
+ {
+ 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)
+ {
+ 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)]
+ };
+
+ return _libraryManager.GetItemList(internalQuery);
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/MusicAlbumSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/MusicAlbumSimilarItemsProvider.cs
new file mode 100644
index 0000000000..c13045deda
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/MusicAlbumSimilarItemsProvider.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for music albums.
+/// </summary>
+public class MusicAlbumSimilarItemsProvider : ILocalSimilarItemsProvider<MusicAlbum>
+{
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicAlbumSimilarItemsProvider"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public MusicAlbumSimilarItemsProvider(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(MusicAlbum item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ var internalQuery = new InternalItemsQuery(query.User)
+ {
+ Genres = item.Genres,
+ Tags = item.Tags,
+ Limit = query.Limit,
+ DtoOptions = query.DtoOptions ?? new DtoOptions(),
+ ExcludeItemIds = [.. query.ExcludeItemIds],
+ ExcludeArtistIds = [.. query.ExcludeArtistIds],
+ IncludeItemTypes = [BaseItemKind.MusicAlbum],
+ EnableGroupByMetadataKey = false,
+ EnableTotalRecordCount = true,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
+ };
+
+ return Task.FromResult(_libraryManager.GetItemList(internalQuery));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/MusicArtistSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/MusicArtistSimilarItemsProvider.cs
new file mode 100644
index 0000000000..3331419442
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/MusicArtistSimilarItemsProvider.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for music artists.
+/// </summary>
+public class MusicArtistSimilarItemsProvider : ILocalSimilarItemsProvider<MusicArtist>
+{
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicArtistSimilarItemsProvider"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public MusicArtistSimilarItemsProvider(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(MusicArtist item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ var internalQuery = new InternalItemsQuery(query.User)
+ {
+ Genres = item.Genres,
+ Tags = item.Tags,
+ Limit = query.Limit,
+ DtoOptions = query.DtoOptions ?? new DtoOptions(),
+ ExcludeItemIds = [.. query.ExcludeItemIds],
+ ExcludeArtistIds = [.. query.ExcludeArtistIds],
+ IncludeItemTypes = [BaseItemKind.MusicArtist],
+ EnableGroupByMetadataKey = false,
+ EnableTotalRecordCount = true,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
+ };
+
+ return Task.FromResult(_libraryManager.GetItemList(internalQuery));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/SeriesSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/SeriesSimilarItemsProvider.cs
new file mode 100644
index 0000000000..0366fb752e
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/SeriesSimilarItemsProvider.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for TV series.
+/// </summary>
+public class SeriesSimilarItemsProvider : ILocalSimilarItemsProvider<Series>
+{
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeriesSimilarItemsProvider"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public SeriesSimilarItemsProvider(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Series item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ var internalQuery = new InternalItemsQuery(query.User)
+ {
+ Genres = item.Genres,
+ Tags = item.Tags,
+ Limit = query.Limit,
+ DtoOptions = query.DtoOptions ?? new DtoOptions(),
+ ExcludeItemIds = [.. query.ExcludeItemIds],
+ IncludeItemTypes = [BaseItemKind.Series],
+ EnableGroupByMetadataKey = false,
+ EnableTotalRecordCount = true,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
+ };
+
+ return Task.FromResult(_libraryManager.GetItemList(internalQuery));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
new file mode 100644
index 0000000000..b56779cf3f
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
@@ -0,0 +1,406 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Manages similar items providers and orchestrates similar items operations.
+/// </summary>
+public class SimilarItemsManager : ISimilarItemsManager
+{
+ private readonly ILogger<SimilarItemsManager> _logger;
+ private readonly IServerApplicationPaths _appPaths;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IFileSystem _fileSystem;
+ private ISimilarItemsProvider[] _similarItemsProviders = [];
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SimilarItemsManager"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="appPaths">The server application paths.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="fileSystem">The file system.</param>
+ public SimilarItemsManager(
+ ILogger<SimilarItemsManager> logger,
+ IServerApplicationPaths appPaths,
+ ILibraryManager libraryManager,
+ IFileSystem fileSystem)
+ {
+ _logger = logger;
+ _appPaths = appPaths;
+ _libraryManager = libraryManager;
+ _fileSystem = fileSystem;
+ }
+
+ /// <inheritdoc/>
+ public void AddParts(IEnumerable<ISimilarItemsProvider> providers)
+ {
+ _similarItemsProviders = providers.ToArray();
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<ISimilarItemsProvider> GetSimilarItemsProviders<T>()
+ where T : BaseItem
+ {
+ var itemType = typeof(T);
+ return _similarItemsProviders
+ .Where(p => (p is ILocalSimilarItemsProvider local && local.Supports(itemType))
+ || (p is IRemoteSimilarItemsProvider remote && remote.Supports(itemType)))
+ .ToList();
+ }
+
+ /// <inheritdoc/>
+ public async Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(
+ BaseItem item,
+ IReadOnlyList<Guid> excludeArtistIds,
+ User? user,
+ DtoOptions dtoOptions,
+ int? limit,
+ LibraryOptions? libraryOptions,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(item);
+ ArgumentNullException.ThrowIfNull(excludeArtistIds);
+
+ var itemType = item.GetType();
+ var requestedLimit = limit ?? 50;
+ var itemKind = item.GetBaseItemKind();
+
+ // Ensure ProviderIds is included in DtoOptions for matching remote provider responses
+ if (!dtoOptions.Fields.Contains(ItemFields.ProviderIds))
+ {
+ dtoOptions.Fields = dtoOptions.Fields.Concat([ItemFields.ProviderIds]).ToArray();
+ }
+
+ // Local providers are always enabled. Remote providers must be explicitly enabled.
+ var localProviders = _similarItemsProviders
+ .OfType<ILocalSimilarItemsProvider>()
+ .Where(p => p.Supports(itemType))
+ .ToList();
+ var remoteProviders = _similarItemsProviders
+ .OfType<IRemoteSimilarItemsProvider>()
+ .Where(p => p.Supports(itemType));
+ var matchingProviders = new List<ISimilarItemsProvider>(localProviders);
+
+ var typeOptions = libraryOptions?.GetTypeOptions(itemType.Name);
+ if (typeOptions?.SimilarItemProviders?.Length > 0)
+ {
+ matchingProviders.AddRange(remoteProviders
+ .Where(p => typeOptions.SimilarItemProviders.Contains(p.Name, StringComparer.OrdinalIgnoreCase)));
+ }
+
+ var orderConfig = typeOptions?.SimilarItemProviderOrder is { Length: > 0 } order
+ ? order
+ : typeOptions?.SimilarItemProviders;
+ var orderedProviders = matchingProviders
+ .OrderBy(p => GetConfiguredSimilarProviderOrder(orderConfig, p.Name))
+ .ToList();
+
+ var allResults = new List<(BaseItem Item, float Score)>();
+ var excludeIds = new HashSet<Guid> { item.Id };
+ foreach (var (providerOrder, provider) in orderedProviders.Index())
+ {
+ if (allResults.Count >= requestedLimit || cancellationToken.IsCancellationRequested)
+ {
+ break;
+ }
+
+ try
+ {
+ if (provider is ILocalSimilarItemsProvider localProvider)
+ {
+ var query = new SimilarItemsQuery
+ {
+ User = user,
+ Limit = requestedLimit - allResults.Count,
+ DtoOptions = dtoOptions,
+ ExcludeItemIds = [.. excludeIds],
+ ExcludeArtistIds = excludeArtistIds
+ };
+
+ var items = await localProvider.GetSimilarItemsAsync(item, query, cancellationToken).ConfigureAwait(false);
+
+ foreach (var (position, resultItem) in items.Index())
+ {
+ if (excludeIds.Add(resultItem.Id))
+ {
+ var score = CalculateScore(null, providerOrder, position);
+ allResults.Add((resultItem, score));
+ }
+ }
+ }
+ else if (provider is IRemoteSimilarItemsProvider remoteProvider)
+ {
+ var cachePath = GetSimilarItemsCachePath(provider.Name, itemType.Name, item.Id);
+
+ var cachedReferences = await TryReadSimilarItemsCacheAsync(cachePath, cancellationToken).ConfigureAwait(false);
+ if (cachedReferences is not null)
+ {
+ var resolvedItems = ResolveRemoteReferences(cachedReferences, providerOrder, user, dtoOptions, itemKind, excludeIds);
+ allResults.AddRange(resolvedItems);
+ continue;
+ }
+
+ var query = new SimilarItemsQuery
+ {
+ User = user,
+ Limit = requestedLimit - allResults.Count,
+ DtoOptions = dtoOptions,
+ ExcludeItemIds = [.. excludeIds],
+ ExcludeArtistIds = excludeArtistIds
+ };
+
+ // Collect references in batches and resolve against local library.
+ // Stop fetching once we have enough resolved local items.
+ const int BatchSize = 20;
+ var remaining = requestedLimit - allResults.Count;
+ var collectedReferences = new List<SimilarItemReference>();
+ var pendingBatch = new List<SimilarItemReference>();
+
+ await foreach (var reference in remoteProvider.GetSimilarItemsAsync(item, query, cancellationToken).ConfigureAwait(false))
+ {
+ collectedReferences.Add(reference);
+ pendingBatch.Add(reference);
+
+ if (pendingBatch.Count >= BatchSize)
+ {
+ var resolvedItems = ResolveRemoteReferences(pendingBatch, providerOrder, user, dtoOptions, itemKind, excludeIds);
+ allResults.AddRange(resolvedItems);
+ remaining -= resolvedItems.Count;
+ pendingBatch.Clear();
+
+ if (remaining <= 0)
+ {
+ break;
+ }
+ }
+ }
+
+ // Resolve any remaining references in the last partial batch
+ if (pendingBatch.Count > 0)
+ {
+ var resolvedItems = ResolveRemoteReferences(pendingBatch, providerOrder, user, dtoOptions, itemKind, excludeIds);
+ allResults.AddRange(resolvedItems);
+ }
+
+ if (collectedReferences.Count > 0 && provider.CacheDuration is not null)
+ {
+ await SaveSimilarItemsCacheAsync(cachePath, collectedReferences, provider.CacheDuration.Value, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Similar items provider {ProviderName} failed for item {ItemId}", provider.Name, item.Id);
+ }
+ }
+
+ return allResults
+ .OrderByDescending(x => x.Score)
+ .Select(x => x.Item)
+ .Take(requestedLimit)
+ .ToList();
+ }
+
+ private List<(BaseItem Item, float Score)> ResolveRemoteReferences(
+ IReadOnlyList<SimilarItemReference> references,
+ int providerOrder,
+ User? user,
+ DtoOptions dtoOptions,
+ BaseItemKind itemKind,
+ HashSet<Guid> excludeIds)
+ {
+ if (references.Count == 0)
+ {
+ return [];
+ }
+
+ var resolvedById = new Dictionary<Guid, (BaseItem Item, float Score)>();
+ var providerLookup = new Dictionary<(string ProviderName, string ProviderId), (float? Score, int Position)>(StringTupleComparer.Instance);
+
+ foreach (var (position, match) in references.Index())
+ {
+ var lookupKey = (match.ProviderName, match.ProviderId);
+ if (!providerLookup.TryGetValue(lookupKey, out var existing))
+ {
+ providerLookup[lookupKey] = (match.Score, position);
+ }
+ else if (match.Score > existing.Score || (match.Score == existing.Score && position < existing.Position))
+ {
+ providerLookup[lookupKey] = (match.Score, position);
+ }
+ }
+
+ var allProviderIds = providerLookup
+ .GroupBy(kvp => kvp.Key.ProviderName)
+ .ToDictionary(g => g.Key, g => g.Select(x => x.Key.ProviderId).ToArray());
+
+ var query = new InternalItemsQuery(user)
+ {
+ HasAnyProviderIds = allProviderIds,
+ IncludeItemTypes = [itemKind],
+ DtoOptions = dtoOptions
+ };
+
+ var items = _libraryManager.GetItemList(query);
+
+ foreach (var item in items)
+ {
+ if (excludeIds.Contains(item.Id) || resolvedById.ContainsKey(item.Id))
+ {
+ continue;
+ }
+
+ foreach (var providerName in allProviderIds.Keys)
+ {
+ 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)
+ {
+ excludeIds.Add(item.Id);
+ resolvedById[item.Id] = (item, score);
+ }
+
+ break;
+ }
+ }
+ }
+
+ return [.. resolvedById.Values];
+ }
+
+ private static float CalculateScore(float? matchScore, int providerOrder, int position)
+ {
+ // Use provider-supplied score if available, otherwise derive from position
+ var baseScore = matchScore ?? (1.0f - (position * 0.02f));
+
+ // Apply small boost based on provider order (higher priority providers get small bonus)
+ var priorityBoost = Math.Max(0, 10 - providerOrder) * 0.005f;
+
+ return Math.Clamp(baseScore + priorityBoost, 0f, 1f);
+ }
+
+ private static int GetConfiguredSimilarProviderOrder(string[]? orderConfig, string providerName)
+ {
+ if (orderConfig is null || orderConfig.Length == 0)
+ {
+ return int.MaxValue;
+ }
+
+ var index = Array.FindIndex(orderConfig, name => string.Equals(name, providerName, StringComparison.OrdinalIgnoreCase));
+ return index >= 0 ? index : int.MaxValue;
+ }
+
+ private string GetSimilarItemsCachePath(string providerName, string baseItemType, Guid itemId)
+ {
+ var dataPath = Path.Combine(
+ _appPaths.CachePath,
+ $"{providerName.ToLowerInvariant()}-similar-{baseItemType.ToLowerInvariant()}");
+ return Path.Combine(dataPath, $"{itemId.ToString("N", CultureInfo.InvariantCulture)}.json");
+ }
+
+ private async Task<List<SimilarItemReference>?> TryReadSimilarItemsCacheAsync(string cachePath, CancellationToken cancellationToken)
+ {
+ var fileInfo = _fileSystem.GetFileSystemInfo(cachePath);
+ if (!fileInfo.Exists || fileInfo.Length == 0)
+ {
+ return null;
+ }
+
+ try
+ {
+ var stream = File.OpenRead(cachePath);
+ await using (stream.ConfigureAwait(false))
+ {
+ var cache = await JsonSerializer.DeserializeAsync<SimilarItemsCache>(stream, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
+ if (cache?.References is not null && DateTime.UtcNow < cache.ExpiresAt)
+ {
+ return cache.References;
+ }
+ }
+ }
+ catch (IOException ex)
+ {
+ _logger.LogWarning(ex, "Failed to read similar items cache from {CachePath}", cachePath);
+ }
+ catch (JsonException ex)
+ {
+ _logger.LogWarning(ex, "Failed to parse similar items cache from {CachePath}", cachePath);
+ }
+
+ return null;
+ }
+
+ private async Task SaveSimilarItemsCacheAsync(string cachePath, List<SimilarItemReference> references, TimeSpan cacheDuration, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var directory = Path.GetDirectoryName(cachePath);
+ if (!string.IsNullOrEmpty(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ var cache = new SimilarItemsCache
+ {
+ References = references,
+ ExpiresAt = DateTime.UtcNow.Add(cacheDuration)
+ };
+
+ var stream = File.Create(cachePath);
+ await using (stream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(stream, cache, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (IOException ex)
+ {
+ _logger.LogWarning(ex, "Failed to save similar items cache to {CachePath}", cachePath);
+ }
+ }
+
+ private sealed class SimilarItemsCache
+ {
+ public List<SimilarItemReference>? References { get; set; }
+
+ public DateTime ExpiresAt { get; set; }
+ }
+
+ private sealed class StringTupleComparer : IEqualityComparer<(string Key, string Value)>
+ {
+ public static readonly StringTupleComparer Instance = new();
+
+ public bool Equals((string Key, string Value) x, (string Key, string Value) y)
+ => string.Equals(x.Key, y.Key, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(x.Value, y.Value, StringComparison.OrdinalIgnoreCase);
+
+ public int GetHashCode((string Key, string Value) obj)
+ => HashCode.Combine(
+ StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Key),
+ StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Value));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
index 71ce3b6012..7c605036cf 100644
--- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
@@ -80,7 +80,7 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
ImageTypes = [imageType],
Limit = 30,
// TODO max parental rating configurable
- MaxParentalRating = new(10, null),
+ MaxParentalRating = new(13, null),
OrderBy =
[
(ItemSortBy.Random, SortOrder.Ascending)
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 5ba3c7e575..8ac5fdf6fc 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -107,5 +107,6 @@
"TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.",
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind.",
- "Original": "Original"
+ "Original": "Original",
+ "LyricDownloadFailureFromForItem": "Fehler beim Download der Songtexte von {0} für {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json
index b530f19fa9..bc089d836c 100644
--- a/Emby.Server.Implementations/Localization/Core/et.json
+++ b/Emby.Server.Implementations/Localization/Core/et.json
@@ -106,5 +106,6 @@
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meedialõigud MediaSegment'i toega pluginatest.",
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
"CleanupUserDataTask": "Puhasta kasutajaandmed",
- "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud."
+ "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud.",
+ "LyricDownloadFailureFromForItem": "Laulusõnade hankimine teenusest {0} loole {1} nurjus"
}
diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json
index 8375c96b82..71c351adcd 100644
--- a/Emby.Server.Implementations/Localization/Core/eu.json
+++ b/Emby.Server.Implementations/Localization/Core/eu.json
@@ -106,5 +106,7 @@
"TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
"TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu.",
"CleanupUserDataTaskDescription": "Gutxienez 90 egunez dagoeneko existitzen ez den multimediatik erabiltzaile-datu guztiak (ikusteko egoera, gogokoen egoera, etab.) garbitzen ditu.",
- "CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina"
+ "CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina",
+ "LyricDownloadFailureFromForItem": "Ezin izan dira {1}-ren letrak deskargatu {0}-tik",
+ "Original": "Jatorrizkoa"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 342f18012a..e05cce47b0 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -106,5 +106,7 @@
"TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
- "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
+ "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
+ "LyricDownloadFailureFromForItem": "Le téléchargement des paroles a échoué de {0} pour {1}",
+ "Original": "Original"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 272ed77485..ceba1dcb41 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -106,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
- "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
+ "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
+ "LyricDownloadFailureFromForItem": "Le téléchargement des paroles à échoué de {0} pour {1}",
+ "Original": "Original"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 363ae502bd..898f5892c9 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": "Verderkijken",
+ "HeaderContinueWatching": "Verder kijken",
"HeaderFavoriteEpisodes": "Favoriete afleveringen",
"HeaderFavoriteShows": "Favoriete series",
"HeaderLiveTV": "Live-tv",
@@ -107,5 +107,6 @@
"CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.",
"CleanupUserDataTask": "Opruimtaak gebruikersdata",
"Genres": "Genres",
- "Original": "Oorspronkelijk"
+ "Original": "Oorspronkelijk",
+ "LyricDownloadFailureFromForItem": "Downloaden van liedteksten voor {1} van {0} mislukt"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index f447dc9457..c4657bdd6e 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -107,5 +107,6 @@
"TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki.",
"CleanupUserDataTaskDescription": "Usuwa wszystkie dane użytkownika (stan oglądanych, status ulubionych itp.) z mediów, które nie są dostępne od co najmniej 90 dni.",
"CleanupUserDataTask": "Zadanie czyszczenia danych użytkownika",
- "Original": "Oryginalny"
+ "Original": "Oryginalny",
+ "LyricDownloadFailureFromForItem": "Błąd podczas pobierania tekstu piosenki z {0} dla {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index b623055ddf..0c42d4a55f 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -106,5 +106,7 @@
"TaskDownloadMissingLyricsDescription": "Şarkı sözlerini indirir",
"TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır.",
"CleanupUserDataTask": "Kullanıcı verisi temizleme görevi",
- "CleanupUserDataTaskDescription": "En az 90 gün boyunca artık mevcut olmayan medyadaki tüm kullanıcı verilerini (İzleme durumu, favori durumu vb.) temizler."
+ "CleanupUserDataTaskDescription": "En az 90 gün boyunca artık mevcut olmayan medyadaki tüm kullanıcı verilerini (İzleme durumu, favori durumu vb.) temizler.",
+ "LyricDownloadFailureFromForItem": "{1} şarkı sözleri {0} adresinden indirilemedi",
+ "Original": "Orijinal"
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 3b989806e7..ccb9d915d1 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -107,5 +107,6 @@
"TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.",
"CleanupUserDataTask": "Завдання очищення даних користувача",
"CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому.",
- "Original": "Оригінал"
+ "Original": "Оригінал",
+ "LyricDownloadFailureFromForItem": "Не вдалося завантажити текст пісні з {0} для {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 2c09942102..2ba665e2ff 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -107,5 +107,6 @@
"TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện",
"CleanupUserDataTask": "Tác vụ dọn dẹp dữ liệu người dùng",
"CleanupUserDataTaskDescription": "Làm sạch tất cả dữ liệu người dùng (trạng thái xem, trạng thái yêu thích, v.v.) từ phương tiện không còn có mặt trong ít nhất 90 ngày.",
- "Original": "Gốc"
+ "Original": "Gốc",
+ "LyricDownloadFailureFromForItem": "Lời bài hát không tải xuống được từ {0} cho {1}"
}