Merge pull request #4499 from crobibero/more-param

Reduce RequestHelpers.Split usage and remove RequestHelpers.GetGuids
This commit is contained in:
Joshua M. Boniface 2020-11-20 12:34:18 -05:00 committed by GitHub
commit 7457c4a95d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 712 additions and 344 deletions

View file

@ -634,7 +634,7 @@ namespace Emby.Server.Implementations.Channels
{ {
var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray(); var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray();
if (query.ChannelIds.Length > 0) if (query.ChannelIds.Count > 0)
{ {
// Avoid implicitly captured closure // Avoid implicitly captured closure
var ids = query.ChannelIds; var ids = query.ChannelIds;

View file

@ -3611,12 +3611,12 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add($"type in ({inClause})"); whereClauses.Add($"type in ({inClause})");
} }
if (query.ChannelIds.Length == 1) if (query.ChannelIds.Count == 1)
{ {
whereClauses.Add("ChannelId=@ChannelId"); whereClauses.Add("ChannelId=@ChannelId");
statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture));
} }
else if (query.ChannelIds.Length > 1) else if (query.ChannelIds.Count > 1)
{ {
var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
whereClauses.Add($"ChannelId in ({inClause})"); whereClauses.Add($"ChannelId in ({inClause})");
@ -4076,7 +4076,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(clause); whereClauses.Add(clause);
} }
if (query.GenreIds.Length > 0) if (query.GenreIds.Count > 0)
{ {
var clauses = new List<string>(); var clauses = new List<string>();
var index = 0; var index = 0;
@ -4097,7 +4097,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(clause); whereClauses.Add(clause);
} }
if (query.Genres.Length > 0) if (query.Genres.Count > 0)
{ {
var clauses = new List<string>(); var clauses = new List<string>();
var index = 0; var index = 0;

View file

@ -1503,7 +1503,7 @@ namespace Emby.Server.Implementations.Library
{ {
if (query.AncestorIds.Length == 0 && if (query.AncestorIds.Length == 0 &&
query.ParentId.Equals(Guid.Empty) && query.ParentId.Equals(Guid.Empty) &&
query.ChannelIds.Length == 0 && query.ChannelIds.Count == 0 &&
query.TopParentIds.Length == 0 && query.TopParentIds.Length == 0 &&
string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) && string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&

View file

@ -150,7 +150,7 @@ namespace Emby.Server.Implementations.Playlists
await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None) await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
.ConfigureAwait(false); .ConfigureAwait(false);
if (options.ItemIdList.Length > 0) if (options.ItemIdList.Count > 0)
{ {
await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false) await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false)
{ {
@ -184,7 +184,7 @@ namespace Emby.Server.Implementations.Playlists
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
} }
public Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId) public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
{ {
var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId); var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Playlists
}); });
} }
private async Task AddToPlaylistInternal(Guid playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options) private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options)
{ {
// Retrieve the existing playlist // Retrieve the existing playlist
var playlist = _libraryManager.GetItemById(playlistId) as Playlist var playlist = _libraryManager.GetItemById(playlistId) as Playlist

View file

@ -89,24 +89,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] string? officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery] string? tags, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery] string? years, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? studios, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery] string? studioIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
@ -131,30 +131,26 @@ namespace Jellyfin.Api.Controllers
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
} }
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = excludeItemTypesArr, ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = includeItemTypesArr, IncludeItemTypes = includeItemTypes,
MediaTypes = mediaTypesArr, MediaTypes = mediaTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
NameLessThan = nameLessThan, NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith, NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater, NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, '|', true), Tags = tags,
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), OfficialRatings = officialRatings,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
GenreIds = RequestHelpers.GetGuids(genreIds), GenreIds = genreIds,
StudioIds = RequestHelpers.GetGuids(studioIds), StudioIds = studioIds,
Person = person, Person = person,
PersonIds = RequestHelpers.GetGuids(personIds), PersonIds = personIds,
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), Years = years,
MinCommunityRating = minCommunityRating, MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
@ -174,9 +170,9 @@ namespace Jellyfin.Api.Controllers
} }
// Studios // Studios
if (!string.IsNullOrEmpty(studios)) if (studios.Length != 0)
{ {
query.StudioIds = studios.Split('|').Select(i => query.StudioIds = studios.Select(i =>
{ {
try try
{ {
@ -230,7 +226,7 @@ namespace Jellyfin.Api.Controllers
var (baseItem, itemCounts) = i; var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes)) if (includeItemTypes.Length != 0)
{ {
dto.ChildCount = itemCounts.ItemCount; dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount; dto.ProgramCount = itemCounts.ProgramCount;
@ -297,24 +293,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] string? officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery] string? tags, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery] string? years, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? studios, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery] string? studioIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
@ -339,30 +335,26 @@ namespace Jellyfin.Api.Controllers
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
} }
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = excludeItemTypesArr, ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = includeItemTypesArr, IncludeItemTypes = includeItemTypes,
MediaTypes = mediaTypesArr, MediaTypes = mediaTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
NameLessThan = nameLessThan, NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith, NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater, NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, '|', true), Tags = tags,
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), OfficialRatings = officialRatings,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
GenreIds = RequestHelpers.GetGuids(genreIds), GenreIds = genreIds,
StudioIds = RequestHelpers.GetGuids(studioIds), StudioIds = studioIds,
Person = person, Person = person,
PersonIds = RequestHelpers.GetGuids(personIds), PersonIds = personIds,
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), Years = years,
MinCommunityRating = minCommunityRating, MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
@ -382,9 +374,9 @@ namespace Jellyfin.Api.Controllers
} }
// Studios // Studios
if (!string.IsNullOrEmpty(studios)) if (studios.Length != 0)
{ {
query.StudioIds = studios.Split('|').Select(i => query.StudioIds = studios.Select(i =>
{ {
try try
{ {
@ -438,7 +430,7 @@ namespace Jellyfin.Api.Controllers
var (baseItem, itemCounts) = i; var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes)) if (includeItemTypes.Length != 0)
{ {
dto.ChildCount = itemCounts.ItemCount; dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount; dto.ProgramCount = itemCounts.ProgramCount;

View file

@ -198,7 +198,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? channelIds) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
@ -208,11 +208,7 @@ namespace Jellyfin.Api.Controllers
{ {
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
ChannelIds = (channelIds ?? string.Empty) ChannelIds = channelIds,
.Split(',')
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => new Guid(i))
.ToArray(),
DtoOptions = new DtoOptions { Fields = fields } DtoOptions = new DtoOptions { Fields = fields }
}; };

View file

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
@ -54,7 +55,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<CollectionCreationResult>> CreateCollection( public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
[FromQuery] string? name, [FromQuery] string? name,
[FromQuery] string? ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery] bool isLocked = false) [FromQuery] bool isLocked = false)
{ {
@ -65,7 +66,7 @@ namespace Jellyfin.Api.Controllers
IsLocked = isLocked, IsLocked = isLocked,
Name = name, Name = name,
ParentId = parentId, ParentId = parentId,
ItemIdList = RequestHelpers.Split(ids, ',', true), ItemIdList = ids,
UserIds = new[] { userId } UserIds = new[] { userId }
}).ConfigureAwait(false); }).ConfigureAwait(false);
@ -88,9 +89,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")] [HttpPost("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids) public async Task<ActionResult> AddToCollection(
[FromRoute, Required] Guid collectionId,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{ {
await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true); await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
return NoContent(); return NoContent();
} }
@ -103,9 +106,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")] [HttpDelete("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids) public async Task<ActionResult> RemoveFromCollection(
[FromRoute, Required] Guid collectionId,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{ {
await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false); await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
@ -50,8 +51,8 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy( public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] string? mediaTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
{ {
var parentItem = string.IsNullOrEmpty(parentId) var parentItem = string.IsNullOrEmpty(parentId)
? null ? null
@ -61,10 +62,11 @@ namespace Jellyfin.Api.Controllers
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length == 1
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
{ {
parentItem = null; parentItem = null;
} }
@ -78,8 +80,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery var query = new InternalItemsQuery
{ {
User = user, User = user,
MediaTypes = (mediaTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries), MediaTypes = mediaTypes,
IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries), IncludeItemTypes = includeItemTypes,
Recursive = true, Recursive = true,
EnableTotalRecordCount = false, EnableTotalRecordCount = false,
DtoOptions = new DtoOptions DtoOptions = new DtoOptions
@ -139,7 +141,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryFilters> GetQueryFilters( public ActionResult<QueryFilters> GetQueryFilters(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isAiring, [FromQuery] bool? isAiring,
[FromQuery] bool? isMovie, [FromQuery] bool? isMovie,
[FromQuery] bool? isSports, [FromQuery] bool? isSports,
@ -156,10 +158,11 @@ namespace Jellyfin.Api.Controllers
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length == 1
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
{ {
parentItem = null; parentItem = null;
} }
@ -167,8 +170,7 @@ namespace Jellyfin.Api.Controllers
var filters = new QueryFilters(); var filters = new QueryFilters();
var genreQuery = new InternalItemsQuery(user) var genreQuery = new InternalItemsQuery(user)
{ {
IncludeItemTypes = IncludeItemTypes = includeItemTypes,
(includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
DtoOptions = new DtoOptions DtoOptions = new DtoOptions
{ {
Fields = Array.Empty<ItemFields>(), Fields = Array.Empty<ItemFields>(),
@ -192,10 +194,11 @@ namespace Jellyfin.Api.Controllers
genreQuery.Parent = parentItem; genreQuery.Parent = parentItem;
} }
if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length == 1
|| string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase) && (string.Equals(includeItemTypes[0], nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase) || string.Equals(includeItemTypes[0], nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase)) || string.Equals(includeItemTypes[0], nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes[0], nameof(Audio), StringComparison.OrdinalIgnoreCase)))
{ {
filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
{ {

View file

@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers
result = _libraryManager.GetGenres(query); result = _libraryManager.GetGenres(query);
} }
var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes); var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
} }

View file

@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasParentalRating, [FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd, [FromQuery] bool? isHd,
[FromQuery] bool? is4K, [FromQuery] bool? is4K,
[FromQuery] string? locationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing, [FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired, [FromQuery] bool? isUnaired,
@ -173,7 +173,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId, [FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId, [FromQuery] bool? hasTvdbId,
[FromQuery] string? excludeItemIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool? recursive, [FromQuery] bool? recursive,
@ -181,34 +181,34 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery] string? tags, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery] string? years, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? studios, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery] string? artists, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
[FromQuery] string? excludeArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery] string? artistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
[FromQuery] string? albumArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
[FromQuery] string? contributingArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
[FromQuery] string? albums, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
[FromQuery] string? albumIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
[FromQuery] string? ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] string? videoTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating, [FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked, [FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder, [FromQuery] bool? isPlaceHolder,
@ -219,12 +219,12 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
[FromQuery] bool? is3D, [FromQuery] bool? is3D,
[FromQuery] string? seriesStatus, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan, [FromQuery] string? nameLessThan,
[FromQuery] string? studioIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
{ {
@ -238,8 +238,9 @@ namespace Jellyfin.Api.Controllers
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length == 1
|| string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase)) && (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase)
|| includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase)))
{ {
parentId = null; parentId = null;
} }
@ -262,7 +263,7 @@ namespace Jellyfin.Api.Controllers
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{ {
recursive = true; recursive = true;
includeItemTypes = "Playlist"; includeItemTypes = new[] { "Playlist" };
} }
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
@ -291,14 +292,14 @@ namespace Jellyfin.Api.Controllers
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
} }
if ((recursive.HasValue && recursive.Value) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder)) if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder))
{ {
var query = new InternalItemsQuery(user!) var query = new InternalItemsQuery(user!)
{ {
IsPlayed = isPlayed, IsPlayed = isPlayed,
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), MediaTypes = mediaTypes,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
Recursive = recursive ?? false, Recursive = recursive ?? false,
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -330,28 +331,28 @@ namespace Jellyfin.Api.Controllers
HasTrailer = hasTrailer, HasTrailer = hasTrailer,
IsHD = isHd, IsHD = isHd,
Is4K = is4K, Is4K = is4K,
Tags = RequestHelpers.Split(tags, '|', true), Tags = tags,
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), OfficialRatings = officialRatings,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
ArtistIds = RequestHelpers.GetGuids(artistIds), ArtistIds = artistIds,
AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds), AlbumArtistIds = albumArtistIds,
ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds), ContributingArtistIds = contributingArtistIds,
GenreIds = RequestHelpers.GetGuids(genreIds), GenreIds = genreIds,
StudioIds = RequestHelpers.GetGuids(studioIds), StudioIds = studioIds,
Person = person, Person = person,
PersonIds = RequestHelpers.GetGuids(personIds), PersonIds = personIds,
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), Years = years,
ImageTypes = imageTypes, ImageTypes = imageTypes,
VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(), VideoTypes = videoTypes,
AdjacentTo = adjacentTo, AdjacentTo = adjacentTo,
ItemIds = RequestHelpers.GetGuids(ids), ItemIds = ids,
MinCommunityRating = minCommunityRating, MinCommunityRating = minCommunityRating,
MinCriticRating = minCriticRating, MinCriticRating = minCriticRating,
ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId), ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId),
ParentIndexNumber = parentIndexNumber, ParentIndexNumber = parentIndexNumber,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds), ExcludeItemIds = excludeItemIds,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
MinDateLastSaved = minDateLastSaved?.ToUniversalTime(), MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
@ -360,7 +361,7 @@ namespace Jellyfin.Api.Controllers
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(), MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
}; };
if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm)) if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
{ {
query.CollapseBoxSetItems = false; query.CollapseBoxSetItems = false;
} }
@ -400,9 +401,9 @@ namespace Jellyfin.Api.Controllers
} }
// Filter by Series Status // Filter by Series Status
if (!string.IsNullOrEmpty(seriesStatus)) if (seriesStatus.Length != 0)
{ {
query.SeriesStatuses = seriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray(); query.SeriesStatuses = seriesStatus;
} }
// ExcludeLocationTypes // ExcludeLocationTypes
@ -411,13 +412,9 @@ namespace Jellyfin.Api.Controllers
query.IsVirtualItem = false; query.IsVirtualItem = false;
} }
if (!string.IsNullOrEmpty(locationTypes)) if (locationTypes.Length > 0 && locationTypes.Length < 4)
{ {
var requestedLocationTypes = locationTypes.Split(','); query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
{
query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
}
} }
// Min official rating // Min official rating
@ -433,9 +430,9 @@ namespace Jellyfin.Api.Controllers
} }
// Artists // Artists
if (!string.IsNullOrEmpty(artists)) if (artists.Length != 0)
{ {
query.ArtistIds = artists.Split('|').Select(i => query.ArtistIds = artists.Select(i =>
{ {
try try
{ {
@ -449,29 +446,29 @@ namespace Jellyfin.Api.Controllers
} }
// ExcludeArtistIds // ExcludeArtistIds
if (!string.IsNullOrWhiteSpace(excludeArtistIds)) if (excludeArtistIds.Length != 0)
{ {
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds); query.ExcludeArtistIds = excludeArtistIds;
} }
if (!string.IsNullOrWhiteSpace(albumIds)) if (albumIds.Length != 0)
{ {
query.AlbumIds = RequestHelpers.GetGuids(albumIds); query.AlbumIds = albumIds;
} }
// Albums // Albums
if (!string.IsNullOrEmpty(albums)) if (albums.Length != 0)
{ {
query.AlbumIds = albums.Split('|').SelectMany(i => query.AlbumIds = albums.SelectMany(i =>
{ {
return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 }); return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
}).ToArray(); }).ToArray();
} }
// Studios // Studios
if (!string.IsNullOrEmpty(studios)) if (studios.Length != 0)
{ {
query.StudioIds = studios.Split('|').Select(i => query.StudioIds = studios.Select(i =>
{ {
try try
{ {
@ -533,12 +530,12 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
{ {
@ -569,13 +566,13 @@ namespace Jellyfin.Api.Controllers
ParentId = parentIdGuid, ParentId = parentIdGuid,
Recursive = true, Recursive = true,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), MediaTypes = mediaTypes,
IsVirtualItem = false, IsVirtualItem = false,
CollapseBoxSetItems = false, CollapseBoxSetItems = false,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
AncestorIds = ancestorIds, AncestorIds = ancestorIds,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
SearchTerm = searchTerm SearchTerm = searchTerm
}); });

View file

@ -362,15 +362,14 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public ActionResult DeleteItems([FromQuery] string? ids) public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids)
{ {
if (string.IsNullOrEmpty(ids)) if (ids.Length == 0)
{ {
return NoContent(); return NoContent();
} }
var itemIds = RequestHelpers.Split(ids, ',', true); foreach (var i in ids)
foreach (var i in itemIds)
{ {
var item = _libraryManager.GetItemById(i); var item = _libraryManager.GetItemById(i);
var auth = _authContext.GetAuthorizationInfo(Request); var auth = _authContext.GetAuthorizationInfo(Request);
@ -691,7 +690,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems( public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery] string? excludeArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
@ -753,9 +752,9 @@ namespace Jellyfin.Api.Controllers
}; };
// ExcludeArtistIds // ExcludeArtistIds
if (!string.IsNullOrEmpty(excludeArtistIds)) if (excludeArtistIds.Length != 0)
{ {
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds); query.ExcludeArtistIds = excludeArtistIds;
} }
List<BaseItem> itemsResult = _libraryManager.GetItemList(query); List<BaseItem> itemsResult = _libraryManager.GetItemList(query);

View file

@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] string? sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] SortOrder? sortOrder, [FromQuery] SortOrder? sortOrder,
[FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true) [FromQuery] bool addCurrentProgram = true)
@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers
IsNews = isNews, IsNews = isNews,
IsKids = isKids, IsKids = isKids,
IsSports = isSports, IsSports = isSports,
SortBy = RequestHelpers.Split(sortBy, ',', true), SortBy = sortBy,
SortOrder = sortOrder ?? SortOrder.Ascending, SortOrder = sortOrder ?? SortOrder.Ascending,
AddCurrentProgram = addCurrentProgram AddCurrentProgram = addCurrentProgram
}, },
@ -539,7 +539,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms( public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
[FromQuery] string? channelIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] DateTime? minStartDate, [FromQuery] DateTime? minStartDate,
[FromQuery] bool? hasAired, [FromQuery] bool? hasAired,
@ -556,8 +556,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@ -573,8 +573,7 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ChannelIds = RequestHelpers.Split(channelIds, ',', true) ChannelIds = channelIds,
.Select(i => new Guid(i)).ToArray(),
HasAired = hasAired, HasAired = hasAired,
IsAiring = isAiring, IsAiring = isAiring,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
@ -591,8 +590,8 @@ namespace Jellyfin.Api.Controllers
IsKids = isKids, IsKids = isKids,
IsSports = isSports, IsSports = isSports,
SeriesTimerId = seriesTimerId, SeriesTimerId = seriesTimerId,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
GenreIds = RequestHelpers.GetGuids(genreIds) GenreIds = genreIds
}; };
if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty)) if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
@ -628,8 +627,7 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true) ChannelIds = body.ChannelIds,
.Select(i => new Guid(i)).ToArray(),
HasAired = body.HasAired, HasAired = body.HasAired,
IsAiring = body.IsAiring, IsAiring = body.IsAiring,
EnableTotalRecordCount = body.EnableTotalRecordCount, EnableTotalRecordCount = body.EnableTotalRecordCount,
@ -646,8 +644,8 @@ namespace Jellyfin.Api.Controllers
IsKids = body.IsKids, IsKids = body.IsKids,
IsSports = body.IsSports, IsSports = body.IsSports,
SeriesTimerId = body.SeriesTimerId, SeriesTimerId = body.SeriesTimerId,
Genres = RequestHelpers.Split(body.Genres, '|', true), Genres = body.Genres,
GenreIds = RequestHelpers.GetGuids(body.GenreIds) GenreIds = body.GenreIds
}; };
if (!body.LibrarySeriesId.Equals(Guid.Empty)) if (!body.LibrarySeriesId.Equals(Guid.Empty))
@ -703,7 +701,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
@ -723,7 +721,7 @@ namespace Jellyfin.Api.Controllers
IsNews = isNews, IsNews = isNews,
IsSports = isSports, IsSports = isSports,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
GenreIds = RequestHelpers.GetGuids(genreIds) GenreIds = genreIds
}; };
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }

View file

@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
var result = _libraryManager.GetMusicGenres(query); var result = _libraryManager.GetMusicGenres(query);
var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes); var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
} }

View file

@ -77,8 +77,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? excludePersonTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? appearsInItemId, [FromQuery] string? appearsInItemId,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
@ -97,8 +97,8 @@ namespace Jellyfin.Api.Controllers
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
{ {
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true), ExcludePersonTypes = excludePersonTypes,
NameContains = searchTerm, NameContains = searchTerm,
User = user, User = user,
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,

View file

@ -63,11 +63,10 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
[FromBody, Required] CreatePlaylistDto createPlaylistRequest) [FromBody, Required] CreatePlaylistDto createPlaylistRequest)
{ {
Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{ {
Name = createPlaylistRequest.Name, Name = createPlaylistRequest.Name,
ItemIdList = idGuidArray, ItemIdList = createPlaylistRequest.Ids,
UserId = createPlaylistRequest.UserId, UserId = createPlaylistRequest.UserId,
MediaType = createPlaylistRequest.MediaType MediaType = createPlaylistRequest.MediaType
}).ConfigureAwait(false); }).ConfigureAwait(false);
@ -87,10 +86,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddToPlaylist( public async Task<ActionResult> AddToPlaylist(
[FromRoute, Required] Guid playlistId, [FromRoute, Required] Guid playlistId,
[FromQuery] string? ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] Guid? userId) [FromQuery] Guid? userId)
{ {
await _playlistManager.AddToPlaylistAsync(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty).ConfigureAwait(false); await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -122,9 +121,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns> /// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")] [HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveFromPlaylist([FromRoute, Required] string playlistId, [FromQuery] string? entryIds) public async Task<ActionResult> RemoveFromPlaylist(
[FromRoute, Required] string playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
{ {
await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false); await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
return NoContent(); return NoContent();
} }

View file

@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -82,9 +83,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery, Required] string searchTerm, [FromQuery, Required] string searchTerm,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] bool? isMovie, [FromQuery] bool? isMovie,
[FromQuery] bool? isSeries, [FromQuery] bool? isSeries,
@ -108,9 +109,9 @@ namespace Jellyfin.Api.Controllers
IncludeStudios = includeStudios, IncludeStudios = includeStudios,
StartIndex = startIndex, StartIndex = startIndex,
UserId = userId ?? Guid.Empty, UserId = userId ?? Guid.Empty,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), MediaTypes = mediaTypes,
ParentId = parentId, ParentId = parentId,
IsKids = isKids, IsKids = isKids,

View file

@ -160,12 +160,12 @@ namespace Jellyfin.Api.Controllers
public ActionResult Play( public ActionResult Play(
[FromRoute, Required] string sessionId, [FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand, [FromQuery, Required] PlayCommand playCommand,
[FromQuery, Required] string itemIds, [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
[FromQuery] long? startPositionTicks) [FromQuery] long? startPositionTicks)
{ {
var playRequest = new PlayRequest var playRequest = new PlayRequest
{ {
ItemIds = RequestHelpers.GetGuids(itemIds), ItemIds = itemIds,
StartPositionTicks = startPositionTicks, StartPositionTicks = startPositionTicks,
PlayCommand = playCommand PlayCommand = playCommand
}; };
@ -378,7 +378,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostCapabilities( public ActionResult PostCapabilities(
[FromQuery] string? id, [FromQuery] string? id,
[FromQuery] string? playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false, [FromQuery] bool supportsSync = false,
@ -391,7 +391,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.ReportCapabilities(id, new ClientCapabilities _sessionManager.ReportCapabilities(id, new ClientCapabilities
{ {
PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true), PlayableMediaTypes = playableMediaTypes,
SupportedCommands = supportedCommands, SupportedCommands = supportedCommands,
SupportsMediaControl = supportsMediaControl, SupportsMediaControl = supportsMediaControl,
SupportsSync = supportsSync, SupportsSync = supportsSync,

View file

@ -73,8 +73,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
@ -94,13 +94,10 @@ namespace Jellyfin.Api.Controllers
var parentItem = _libraryManager.GetParentItem(parentId, userId); var parentItem = _libraryManager.GetParentItem(parentId, userId);
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = excludeItemTypesArr, ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = includeItemTypesArr, IncludeItemTypes = includeItemTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -125,7 +122,7 @@ namespace Jellyfin.Api.Controllers
} }
var result = _libraryManager.GetStudios(query); var result = _libraryManager.GetStudios(query);
var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes); var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
} }

View file

@ -4,6 +4,7 @@ using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -58,8 +59,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions( public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromQuery] string? mediaType, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
[FromQuery] string? type, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false) [FromQuery] bool enableTotalRecordCount = false)
@ -70,8 +71,8 @@ namespace Jellyfin.Api.Controllers
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
{ {
OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
MediaTypes = RequestHelpers.Split(mediaType!, ',', true), MediaTypes = mediaType,
IncludeItemTypes = RequestHelpers.Split(type!, ',', true), IncludeItemTypes = type,
IsVirtualItem = false, IsVirtualItem = false,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,

View file

@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasParentalRating, [FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd, [FromQuery] bool? isHd,
[FromQuery] bool? is4K, [FromQuery] bool? is4K,
[FromQuery] string? locationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing, [FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired, [FromQuery] bool? isUnaired,
@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId, [FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId, [FromQuery] bool? hasTvdbId,
[FromQuery] string? excludeItemIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool? recursive, [FromQuery] bool? recursive,
@ -147,33 +147,33 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery] string? tags, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery] string? years, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? studios, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
[FromQuery] string? artists, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
[FromQuery] string? excludeArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery] string? artistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
[FromQuery] string? albumArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
[FromQuery] string? contributingArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
[FromQuery] string? albums, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
[FromQuery] string? albumIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
[FromQuery] string? ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] string? videoTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating, [FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked, [FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder, [FromQuery] bool? isPlaceHolder,
@ -184,16 +184,16 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
[FromQuery] bool? is3D, [FromQuery] bool? is3D,
[FromQuery] string? seriesStatus, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan, [FromQuery] string? nameLessThan,
[FromQuery] string? studioIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
{ {
var includeItemTypes = "Trailer"; var includeItemTypes = new[] { "Trailer" };
return _itemsController return _itemsController
.GetItems( .GetItems(

View file

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
@ -96,7 +97,7 @@ namespace Jellyfin.Api.Controllers
[ProducesAudioFile] [ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream( public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery] string? container, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container,
[FromQuery] string? mediaSourceId, [FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId, [FromQuery] string? deviceId,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
@ -261,7 +262,7 @@ namespace Jellyfin.Api.Controllers
} }
private DeviceProfile GetDeviceProfile( private DeviceProfile GetDeviceProfile(
string? container, string[] containers,
string? transcodingContainer, string? transcodingContainer,
string? audioCodec, string? audioCodec,
string? transcodingProtocol, string? transcodingProtocol,
@ -273,7 +274,6 @@ namespace Jellyfin.Api.Controllers
{ {
var deviceProfile = new DeviceProfile(); var deviceProfile = new DeviceProfile();
var containers = RequestHelpers.Split(container, ',', true);
int len = containers.Length; int len = containers.Length;
var directPlayProfiles = new DirectPlayProfile[len]; var directPlayProfiles = new DirectPlayProfile[len];
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
@ -330,7 +330,7 @@ namespace Jellyfin.Api.Controllers
if (conditions.Count > 0) if (conditions.Count > 0)
{ {
// codec profile // codec profile
codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = container, Conditions = conditions.ToArray() }); codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = string.Join(',', containers), Conditions = conditions.ToArray() });
} }
deviceProfile.CodecProfiles = codecProfiles.ToArray(); deviceProfile.CodecProfiles = codecProfiles.ToArray();

View file

@ -269,7 +269,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
new LatestItemsQuery new LatestItemsQuery
{ {
GroupItems = groupItems, GroupItems = groupItems,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
IsPlayed = isPlayed, IsPlayed = isPlayed,
Limit = limit, Limit = limit,
ParentId = parentId ?? Guid.Empty, ParentId = parentId ?? Guid.Empty,

View file

@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos; using Jellyfin.Api.Models.UserViewDtos;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryResult<BaseItemDto>> GetUserViews( public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent, [FromQuery] bool? includeExternalContent,
[FromQuery] string? presetViews, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
[FromQuery] bool includeHidden = false) [FromQuery] bool includeHidden = false)
{ {
var query = new UserViewQuery var query = new UserViewQuery
@ -81,9 +82,9 @@ namespace Jellyfin.Api.Controllers
query.IncludeExternalContent = includeExternalContent.Value; query.IncludeExternalContent = includeExternalContent.Value;
} }
if (!string.IsNullOrWhiteSpace(presetViews)) if (presetViews.Length != 0)
{ {
query.PresetViews = RequestHelpers.Split(presetViews, ',', true); query.PresetViews = presetViews;
} }
var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty; var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;

View file

@ -10,6 +10,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
@ -203,9 +204,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> MergeVersions([FromQuery, Required] string itemIds) public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds)
{ {
var items = RequestHelpers.Split(itemIds, ',', true) var items = itemIds
.Select(i => _libraryManager.GetItemById(i)) .Select(i => _libraryManager.GetItemById(i))
.OfType<Video>() .OfType<Video>()
.OrderBy(i => i.Id) .OrderBy(i => i.Id)

View file

@ -73,9 +73,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
@ -103,19 +103,15 @@ namespace Jellyfin.Api.Controllers
IList<BaseItem> items; IList<BaseItem> items;
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = excludeItemTypesArr, ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = includeItemTypesArr, IncludeItemTypes = includeItemTypes,
MediaTypes = mediaTypesArr, MediaTypes = mediaTypes,
DtoOptions = dtoOptions DtoOptions = dtoOptions
}; };
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypesArr, includeItemTypesArr, mediaTypesArr); bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
if (parentItem.IsFolder) if (parentItem.IsFolder)
{ {

View file

@ -122,49 +122,6 @@ namespace Jellyfin.Api.Helpers
return session; return session;
} }
/// <summary>
/// Get Guid array from string.
/// </summary>
/// <param name="value">String value.</param>
/// <returns>Guid array.</returns>
internal static Guid[] GetGuids(string? value)
{
if (value == null)
{
return Array.Empty<Guid>();
}
return Split(value, ',', true)
.Select(i => new Guid(i))
.ToArray();
}
/// <summary>
/// Gets the item fields.
/// </summary>
/// <param name="fields">The fields string.</param>
/// <returns>IEnumerable{ItemFields}.</returns>
internal static ItemFields[] GetItemFields(string? fields)
{
if (string.IsNullOrEmpty(fields))
{
return Array.Empty<ItemFields>();
}
return Split(fields, ',', true)
.Select(v =>
{
if (Enum.TryParse(v, true, out ItemFields value))
{
return (ItemFields?)value;
}
return null;
}).Where(i => i.HasValue)
.Select(i => i!.Value)
.ToArray();
}
internal static QueryResult<BaseItemDto> CreateQueryResult( internal static QueryResult<BaseItemDto> CreateQueryResult(
QueryResult<(BaseItem, ItemCounts)> result, QueryResult<(BaseItem, ItemCounts)> result,
DtoOptions dtoOptions, DtoOptions dtoOptions,

View file

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.ModelBinders
{
/// <summary>
/// Comma delimited array model binder.
/// Returns an empty array of specified type if there is no query parameter.
/// </summary>
public class PipeDelimitedArrayModelBinder : IModelBinder
{
private readonly ILogger<PipeDelimitedArrayModelBinder> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="PipeDelimitedArrayModelBinder"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{PipeDelimitedArrayModelBinder}"/> interface.</param>
public PipeDelimitedArrayModelBinder(ILogger<PipeDelimitedArrayModelBinder> logger)
{
_logger = logger;
}
/// <inheritdoc/>
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
var converter = TypeDescriptor.GetConverter(elementType);
if (valueProviderResult.Length > 1)
{
var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter);
bindingContext.Result = ModelBindingResult.Success(typedValues);
}
else
{
var value = valueProviderResult.FirstValue;
if (value != null)
{
var splitValues = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
var typedValues = GetParsedResult(splitValues, elementType, converter);
bindingContext.Result = ModelBindingResult.Success(typedValues);
}
else
{
var emptyResult = Array.CreateInstance(elementType, 0);
bindingContext.Result = ModelBindingResult.Success(emptyResult);
}
}
return Task.CompletedTask;
}
private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter)
{
var parsedValues = new object?[values.Count];
var convertedCount = 0;
for (var i = 0; i < values.Count; i++)
{
try
{
parsedValues[i] = converter.ConvertFromString(values[i].Trim());
convertedCount++;
}
catch (FormatException e)
{
_logger.LogWarning(e, "Error converting value.");
}
}
var typedValues = Array.CreateInstance(elementType, convertedCount);
var typedValueIndex = 0;
for (var i = 0; i < parsedValues.Length; i++)
{
if (parsedValues[i] != null)
{
typedValues.SetValue(parsedValues[i], typedValueIndex);
typedValueIndex++;
}
}
return typedValues;
}
}
}

View file

@ -16,7 +16,8 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// <summary> /// <summary>
/// Gets or sets the channels to return guide information for. /// Gets or sets the channels to return guide information for.
/// </summary> /// </summary>
public string? ChannelIds { get; set; } [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
public IReadOnlyList<Guid> ChannelIds { get; set; } = Array.Empty<Guid>();
/// <summary> /// <summary>
/// Gets or sets optional. Filter by user id. /// Gets or sets optional. Filter by user id.
@ -115,12 +116,14 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// <summary> /// <summary>
/// Gets or sets the genres to return guide information for. /// Gets or sets the genres to return guide information for.
/// </summary> /// </summary>
public string? Genres { get; set; } [JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))]
public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
/// <summary> /// <summary>
/// Gets or sets the genre ids to return guide information for. /// Gets or sets the genre ids to return guide information for.
/// </summary> /// </summary>
public string? GenreIds { get; set; } [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
public IReadOnlyList<Guid> GenreIds { get; set; } = Array.Empty<Guid>();
/// <summary> /// <summary>
/// Gets or sets include image information in output. /// Gets or sets include image information in output.

View file

@ -1,4 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using MediaBrowser.Common.Json.Converters;
namespace Jellyfin.Api.Models.PlaylistDtos namespace Jellyfin.Api.Models.PlaylistDtos
{ {
@ -15,7 +18,8 @@ namespace Jellyfin.Api.Models.PlaylistDtos
/// <summary> /// <summary>
/// Gets or sets item ids to add to the playlist. /// Gets or sets item ids to add to the playlist.
/// </summary> /// </summary>
public string? Ids { get; set; } [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
public IReadOnlyList<Guid> Ids { get; set; } = Array.Empty<Guid>();
/// <summary> /// <summary>
/// Gets or sets the user id. /// Gets or sets the user id.

View file

@ -43,7 +43,8 @@ namespace MediaBrowser.Common.Json.Converters
} }
catch (FormatException) catch (FormatException)
{ {
// TODO log when upgraded to .Net5 // TODO log when upgraded to .Net6
// https://github.com/dotnet/runtime/issues/42975
// _logger.LogWarning(e, "Error converting value."); // _logger.LogWarning(e, "Error converting value.");
} }
} }

View file

@ -0,0 +1,75 @@
using System;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Convert Pipe delimited string to array of type.
/// </summary>
/// <typeparam name="T">Type to convert to.</typeparam>
public class JsonPipeDelimitedArrayConverter<T> : JsonConverter<T[]>
{
private readonly TypeConverter _typeConverter;
/// <summary>
/// Initializes a new instance of the <see cref="JsonPipeDelimitedArrayConverter{T}"/> class.
/// </summary>
public JsonPipeDelimitedArrayConverter()
{
_typeConverter = TypeDescriptor.GetConverter(typeof(T));
}
/// <inheritdoc />
public override T[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
var stringEntries = reader.GetString()?.Split('|', StringSplitOptions.RemoveEmptyEntries);
if (stringEntries == null || stringEntries.Length == 0)
{
return Array.Empty<T>();
}
var parsedValues = new object[stringEntries.Length];
var convertedCount = 0;
for (var i = 0; i < stringEntries.Length; i++)
{
try
{
parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
convertedCount++;
}
catch (FormatException)
{
// TODO log when upgraded to .Net6
// https://github.com/dotnet/runtime/issues/42975
// _logger.LogWarning(e, "Error converting value.");
}
}
var typedValues = new T[convertedCount];
var typedValueIndex = 0;
for (var i = 0; i < stringEntries.Length; i++)
{
if (parsedValues[i] != null)
{
typedValues.SetValue(parsedValues[i], typedValueIndex);
typedValueIndex++;
}
}
return typedValues;
}
return JsonSerializer.Deserialize<T[]>(ref reader, options);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}
}

View file

@ -0,0 +1,28 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Json Pipe delimited array converter factory.
/// </summary>
/// <remarks>
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
/// </remarks>
public class JsonPipeDelimitedArrayConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return true;
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
return (JsonConverter)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType));
}
}
}

View file

@ -20,7 +20,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup> </ItemGroup>

View file

@ -1067,12 +1067,12 @@ namespace MediaBrowser.Controller.Entities
return false; return false;
} }
if (request.Genres.Length > 0) if (request.Genres.Count > 0)
{ {
return false; return false;
} }
if (request.GenreIds.Length > 0) if (request.GenreIds.Count > 0)
{ {
return false; return false;
} }
@ -1177,7 +1177,7 @@ namespace MediaBrowser.Controller.Entities
return false; return false;
} }
if (request.GenreIds.Length > 0) if (request.GenreIds.Count > 0)
{ {
return false; return false;
} }

View file

@ -46,7 +46,7 @@ namespace MediaBrowser.Controller.Entities
public string[] ExcludeInheritedTags { get; set; } public string[] ExcludeInheritedTags { get; set; }
public string[] Genres { get; set; } public IReadOnlyList<string> Genres { get; set; }
public bool? IsSpecialSeason { get; set; } public bool? IsSpecialSeason { get; set; }
@ -116,7 +116,7 @@ namespace MediaBrowser.Controller.Entities
public Guid[] StudioIds { get; set; } public Guid[] StudioIds { get; set; }
public Guid[] GenreIds { get; set; } public IReadOnlyList<Guid> GenreIds { get; set; }
public ImageType[] ImageTypes { get; set; } public ImageType[] ImageTypes { get; set; }
@ -162,7 +162,7 @@ namespace MediaBrowser.Controller.Entities
public double? MinCommunityRating { get; set; } public double? MinCommunityRating { get; set; }
public Guid[] ChannelIds { get; set; } public IReadOnlyList<Guid> ChannelIds { get; set; }
public int? ParentIndexNumber { get; set; } public int? ParentIndexNumber { get; set; }

View file

@ -791,7 +791,7 @@ namespace MediaBrowser.Controller.Entities
} }
// Apply genre filter // Apply genre filter
if (query.Genres.Length > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase))) if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
{ {
return false; return false;
} }
@ -822,7 +822,7 @@ namespace MediaBrowser.Controller.Entities
} }
// Apply genre filter // Apply genre filter
if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id => if (query.GenreIds.Count > 0 && !query.GenreIds.Any(id =>
{ {
var genreItem = libraryManager.GetItemById(id); var genreItem = libraryManager.GetItemById(id);
return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase); return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase);

View file

@ -31,7 +31,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="itemIds">The item ids.</param> /// <param name="itemIds">The item ids.</param>
/// <param name="userId">The user identifier.</param> /// <param name="userId">The user identifier.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId); Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
/// <summary> /// <summary>
/// Removes from playlist. /// Removes from playlist.

View file

@ -2,6 +2,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Generic;
namespace MediaBrowser.Model.Playlists namespace MediaBrowser.Model.Playlists
{ {
@ -9,15 +10,10 @@ namespace MediaBrowser.Model.Playlists
{ {
public string Name { get; set; } public string Name { get; set; }
public Guid[] ItemIdList { get; set; } public IReadOnlyList<Guid> ItemIdList { get; set; } = Array.Empty<Guid>();
public string MediaType { get; set; } public string MediaType { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
public PlaylistCreationRequest()
{
ItemIdList = Array.Empty<Guid>();
}
} }
} }

View file

@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Api.ModelBinders;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.ModelBinders
{
public sealed class PipeDelimitedArrayModelBinderTests
{
[Fact]
public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedStringArrayQuery()
{
var queryParamName = "test";
IReadOnlyList<string> queryParamValues = new[] { "lol", "xd" };
var queryParamString = "lol|xd";
var queryParamType = typeof(string[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
CultureInfo.InvariantCulture);
var bindingContextMock = new Mock<ModelBindingContext>();
bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
bindingContextMock.SetupProperty(b => b.Result);
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
Assert.Equal((IReadOnlyList<string>?)bindingContextMock.Object?.Result.Model, queryParamValues);
}
[Fact]
public async Task BindModelAsync_CorrectlyBindsValidDelimitedIntArrayQuery()
{
var queryParamName = "test";
IReadOnlyList<int> queryParamValues = new[] { 42, 0 };
var queryParamString = "42|0";
var queryParamType = typeof(int[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
CultureInfo.InvariantCulture);
var bindingContextMock = new Mock<ModelBindingContext>();
bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
bindingContextMock.SetupProperty(b => b.Result);
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
Assert.Equal((IReadOnlyList<int>?)bindingContextMock.Object.Result.Model, queryParamValues);
}
[Fact]
public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedEnumArrayQuery()
{
var queryParamName = "test";
IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much };
var queryParamString = "How|Much";
var queryParamType = typeof(TestType[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
CultureInfo.InvariantCulture);
var bindingContextMock = new Mock<ModelBindingContext>();
bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
bindingContextMock.SetupProperty(b => b.Result);
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
}
[Fact]
public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedEnumArrayQueryWithDoublePipes()
{
var queryParamName = "test";
IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much };
var queryParamString = "How||Much";
var queryParamType = typeof(TestType[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
CultureInfo.InvariantCulture);
var bindingContextMock = new Mock<ModelBindingContext>();
bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
bindingContextMock.SetupProperty(b => b.Result);
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
}
[Fact]
public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery()
{
var queryParamName = "test";
IReadOnlyList<TestType> queryParamValues = new[] { TestType.How, TestType.Much };
var queryParamString1 = "How";
var queryParamString2 = "Much";
var queryParamType = typeof(TestType[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues>
{
{ queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) },
}),
CultureInfo.InvariantCulture);
var bindingContextMock = new Mock<ModelBindingContext>();
bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
bindingContextMock.SetupProperty(b => b.Result);
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
}
[Fact]
public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery()
{
var queryParamName = "test";
IReadOnlyList<TestType> queryParamValues = Array.Empty<TestType>();
var queryParamType = typeof(TestType[]);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues>
{
{ queryParamName, new StringValues(value: null) },
}),
CultureInfo.InvariantCulture);
var bindingContextMock = new Mock<ModelBindingContext>();
bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
bindingContextMock.SetupProperty(b => b.Result);
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
Assert.Equal((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model, queryParamValues);
}
[Fact]
public async Task BindModelAsync_EnumArrayQuery_BindValidOnly()
{
var queryParamName = "test";
var queryParamString = "🔥|😢";
var queryParamType = typeof(IReadOnlyList<TestType>);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues> { { queryParamName, new StringValues(queryParamString) } }),
CultureInfo.InvariantCulture);
var bindingContextMock = new Mock<ModelBindingContext>();
bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
bindingContextMock.SetupProperty(b => b.Result);
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
Assert.Empty((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model);
}
[Fact]
public async Task BindModelAsync_EnumArrayQuery_BindValidOnly_2()
{
var queryParamName = "test";
var queryParamString1 = "How";
var queryParamString2 = "😱";
var queryParamType = typeof(IReadOnlyList<TestType>);
var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger<PipeDelimitedArrayModelBinder>());
var valueProvider = new QueryStringValueProvider(
new BindingSource(string.Empty, string.Empty, false, false),
new QueryCollection(new Dictionary<string, StringValues>
{
{ queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) },
}),
CultureInfo.InvariantCulture);
var bindingContextMock = new Mock<ModelBindingContext>();
bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
bindingContextMock.SetupProperty(b => b.Result);
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
Assert.Single((IReadOnlyList<TestType>?)bindingContextMock.Object.Result.Model);
}
}
}