aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Controllers
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-05-16 09:57:06 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-05-16 09:57:06 +0200
commitea8f6c51fd1013b6c841fb74a7fcde532d57d8da (patch)
tree53ccd2afeda449f22e4198dfcf3b78993938a654 /Jellyfin.Api/Controllers
parentd71194aa8cb07d998c0ed15df964c7c1259e7f17 (diff)
parentc4a21cb3224b284ecde65cf978a12de82d9f910c (diff)
Merge remote-tracking branch 'upstream/master' into search-rebased
Diffstat (limited to 'Jellyfin.Api/Controllers')
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs37
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs35
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs82
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs6
5 files changed, 112 insertions, 51 deletions
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index f97ab414ce..f19ca77818 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -87,6 +87,7 @@ public class ArtistsController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the artists.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use GetPersons")]
public ActionResult<QueryResult<BaseItemDto>> GetArtists(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
@@ -258,6 +259,7 @@ public class ArtistsController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the album artists.</returns>
[HttpGet("AlbumArtists")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use GetPersons")]
public ActionResult<QueryResult<BaseItemDto>> GetAlbumArtists(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
@@ -399,6 +401,7 @@ public class ArtistsController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the artist.</returns>
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use GetPerson")]
public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 2f53784db1..cfc8be28ae 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -8,6 +8,8 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -24,16 +26,19 @@ public class FilterController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
+ private readonly ILocalizationManager _localization;
/// <summary>
/// Initializes a new instance of the <see cref="FilterController"/> class.
/// </summary>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public FilterController(ILibraryManager libraryManager, IUserManager userManager)
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public FilterController(ILibraryManager libraryManager, IUserManager userManager, ILocalizationManager localization)
{
_libraryManager = libraryManager;
_userManager = userManager;
+ _localization = localization;
}
/// <summary>
@@ -183,6 +188,36 @@ public class FilterController : BaseJellyfinApiController
}).ToArray();
}
+ if (includeItemTypes.Contains(BaseItemKind.Movie) || includeItemTypes.Contains(BaseItemKind.Series))
+ {
+ filters.AudioLanguages = _libraryManager
+ .GetMediaStreamLanguages(MediaStreamType.Audio)
+ .Select(language =>
+ {
+ var culture = _localization.FindLanguageInfo(language);
+ return new NameValuePair
+ {
+ Name = culture is null ? language : $"{culture.DisplayName} ({language})",
+ Value = language
+ };
+ })
+ .OrderBy(l => l.Name)
+ .ToArray();
+ filters.SubtitleLanguages = _libraryManager
+ .GetMediaStreamLanguages(MediaStreamType.Subtitle)
+ .Select(language =>
+ {
+ var culture = _localization.FindLanguageInfo(language);
+ return new NameValuePair
+ {
+ Name = culture is null ? language : $"{culture.DisplayName} ({language})",
+ Value = language
+ };
+ })
+ .OrderBy(l => l.Name)
+ .ToArray();
+ }
+
return filters;
}
}
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index e36a128192..234282f7bd 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -162,6 +162,8 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+ /// <param name="audioLanguages">Optional. If specified, results will be filtered based on audio language. This allows multiple, comma delimited values.</param>
+ /// <param name="subtitleLanguages">Optional. If specified, results will be filtered based on subtitle language. This allows multiple, comma delimited values.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
@@ -252,6 +254,8 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] audioLanguages,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] subtitleLanguages,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
@@ -432,6 +436,8 @@ public class ItemsController : BaseJellyfinApiController
MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
MinPremiereDate = minPremiereDate?.ToUniversalTime(),
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
+ AudioLanguages = audioLanguages,
+ SubtitleLanguages = subtitleLanguages,
};
if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
@@ -439,6 +445,33 @@ public class ItemsController : BaseJellyfinApiController
query.CollapseBoxSetItems = false;
}
+ if (query.SubtitleLanguages.Count > 0 && query.HasSubtitles.HasValue)
+ {
+ if (query.HasSubtitles.Value)
+ {
+ // if we check for specific subtitles we don't need a separate check for subtitle existence
+ query.HasSubtitles = null;
+ }
+ else
+ {
+ // if we search for items without subtitles, we don't need to check for subtitles of a specific language
+ query.SubtitleLanguages = [];
+ }
+ }
+
+ // for filter values that rely on media streams, we need to include alternative and linked versions
+ if (query.HasSubtitles.HasValue
+ || query.SubtitleLanguages.Count > 0
+ || query.AudioLanguages.Count > 0
+ || query.Is3D.HasValue
+ || query.IsHD.HasValue
+ || query.Is4K.HasValue
+ || query.VideoTypes.Length > 0
+ )
+ {
+ query.IncludeOwnedItems = true;
+ }
+
query.ApplyFilters(filters);
// Filter by Series Status
@@ -843,6 +876,8 @@ public class ItemsController : BaseJellyfinApiController
nameLessThan,
studioIds,
genreIds,
+ [],
+ [],
enableTotalRecordCount,
enableImages).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 69c17f2486..abf27b7702 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -47,6 +47,7 @@ namespace Jellyfin.Api.Controllers;
public class LibraryController : BaseJellyfinApiController
{
private readonly IProviderManager _providerManager;
+ private readonly ISimilarItemsManager _similarItemsManager;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDtoService _dtoService;
@@ -60,6 +61,7 @@ public class LibraryController : BaseJellyfinApiController
/// Initializes a new instance of the <see cref="LibraryController"/> class.
/// </summary>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="similarItemsManager">Instance of the <see cref="ISimilarItemsManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
@@ -70,6 +72,7 @@ public class LibraryController : BaseJellyfinApiController
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public LibraryController(
IProviderManager providerManager,
+ ISimilarItemsManager similarItemsManager,
ILibraryManager libraryManager,
IUserManager userManager,
IDtoService dtoService,
@@ -80,6 +83,7 @@ public class LibraryController : BaseJellyfinApiController
IServerConfigurationManager serverConfigurationManager)
{
_providerManager = providerManager;
+ _similarItemsManager = similarItemsManager;
_libraryManager = libraryManager;
_userManager = userManager;
_dtoService = dtoService;
@@ -708,6 +712,7 @@ public class LibraryController : BaseJellyfinApiController
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <response code="200">Similar items returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
[HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
@@ -718,12 +723,13 @@ public class LibraryController : BaseJellyfinApiController
[HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetSimilarItems(
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ CancellationToken cancellationToken)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -746,57 +752,22 @@ public class LibraryController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields };
- var program = item as IHasProgramAttributes;
- bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer;
- bool? isSeries = item is Series || (program is not null && program.IsSeries);
+ // Get library options for provider configuration
+ var libraryOptions = _libraryManager.GetLibraryOptions(item);
- var includeItemTypes = new List<BaseItemKind>();
- if (isMovie.Value)
- {
- includeItemTypes.Add(BaseItemKind.Movie);
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- includeItemTypes.Add(BaseItemKind.Trailer);
- includeItemTypes.Add(BaseItemKind.LiveTvProgram);
- }
- }
- else if (isSeries.Value)
- {
- includeItemTypes.Add(BaseItemKind.Series);
- }
- else
- {
- // For non series and movie types these columns are typically null
- // isSeries = null;
- isMovie = null;
- includeItemTypes.Add(item.GetBaseItemKind());
- }
-
- var query = new InternalItemsQuery(user)
- {
- Genres = item.Genres,
- Tags = item.Tags,
- Limit = limit,
- IncludeItemTypes = includeItemTypes.ToArray(),
- DtoOptions = dtoOptions,
- EnableTotalRecordCount = !isMovie ?? true,
- EnableGroupByMetadataKey = isMovie ?? false,
- ExcludeItemIds = [itemId],
- OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
- };
-
- // ExcludeArtistIds
- if (excludeArtistIds.Length != 0)
- {
- query.ExcludeArtistIds = excludeArtistIds;
- }
-
- var itemsResult = _libraryManager.GetItemList(query);
+ var itemsResult = await _similarItemsManager.GetSimilarItemsAsync(
+ item,
+ excludeArtistIds,
+ user,
+ dtoOptions,
+ limit,
+ libraryOptions,
+ cancellationToken).ConfigureAwait(false);
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
return new QueryResult<BaseItemDto>(
- query.StartIndex,
+ 0,
itemsResult.Count,
returnList);
}
@@ -907,6 +878,17 @@ public class LibraryController : BaseJellyfinApiController
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray(),
+ SimilarItemProviders = plugins
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalSimilarityProvider || p.Type == MetadataPluginType.SimilarityProvider))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = i.Type == MetadataPluginType.LocalSimilarityProvider
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray(),
+
SupportedImageTypes = plugins
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
@@ -935,11 +917,11 @@ public class LibraryController : BaseJellyfinApiController
try
{
await _activityManager.CreateAsync(new ActivityLog(
- string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
+ string.Format(CultureInfo.InvariantCulture, _localization.GetServerLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
"UserDownloadingContent",
User.GetUserId())
{
- ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
+ ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetServerLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
ItemId = item.Id.ToString("N", CultureInfo.InvariantCulture)
}).ConfigureAwait(false);
}
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index e2075c2b8d..121db66858 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -115,6 +115,8 @@ public class TrailersController : BaseJellyfinApiController
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+ /// <param name="audioLanguages">Optional. If specified, results will be filtered based on audio language. This allows multiple, comma delimited values.</param>
+ /// <param name="subtitleLanguages">Optional. If specified, results will be filtered based on subtitale language. This allows multiple, comma delimited values.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
@@ -203,6 +205,8 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] audioLanguages,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] subtitleLanguages,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
@@ -294,6 +298,8 @@ public class TrailersController : BaseJellyfinApiController
nameLessThan,
studioIds,
genreIds,
+ audioLanguages,
+ subtitleLanguages,
enableTotalRecordCount,
enableImages).ConfigureAwait(false);
}