From 056dcf7e81049a9d92064d5f91dadfbcf9093a63 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 14 Nov 2024 09:04:35 +0000 Subject: Added Pipeline debug code --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'Jellyfin.Api/Controllers/LibraryStructureController.cs') diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 93c2393f3..c52068000 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -106,7 +106,15 @@ public class LibraryStructureController : BaseJellyfinApiController [FromQuery] string name, [FromQuery] bool refreshLibrary = false) { - await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); + try + { + await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); + } + catch (Exception ex) + { + return BadRequest(ex.ToString()); + } + return NoContent(); } -- cgit v1.2.3 From 432cfba2e2516709dfaea004a2d74095aac6a6f3 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 14 Nov 2024 09:55:32 +0000 Subject: Reverted Test code --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 9 +-------- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 5 +++++ 2 files changed, 6 insertions(+), 8 deletions(-) (limited to 'Jellyfin.Api/Controllers/LibraryStructureController.cs') diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index c52068000..c3e3b659b 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -106,14 +106,7 @@ public class LibraryStructureController : BaseJellyfinApiController [FromQuery] string name, [FromQuery] bool refreshLibrary = false) { - try - { - await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); - } - catch (Exception ex) - { - return BadRequest(ex.ToString()); - } + await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index f1afd3543..14300d237 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1290,6 +1290,11 @@ public sealed class BaseItemRepository( { foreach (var ancestorId in item.AncestorIds) { + if (!context.BaseItems.Any(f => f.Id == ancestorId)) + { + throw new InvalidOperationException($"Cannot link non-existent parent: {ancestorId}"); + } + context.AncestorIds.Add(new AncestorId() { ParentItemId = ancestorId, -- cgit v1.2.3 From aea255f91026598c297d11f032cc483b8608616d Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 14 Nov 2024 10:14:41 +0000 Subject: Deterministic tests my *** --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 9 ++++++++- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 4 ++-- .../Controllers/LibraryStructureControllerTests.cs | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) (limited to 'Jellyfin.Api/Controllers/LibraryStructureController.cs') diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index c3e3b659b..7838c2f61 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -88,7 +88,14 @@ public class LibraryStructureController : BaseJellyfinApiController libraryOptions.PathInfos = Array.ConvertAll(paths, i => new MediaPathInfo(i)); } - await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); + try + { + await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); + } + catch (System.Exception ex) + { + return BadRequest(ex.ToString()); + } return NoContent(); } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 14300d237..f62d6fc1a 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -83,7 +83,7 @@ public sealed class BaseItemRepository( context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete(); context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); - context.AncestorIds.Where(e => e.ItemId == id).ExecuteDelete(); + context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete(); context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete(); context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete(); @@ -1292,7 +1292,7 @@ public sealed class BaseItemRepository( { if (!context.BaseItems.Any(f => f.Id == ancestorId)) { - throw new InvalidOperationException($"Cannot link non-existent parent: {ancestorId}"); + continue; } context.AncestorIds.Add(new AncestorId() diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs index dc8c33c73..6b749f0a8 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs @@ -77,7 +77,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture Date: Thu, 14 Nov 2024 10:25:49 +0000 Subject: reverted dbg code --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 11 +++-------- .../Controllers/LibraryStructureControllerTests.cs | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) (limited to 'Jellyfin.Api/Controllers/LibraryStructureController.cs') diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 7838c2f61..55000fc91 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -88,14 +88,7 @@ public class LibraryStructureController : BaseJellyfinApiController libraryOptions.PathInfos = Array.ConvertAll(paths, i => new MediaPathInfo(i)); } - try - { - await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); - } - catch (System.Exception ex) - { - return BadRequest(ex.ToString()); - } + await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); return NoContent(); } @@ -106,6 +99,7 @@ public class LibraryStructureController : BaseJellyfinApiController /// The name of the folder. /// Whether to refresh the library. /// Folder removed. + /// Folder not found. /// A . [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -113,6 +107,7 @@ public class LibraryStructureController : BaseJellyfinApiController [FromQuery] string name, [FromQuery] bool refreshLibrary = false) { + // TODO: refactor! this relies on an FileNotFound exception to return NotFound when attempting to remove a library that does not exist. await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); return NoContent(); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs index 6b749f0a8..0376f57cc 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs @@ -77,7 +77,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture Date: Fri, 14 Feb 2025 04:24:55 +0100 Subject: Make the JsonConverters for delimited arrays more generic (#13396) * Make the JsonConverters for delimited arrays more generic Also adds some tests for serialization (with different types) as we didn't have any before. * Ignore warnings --- Jellyfin.Api/Controllers/ArtistsController.cs | 68 +++--- Jellyfin.Api/Controllers/ChannelsController.cs | 14 +- Jellyfin.Api/Controllers/CollectionController.cs | 6 +- Jellyfin.Api/Controllers/FilterController.cs | 6 +- Jellyfin.Api/Controllers/GenresController.cs | 12 +- Jellyfin.Api/Controllers/InstantMixController.cs | 32 +-- Jellyfin.Api/Controllers/ItemsController.cs | 144 ++++++------- Jellyfin.Api/Controllers/LibraryController.cs | 18 +- .../Controllers/LibraryStructureController.cs | 2 +- Jellyfin.Api/Controllers/LiveTvController.cs | 34 +-- Jellyfin.Api/Controllers/MoviesController.cs | 2 +- Jellyfin.Api/Controllers/MusicGenresController.cs | 12 +- Jellyfin.Api/Controllers/PersonsController.cs | 10 +- Jellyfin.Api/Controllers/PlaylistsController.cs | 10 +- Jellyfin.Api/Controllers/SearchController.cs | 6 +- Jellyfin.Api/Controllers/SessionController.cs | 6 +- Jellyfin.Api/Controllers/StudiosController.cs | 8 +- Jellyfin.Api/Controllers/SuggestionsController.cs | 8 +- Jellyfin.Api/Controllers/TrailersController.cs | 60 +++--- Jellyfin.Api/Controllers/TvShowsController.cs | 16 +- .../Controllers/UniversalAudioController.cs | 2 +- Jellyfin.Api/Controllers/UserLibraryController.cs | 12 +- Jellyfin.Api/Controllers/UserViewsController.cs | 4 +- Jellyfin.Api/Controllers/VideosController.cs | 2 +- Jellyfin.Api/Controllers/YearsController.cs | 14 +- .../ModelBinders/CommaDelimitedArrayModelBinder.cs | 89 -------- .../CommaDelimitedCollectionModelBinder.cs | 89 ++++++++ .../ModelBinders/PipeDelimitedArrayModelBinder.cs | 89 -------- .../PipeDelimitedCollectionModelBinder.cs | 89 ++++++++ Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs | 14 +- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 2 +- .../Models/PlaylistDtos/UpdatePlaylistDto.cs | 2 +- MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs | 4 +- .../Converters/JsonCommaDelimitedArrayConverter.cs | 19 -- .../JsonCommaDelimitedArrayConverterFactory.cs | 28 --- .../JsonCommaDelimitedCollectionConverter.cs | 19 ++ ...JsonCommaDelimitedCollectionConverterFactory.cs | 31 +++ .../Json/Converters/JsonDelimitedArrayConverter.cs | 90 -------- .../Converters/JsonDelimitedCollectionConverter.cs | 76 +++++++ .../Converters/JsonPipeDelimitedArrayConverter.cs | 19 -- .../JsonPipeDelimitedArrayConverterFactory.cs | 28 --- .../JsonPipeDelimitedCollectionConverter.cs | 19 ++ .../JsonPipeDelimitedCollectionConverterFactory.cs | 31 +++ .../CommaDelimitedArrayModelBinderTests.cs | 230 --------------------- .../CommaDelimitedCollectionModelBinderTests.cs | 230 +++++++++++++++++++++ .../PipeDelimitedArrayModelBinderTests.cs | 230 --------------------- .../PipeDelimitedCollectionModelBinderTests.cs | 230 +++++++++++++++++++++ .../Converters/JsonCommaDelimitedArrayTests.cs | 135 ------------ .../JsonCommaDelimitedCollectionTests.cs | 208 +++++++++++++++++++ .../JsonCommaDelimitedIReadOnlyListTests.cs | 13 ++ .../Json/Models/GenericBodyArrayModel.cs | 2 +- .../Models/GenericBodyIReadOnlyCollectionModel.cs | 19 ++ .../Json/Models/GenericBodyIReadOnlyListModel.cs | 2 +- .../Json/Models/GenericBodyListModel.cs | 22 ++ 54 files changed, 1343 insertions(+), 1224 deletions(-) delete mode 100644 Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs create mode 100644 Jellyfin.Api/ModelBinders/CommaDelimitedCollectionModelBinder.cs delete mode 100644 Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs create mode 100644 Jellyfin.Api/ModelBinders/PipeDelimitedCollectionModelBinder.cs delete mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs delete mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverterFactory.cs delete mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonDelimitedCollectionConverter.cs delete mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs delete mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverter.cs create mode 100644 src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverterFactory.cs delete mode 100644 tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs create mode 100644 tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedCollectionModelBinderTests.cs delete mode 100644 tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs create mode 100644 tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedCollectionModelBinderTests.cs delete mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedCollectionTests.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyCollectionModel.cs create mode 100644 tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyListModel.cs (limited to 'Jellyfin.Api/Controllers/LibraryStructureController.cs') diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 8b931f162..10556da65 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -91,31 +91,31 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds, [FromQuery] Guid? userId, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -295,31 +295,31 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds, [FromQuery] Guid? userId, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index f83c71b57..2f55e88ec 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -121,10 +121,10 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -197,9 +197,9 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] channelIds) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 2d9f1ed69..c37f37633 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -50,7 +50,7 @@ public class CollectionController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CreateCollection( [FromQuery] string? name, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] ids, [FromQuery] Guid? parentId, [FromQuery] bool isLocked = false) { @@ -86,7 +86,7 @@ public class CollectionController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task AddToCollection( [FromRoute, Required] Guid collectionId, - [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids) { await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true); return NoContent(); @@ -103,7 +103,7 @@ public class CollectionController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task RemoveFromCollection( [FromRoute, Required] Guid collectionId, - [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids) { await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false); return NoContent(); diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 4abca3271..3f9aa93a6 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -50,8 +50,8 @@ public class FilterController : BaseJellyfinApiController public ActionResult GetQueryFiltersLegacy( [FromQuery] Guid? userId, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -137,7 +137,7 @@ public class FilterController : BaseJellyfinApiController public ActionResult GetQueryFilters( [FromQuery] Guid? userId, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isAiring, [FromQuery] bool? isMovie, [FromQuery] bool? isSports, diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 54d48aec2..f0d17decb 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -76,18 +76,18 @@ public class GenresController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] Guid? userId, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 87a856d38..e326b925b 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -73,11 +73,11 @@ public class InstantMixController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -117,11 +117,11 @@ public class InstantMixController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -161,11 +161,11 @@ public class InstantMixController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -203,11 +203,11 @@ public class InstantMixController : BaseJellyfinApiController [FromRoute, Required] string name, [FromQuery] Guid? userId, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -241,11 +241,11 @@ public class InstantMixController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -285,11 +285,11 @@ public class InstantMixController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -330,11 +330,11 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, Required] Guid id, [FromQuery] Guid? userId, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes) { return GetInstantMixFromArtists( id, @@ -368,11 +368,11 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, Required] Guid id, [FromQuery] Guid? userId, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 775d723b0..ed2f49b86 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -171,8 +171,8 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, [FromQuery] bool? is4K, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes, [FromQuery] bool? isMissing, [FromQuery] bool? isUnaired, [FromQuery] double? minCommunityRating, @@ -190,42 +190,42 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] bool? isNews, [FromQuery] bool? isKids, [FromQuery] bool? isSports, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? recursive, [FromQuery] string? searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] artists, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] albums, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes, [FromQuery] string? minOfficialRating, [FromQuery] bool? isLocked, [FromQuery] bool? isPlaceHolder, @@ -236,12 +236,12 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] bool? is3D, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { @@ -638,8 +638,8 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, [FromQuery] bool? is4K, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes, [FromQuery] bool? isMissing, [FromQuery] bool? isUnaired, [FromQuery] double? minCommunityRating, @@ -657,42 +657,42 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] bool? isNews, [FromQuery] bool? isKids, [FromQuery] bool? isSports, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? recursive, [FromQuery] string? searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] artists, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] albums, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes, [FromQuery] string? minOfficialRating, [FromQuery] bool? isLocked, [FromQuery] bool? isPlaceHolder, @@ -703,12 +703,12 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] bool? is3D, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) => GetItems( @@ -827,13 +827,13 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true, [FromQuery] bool excludeActiveSessions = false) @@ -929,13 +929,13 @@ public class ItemsController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true, [FromQuery] bool excludeActiveSessions = false) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 0b2d4b032..7c6160fc4 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -144,8 +144,8 @@ public class LibraryController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -218,8 +218,8 @@ public class LibraryController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() @@ -290,8 +290,8 @@ public class LibraryController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[]? sortBy = null, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[]? sortOrder = null) { var themeSongs = GetThemeSongs( itemId, @@ -400,7 +400,7 @@ public class LibraryController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids) { var isApiKey = User.GetIsApiKey(); var userId = User.GetUserId(); @@ -722,10 +722,10 @@ public class LibraryController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarItems( [FromRoute, Required] Guid itemId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds, [FromQuery] Guid? userId, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.IsNullOrEmpty() diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 55000fc91..2a885662b 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -77,7 +77,7 @@ public class LibraryStructureController : BaseJellyfinApiController public async Task AddVirtualFolder( [FromQuery] string name, [FromQuery] CollectionTypeOptions? collectionType, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] paths, [FromBody] AddVirtualFolderDto? libraryOptionsDto, [FromQuery] bool refreshLibrary = false) { diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 421f23fa1..a3b4c8700 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -159,10 +159,10 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? isDisliked, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy, [FromQuery] SortOrder? sortOrder, [FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool addCurrentProgram = true) @@ -283,8 +283,8 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] string? seriesTimerId, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, @@ -371,8 +371,8 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] string? seriesTimerId, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) { @@ -566,7 +566,7 @@ public class LiveTvController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.LiveTvAccess)] public async Task>> GetLiveTvPrograms( - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] channelIds, [FromQuery] Guid? userId, [FromQuery] DateTime? minStartDate, [FromQuery] bool? hasAired, @@ -581,17 +581,17 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? isSports, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] string? seriesTimerId, [FromQuery] Guid? librarySeriesId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool enableTotalRecordCount = true) { userId = RequestHelpers.GetUserId(User, userId); @@ -730,9 +730,9 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? isSports, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) { diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 2d917d61f..cbbaaddbf 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -65,7 +65,7 @@ public class MoviesController : BaseJellyfinApiController public ActionResult> GetMovieRecommendations( [FromQuery] Guid? userId, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] int categoryLimit = 5, [FromQuery] int itemLimit = 8) { diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 5411baa3e..e8bc8f265 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -76,18 +76,18 @@ public class MusicGenresController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] Guid? userId, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 6ca308601..b0c493fbe 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -67,14 +67,14 @@ public class PersonsController : BaseJellyfinApiController public ActionResult> GetPersons( [FromQuery] int? limit, [FromQuery] string? searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] excludePersonTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes, [FromQuery] Guid? appearsInItemId, [FromQuery] Guid? userId, [FromQuery] bool? enableImages = true) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 1ab36ccc6..ec5fdab38 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -76,7 +76,7 @@ public class PlaylistsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CreatePlaylist( [FromQuery, ParameterObsolete] string? name, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder)), ParameterObsolete] IReadOnlyList ids, [FromQuery, ParameterObsolete] Guid? userId, [FromQuery, ParameterObsolete] MediaType? mediaType, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest) @@ -370,7 +370,7 @@ public class PlaylistsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task AddItemToPlaylist( [FromRoute, Required] Guid playlistId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); @@ -446,7 +446,7 @@ public class PlaylistsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task RemoveItemFromPlaylist( [FromRoute, Required] string playlistId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] entryIds) { var callingUserId = User.GetUserId(); @@ -493,11 +493,11 @@ public class PlaylistsController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes) { var callingUserId = userId ?? User.GetUserId(); var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId); diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 8bae6fb9b..ecf2335ba 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -84,9 +84,9 @@ public class SearchController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery] Guid? userId, [FromQuery, Required] string searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes, [FromQuery] Guid? parentId, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 2f9e9f091..9886d03de 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -122,7 +122,7 @@ public class SessionController : BaseJellyfinApiController public async Task Play( [FromRoute, Required] string sessionId, [FromQuery, Required] PlayCommand playCommand, - [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds, + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] itemIds, [FromQuery] long? startPositionTicks, [FromQuery] string? mediaSourceId, [FromQuery] int? audioStreamIndex, @@ -347,8 +347,8 @@ public class SessionController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task PostCapabilities( [FromQuery] string? id, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] playableMediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsPersistentIdentifier = true) { diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 708fc7436..43c5384dc 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -73,13 +73,13 @@ public class StudiosController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] Guid? userId, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index ad625cc6e..9b56d0849 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -59,8 +59,8 @@ public class SuggestionsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSuggestions( [FromQuery] Guid? userId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaType, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] type, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool enableTotalRecordCount = false) @@ -115,8 +115,8 @@ public class SuggestionsController : BaseJellyfinApiController [ApiExplorerSettings(IgnoreApi = true)] public ActionResult> GetSuggestionsLegacy( [FromRoute, Required] Guid userId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaType, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] type, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool enableTotalRecordCount = false) diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index d7d0cc454..7ee4396bb 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -130,8 +130,8 @@ public class TrailersController : BaseJellyfinApiController [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, [FromQuery] bool? is4K, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes, [FromQuery] bool? isMissing, [FromQuery] bool? isUnaired, [FromQuery] double? minCommunityRating, @@ -149,41 +149,41 @@ public class TrailersController : BaseJellyfinApiController [FromQuery] bool? isNews, [FromQuery] bool? isKids, [FromQuery] bool? isSports, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? recursive, [FromQuery] string? searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? isPlayed, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] studios, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] artists, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] albums, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes, [FromQuery] string? minOfficialRating, [FromQuery] bool? isLocked, [FromQuery] bool? isPlaceHolder, @@ -194,12 +194,12 @@ public class TrailersController : BaseJellyfinApiController [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] bool? is3D, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 914ccd7f9..df46c2dac 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -77,12 +77,12 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] Guid? seriesId, [FromQuery] Guid? parentId, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, @@ -143,11 +143,11 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] Guid? parentId, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { userId = RequestHelpers.GetUserId(User, userId); @@ -208,7 +208,7 @@ public class TvShowsController : BaseJellyfinApiController public ActionResult> GetEpisodes( [FromRoute, Required] Guid seriesId, [FromQuery] Guid? userId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] int? season, [FromQuery] Guid? seasonId, [FromQuery] bool? isMissing, @@ -218,7 +218,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] ItemSortBy? sortBy) { @@ -332,13 +332,13 @@ public class TvShowsController : BaseJellyfinApiController public ActionResult> GetSeasons( [FromRoute, Required] Guid seriesId, [FromQuery] Guid? userId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery] bool? isSpecialSeason, [FromQuery] bool? isMissing, [FromQuery] Guid? adjacentTo, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { userId = RequestHelpers.GetUserId(User, userId); diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 4fe2d52da..a5b5fde62 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -98,7 +98,7 @@ public class UniversalAudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetUniversalAudioStream( [FromRoute, Required] Guid itemId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] container, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 7cce13e42..6cc2b4244 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -523,12 +523,12 @@ public class UserLibraryController : BaseJellyfinApiController public ActionResult> GetLatestMedia( [FromQuery] Guid? userId, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isPlayed, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] int limit = 20, [FromQuery] bool groupItems = true) @@ -608,12 +608,12 @@ public class UserLibraryController : BaseJellyfinApiController public ActionResult> GetLatestMediaLegacy( [FromRoute, Required] Guid userId, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isPlayed, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] int limit = 20, [FromQuery] bool groupItems = true) diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index e24f78a88..64b2dffb3 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -66,7 +66,7 @@ public class UserViewsController : BaseJellyfinApiController public QueryResult GetUserViews( [FromQuery] Guid? userId, [FromQuery] bool? includeExternalContent, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] CollectionType?[] presetViews, [FromQuery] bool includeHidden = false) { userId = RequestHelpers.GetUserId(User, userId); @@ -110,7 +110,7 @@ public class UserViewsController : BaseJellyfinApiController public QueryResult GetUserViewsLegacy( [FromRoute, Required] Guid userId, [FromQuery] bool? includeExternalContent, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] CollectionType?[] presetViews, [FromQuery] bool includeHidden = false) => GetUserViews(userId, includeExternalContent, presetViews, includeHidden); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 8348fd937..6f18c1603 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -184,7 +184,7 @@ public class VideosController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + public async Task MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids) { var userId = User.GetUserId(); var items = ids diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index e709e43e2..2b32ae728 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -72,16 +72,16 @@ public class YearsController : BaseJellyfinApiController public ActionResult> GetYears( [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery] Guid? userId, [FromQuery] bool recursive = true, [FromQuery] bool? enableImages = true) diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs deleted file mode 100644 index 3e3604b2a..000000000 --- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs +++ /dev/null @@ -1,89 +0,0 @@ -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; - -/// -/// Comma delimited array model binder. -/// Returns an empty array of specified type if there is no query parameter. -/// -public class CommaDelimitedArrayModelBinder : IModelBinder -{ - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - public CommaDelimitedArrayModelBinder(ILogger logger) - { - _logger = logger; - } - - /// - 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 is not 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 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.LogDebug(e, "Error converting value."); - } - } - - var typedValues = Array.CreateInstance(elementType, convertedCount); - var typedValueIndex = 0; - for (var i = 0; i < parsedValues.Length; i++) - { - if (parsedValues[i] is not null) - { - typedValues.SetValue(parsedValues[i], typedValueIndex); - typedValueIndex++; - } - } - - return typedValues; - } -} diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedCollectionModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedCollectionModelBinder.cs new file mode 100644 index 000000000..25b84cbcc --- /dev/null +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedCollectionModelBinder.cs @@ -0,0 +1,89 @@ +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; + +/// +/// Comma delimited collection model binder. +/// Returns an empty array of specified type if there is no query parameter. +/// +public class CommaDelimitedCollectionModelBinder : IModelBinder +{ + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public CommaDelimitedCollectionModelBinder(ILogger logger) + { + _logger = logger; + } + + /// + 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 is not 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 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.LogDebug(e, "Error converting value."); + } + } + + var typedValues = Array.CreateInstance(elementType, convertedCount); + var typedValueIndex = 0; + for (var i = 0; i < parsedValues.Length; i++) + { + if (parsedValues[i] is not null) + { + typedValues.SetValue(parsedValues[i], typedValueIndex); + typedValueIndex++; + } + } + + return typedValues; + } +} diff --git a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs deleted file mode 100644 index ae9f0a8cd..000000000 --- a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs +++ /dev/null @@ -1,89 +0,0 @@ -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; - -/// -/// Comma delimited array model binder. -/// Returns an empty array of specified type if there is no query parameter. -/// -public class PipeDelimitedArrayModelBinder : IModelBinder -{ - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - public PipeDelimitedArrayModelBinder(ILogger logger) - { - _logger = logger; - } - - /// - 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 is not 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 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.LogDebug(e, "Error converting value."); - } - } - - var typedValues = Array.CreateInstance(elementType, convertedCount); - var typedValueIndex = 0; - for (var i = 0; i < parsedValues.Length; i++) - { - if (parsedValues[i] is not null) - { - typedValues.SetValue(parsedValues[i], typedValueIndex); - typedValueIndex++; - } - } - - return typedValues; - } -} diff --git a/Jellyfin.Api/ModelBinders/PipeDelimitedCollectionModelBinder.cs b/Jellyfin.Api/ModelBinders/PipeDelimitedCollectionModelBinder.cs new file mode 100644 index 000000000..7d0fb2e19 --- /dev/null +++ b/Jellyfin.Api/ModelBinders/PipeDelimitedCollectionModelBinder.cs @@ -0,0 +1,89 @@ +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; + +/// +/// Comma delimited collection model binder. +/// Returns an empty collection of specified type if there is no query parameter. +/// +public class PipeDelimitedCollectionModelBinder : IModelBinder +{ + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public PipeDelimitedCollectionModelBinder(ILogger logger) + { + _logger = logger; + } + + /// + 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 is not 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 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.LogDebug(e, "Error converting value."); + } + } + + var typedValues = Array.CreateInstance(elementType, convertedCount); + var typedValueIndex = 0; + for (var i = 0; i < parsedValues.Length; i++) + { + if (parsedValues[i] is not null) + { + typedValues.SetValue(parsedValues[i], typedValueIndex); + typedValueIndex++; + } + } + + return typedValues; + } +} diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index 190d90681..dece66426 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs @@ -17,7 +17,7 @@ public class GetProgramsDto /// /// Gets or sets the channels to return guide information for. /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public IReadOnlyList? ChannelIds { get; set; } /// @@ -93,25 +93,25 @@ public class GetProgramsDto /// /// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate. /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public IReadOnlyList? SortBy { get; set; } /// /// Gets or sets sort order. /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public IReadOnlyList? SortOrder { get; set; } /// /// Gets or sets the genres to return guide information for. /// - [JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonPipeDelimitedCollectionConverterFactory))] public IReadOnlyList? Genres { get; set; } /// /// Gets or sets the genre ids to return guide information for. /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public IReadOnlyList? GenreIds { get; set; } /// @@ -133,7 +133,7 @@ public class GetProgramsDto /// /// Gets or sets the image types to include in the output. /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public IReadOnlyList? EnableImageTypes { get; set; } /// @@ -154,6 +154,6 @@ public class GetProgramsDto /// /// Gets or sets specify additional fields of information to return in the output. /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public IReadOnlyList? Fields { get; set; } } diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index 61a3f2ed6..891d758c4 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -20,7 +20,7 @@ public class CreatePlaylistDto /// /// Gets or sets item ids to add to the playlist. /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public IReadOnlyList Ids { get; set; } = []; /// diff --git a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs index 80e20995c..339a0d5d2 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs @@ -19,7 +19,7 @@ public class UpdatePlaylistDto /// /// Gets or sets item ids of the playlist. /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public IReadOnlyList? Ids { get; set; } /// diff --git a/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs index 5963ed270..d481593cd 100644 --- a/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs +++ b/MediaBrowser.Model/Dto/ClientCapabilitiesDto.cs @@ -15,13 +15,13 @@ public class ClientCapabilitiesDto /// /// Gets or sets the list of playable media types. /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public IReadOnlyList PlayableMediaTypes { get; set; } = []; /// /// Gets or sets the list of supported commands. /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public IReadOnlyList SupportedCommands { get; set; } = []; /// diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs deleted file mode 100644 index ccbc296fd..000000000 --- a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Jellyfin.Extensions.Json.Converters -{ - /// - /// Convert comma delimited string to array of type. - /// - /// Type to convert to. - public sealed class JsonCommaDelimitedArrayConverter : JsonDelimitedArrayConverter - { - /// - /// Initializes a new instance of the class. - /// - public JsonCommaDelimitedArrayConverter() : base() - { - } - - /// - protected override char Delimiter => ','; - } -} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs deleted file mode 100644 index a95e493db..000000000 --- a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedArrayConverterFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Jellyfin.Extensions.Json.Converters -{ - /// - /// Json comma delimited array converter factory. - /// - /// - /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. - /// - public class JsonCommaDelimitedArrayConverterFactory : JsonConverterFactory - { - /// - public override bool CanConvert(Type typeToConvert) - { - return true; - } - - /// - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; - return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType)); - } - } -} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverter.cs new file mode 100644 index 000000000..b1946143d --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverter.cs @@ -0,0 +1,19 @@ +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Convert comma delimited string to collection of type. + /// + /// Type to convert to. + public sealed class JsonCommaDelimitedCollectionConverter : JsonDelimitedCollectionConverter + { + /// + /// Initializes a new instance of the class. + /// + public JsonCommaDelimitedCollectionConverter() : base() + { + } + + /// + protected override char Delimiter => ','; + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverterFactory.cs new file mode 100644 index 000000000..daa79b2b5 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonCommaDelimitedCollectionConverterFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Json comma delimited collection converter factory. + /// + /// + /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. + /// + public class JsonCommaDelimitedCollectionConverterFactory : JsonConverterFactory + { + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsArray + || (typeToConvert.IsGenericType + && (typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyCollection<>)) || typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyList<>)))); + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; + return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedCollectionConverter<>).MakeGenericType(structType)); + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs deleted file mode 100644 index 7472f9c66..000000000 --- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Jellyfin.Extensions.Json.Converters -{ - /// - /// Convert delimited string to array of type. - /// - /// Type to convert to. - public abstract class JsonDelimitedArrayConverter : JsonConverter - { - private readonly TypeConverter _typeConverter; - - /// - /// Initializes a new instance of the class. - /// - protected JsonDelimitedArrayConverter() - { - _typeConverter = TypeDescriptor.GetConverter(typeof(T)); - } - - /// - /// Gets the array delimiter. - /// - protected virtual char Delimiter { get; } - - /// - public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - { - // null got handled higher up the call stack - var stringEntries = reader.GetString()!.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries); - if (stringEntries.Length == 0) - { - return []; - } - - var typedValues = new List(); - for (var i = 0; i < stringEntries.Length; i++) - { - try - { - var parsedValue = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()); - if (parsedValue is not null) - { - typedValues.Add((T)parsedValue); - } - } - catch (FormatException) - { - // Ignore unconvertible inputs - } - } - - return typedValues.ToArray(); - } - - return JsonSerializer.Deserialize(ref reader, options); - } - - /// - public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options) - { - if (value is not null) - { - writer.WriteStartArray(); - if (value.Length > 0) - { - foreach (var it in value) - { - if (it is not null) - { - writer.WriteStringValue(it.ToString()); - } - } - } - - writer.WriteEndArray(); - } - else - { - writer.WriteNullValue(); - } - } - } -} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedCollectionConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedCollectionConverter.cs new file mode 100644 index 000000000..fe85d7f73 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedCollectionConverter.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Convert delimited string to array of type. + /// + /// Type to convert to. + public abstract class JsonDelimitedCollectionConverter : JsonConverter> + { + private readonly TypeConverter _typeConverter; + + /// + /// Initializes a new instance of the class. + /// + protected JsonDelimitedCollectionConverter() + { + _typeConverter = TypeDescriptor.GetConverter(typeof(T)); + } + + /// + /// Gets the array delimiter. + /// + protected virtual char Delimiter { get; } + + /// + public override IReadOnlyCollection? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + // null got handled higher up the call stack + var stringEntries = reader.GetString()!.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries); + if (stringEntries.Length == 0) + { + return []; + } + + var typedValues = new List(); + for (var i = 0; i < stringEntries.Length; i++) + { + try + { + var parsedValue = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()); + if (parsedValue is not null) + { + typedValues.Add((T)parsedValue); + } + } + catch (FormatException) + { + // Ignore unconvertible inputs + } + } + + if (typeToConvert.IsArray) + { + return typedValues.ToArray(); + } + + return typedValues; + } + + return JsonSerializer.Deserialize(ref reader, options); + } + + /// + public override void Write(Utf8JsonWriter writer, IReadOnlyCollection? value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, options); + } + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs deleted file mode 100644 index 55720ee4f..000000000 --- a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Jellyfin.Extensions.Json.Converters -{ - /// - /// Convert Pipe delimited string to array of type. - /// - /// Type to convert to. - public sealed class JsonPipeDelimitedArrayConverter : JsonDelimitedArrayConverter - { - /// - /// Initializes a new instance of the class. - /// - public JsonPipeDelimitedArrayConverter() : base() - { - } - - /// - protected override char Delimiter => '|'; - } -} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs deleted file mode 100644 index ae9e1f67a..000000000 --- a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Jellyfin.Extensions.Json.Converters -{ - /// - /// Json Pipe delimited array converter factory. - /// - /// - /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. - /// - public class JsonPipeDelimitedArrayConverterFactory : JsonConverterFactory - { - /// - public override bool CanConvert(Type typeToConvert) - { - return true; - } - - /// - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; - return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType)); - } - } -} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverter.cs new file mode 100644 index 000000000..57378a360 --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverter.cs @@ -0,0 +1,19 @@ +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Convert Pipe delimited string to array of type. + /// + /// Type to convert to. + public sealed class JsonPipeDelimitedCollectionConverter : JsonDelimitedCollectionConverter + { + /// + /// Initializes a new instance of the class. + /// + public JsonPipeDelimitedCollectionConverter() : base() + { + } + + /// + protected override char Delimiter => '|'; + } +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverterFactory.cs new file mode 100644 index 000000000..f487fcaca --- /dev/null +++ b/src/Jellyfin.Extensions/Json/Converters/JsonPipeDelimitedCollectionConverterFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Jellyfin.Extensions.Json.Converters +{ + /// + /// Json Pipe delimited collection converter factory. + /// + /// + /// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow. + /// + public class JsonPipeDelimitedCollectionConverterFactory : JsonConverterFactory + { + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsArray + || (typeToConvert.IsGenericType + && (typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyCollection<>)) || typeToConvert.GetGenericTypeDefinition().IsAssignableFrom(typeof(IReadOnlyList<>)))); + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0]; + return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedCollectionConverter<>).MakeGenericType(structType)); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs deleted file mode 100644 index e37c9d91f..000000000 --- a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs +++ /dev/null @@ -1,230 +0,0 @@ -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 CommaDelimitedArrayModelBinderTests - { - [Fact] - public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery() - { - var queryParamName = "test"; - IReadOnlyList queryParamValues = new[] { "lol", "xd" }; - var queryParamString = "lol,xd"; - var queryParamType = typeof(string[]); - - var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object?.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedIntArrayQuery() - { - var queryParamName = "test"; - IReadOnlyList queryParamValues = new[] { 42, 0 }; - var queryParamString = "42,0"; - var queryParamType = typeof(int[]); - - var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQuery() - { - var queryParamName = "test"; - IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; - var queryParamString = "How,Much"; - var queryParamType = typeof(TestType[]); - - var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQueryWithDoubleCommas() - { - var queryParamName = "test"; - IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; - var queryParamString = "How,,Much"; - var queryParamType = typeof(TestType[]); - - var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery() - { - var queryParamName = "test"; - IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; - var queryParamString1 = "How"; - var queryParamString2 = "Much"; - var queryParamType = typeof(TestType[]); - - var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); - - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary - { - { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, - }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery() - { - var queryParamName = "test"; - IReadOnlyList queryParamValues = Array.Empty(); - var queryParamType = typeof(TestType[]); - - var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); - - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary - { - { queryParamName, new StringValues(value: null) }, - }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_EnumArrayQuery_BindValidOnly() - { - var queryParamName = "test"; - var queryParamString = "🔥,😢"; - var queryParamType = typeof(IReadOnlyList); - - var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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); - var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model; - Assert.NotNull(listResult); - Assert.Empty(listResult); - } - - [Fact] - public async Task BindModelAsync_EnumArrayQuery_BindValidOnly_2() - { - var queryParamName = "test"; - var queryParamString1 = "How"; - var queryParamString2 = "😱"; - var queryParamType = typeof(IReadOnlyList); - - var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); - - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary - { - { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, - }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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); - var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model; - Assert.NotNull(listResult); - Assert.Single(listResult); - } - } -} diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedCollectionModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedCollectionModelBinderTests.cs new file mode 100644 index 000000000..e6b9acfe1 --- /dev/null +++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedCollectionModelBinderTests.cs @@ -0,0 +1,230 @@ +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 CommaDelimitedCollectionModelBinderTests + { + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = new[] { "lol", "xd" }; + var queryParamString = "lol,xd"; + var queryParamType = typeof(string[]); + + var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object?.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedIntArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = new[] { 42, 0 }; + var queryParamString = "42,0"; + var queryParamType = typeof(int[]); + + var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; + var queryParamString = "How,Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQueryWithDoubleCommas() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; + var queryParamString = "How,,Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; + var queryParamString1 = "How"; + var queryParamString2 = "Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger()); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary + { + { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = Array.Empty(); + var queryParamType = typeof(TestType[]); + + var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger()); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary + { + { queryParamName, new StringValues(value: null) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_EnumArrayQuery_BindValidOnly() + { + var queryParamName = "test"; + var queryParamString = "🔥,😢"; + var queryParamType = typeof(IReadOnlyList); + + var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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); + var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model; + Assert.NotNull(listResult); + Assert.Empty(listResult); + } + + [Fact] + public async Task BindModelAsync_EnumArrayQuery_BindValidOnly_2() + { + var queryParamName = "test"; + var queryParamString1 = "How"; + var queryParamString2 = "😱"; + var queryParamType = typeof(IReadOnlyList); + + var modelBinder = new CommaDelimitedCollectionModelBinder(new NullLogger()); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary + { + { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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); + var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model; + Assert.NotNull(listResult); + Assert.Single(listResult); + } + } +} diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs deleted file mode 100644 index 7c05ee036..000000000 --- a/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs +++ /dev/null @@ -1,230 +0,0 @@ -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 queryParamValues = new[] { "lol", "xd" }; - var queryParamString = "lol|xd"; - var queryParamType = typeof(string[]); - - var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger()); - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object?.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_CorrectlyBindsValidDelimitedIntArrayQuery() - { - var queryParamName = "test"; - IReadOnlyList queryParamValues = new[] { 42, 0 }; - var queryParamString = "42|0"; - var queryParamType = typeof(int[]); - - var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger()); - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedEnumArrayQuery() - { - var queryParamName = "test"; - IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; - var queryParamString = "How|Much"; - var queryParamType = typeof(TestType[]); - - var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger()); - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedEnumArrayQueryWithDoublePipes() - { - var queryParamName = "test"; - IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; - var queryParamString = "How||Much"; - var queryParamType = typeof(TestType[]); - - var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger()); - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery() - { - var queryParamName = "test"; - IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; - var queryParamString1 = "How"; - var queryParamString2 = "Much"; - var queryParamType = typeof(TestType[]); - - var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger()); - - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary - { - { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, - }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery() - { - var queryParamName = "test"; - IReadOnlyList queryParamValues = Array.Empty(); - var queryParamType = typeof(TestType[]); - - var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger()); - - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary - { - { queryParamName, new StringValues(value: null) }, - }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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?)bindingContextMock.Object.Result.Model, queryParamValues); - } - - [Fact] - public async Task BindModelAsync_EnumArrayQuery_BindValidOnly() - { - var queryParamName = "test"; - var queryParamString = "🔥|😢"; - var queryParamType = typeof(IReadOnlyList); - - var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger()); - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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); - var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model; - Assert.NotNull(listResult); - Assert.Empty(listResult); - } - - [Fact] - public async Task BindModelAsync_EnumArrayQuery_BindValidOnly_2() - { - var queryParamName = "test"; - var queryParamString1 = "How"; - var queryParamString2 = "😱"; - var queryParamType = typeof(IReadOnlyList); - - var modelBinder = new PipeDelimitedArrayModelBinder(new NullLogger()); - - var valueProvider = new QueryStringValueProvider( - new BindingSource(string.Empty, string.Empty, false, false), - new QueryCollection(new Dictionary - { - { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, - }), - CultureInfo.InvariantCulture); - var bindingContextMock = new Mock(); - 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); - var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model; - Assert.NotNull(listResult); - Assert.Single(listResult); - } - } -} diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedCollectionModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedCollectionModelBinderTests.cs new file mode 100644 index 000000000..941f4f12c --- /dev/null +++ b/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedCollectionModelBinderTests.cs @@ -0,0 +1,230 @@ +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 PipeDelimitedCollectionModelBinderTests + { + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedStringArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = new[] { "lol", "xd" }; + var queryParamString = "lol|xd"; + var queryParamType = typeof(string[]); + + var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object?.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidDelimitedIntArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = new[] { 42, 0 }; + var queryParamString = "42|0"; + var queryParamType = typeof(int[]); + + var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedEnumArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; + var queryParamString = "How|Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidPipeDelimitedEnumArrayQueryWithDoublePipes() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; + var queryParamString = "How||Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = new[] { TestType.How, TestType.Much }; + var queryParamString1 = "How"; + var queryParamString2 = "Much"; + var queryParamType = typeof(TestType[]); + + var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger()); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary + { + { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery() + { + var queryParamName = "test"; + IReadOnlyList queryParamValues = Array.Empty(); + var queryParamType = typeof(TestType[]); + + var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger()); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary + { + { queryParamName, new StringValues(value: null) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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?)bindingContextMock.Object.Result.Model, queryParamValues); + } + + [Fact] + public async Task BindModelAsync_EnumArrayQuery_BindValidOnly() + { + var queryParamName = "test"; + var queryParamString = "🔥|😢"; + var queryParamType = typeof(IReadOnlyList); + + var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger()); + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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); + var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model; + Assert.NotNull(listResult); + Assert.Empty(listResult); + } + + [Fact] + public async Task BindModelAsync_EnumArrayQuery_BindValidOnly_2() + { + var queryParamName = "test"; + var queryParamString1 = "How"; + var queryParamString2 = "😱"; + var queryParamType = typeof(IReadOnlyList); + + var modelBinder = new PipeDelimitedCollectionModelBinder(new NullLogger()); + + var valueProvider = new QueryStringValueProvider( + new BindingSource(string.Empty, string.Empty, false, false), + new QueryCollection(new Dictionary + { + { queryParamName, new StringValues(new[] { queryParamString1, queryParamString2 }) }, + }), + CultureInfo.InvariantCulture); + var bindingContextMock = new Mock(); + 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); + var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model; + Assert.NotNull(listResult); + Assert.Single(listResult); + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs deleted file mode 100644 index d247b8cb1..000000000 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedArrayTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using Jellyfin.Extensions.Tests.Json.Models; -using MediaBrowser.Model.Session; -using Xunit; - -namespace Jellyfin.Extensions.Tests.Json.Converters -{ - public class JsonCommaDelimitedArrayTests - { - private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() - { - Converters = - { - new JsonStringEnumConverter() - } - }; - - [Fact] - public void Deserialize_String_Null_Success() - { - var value = JsonSerializer.Deserialize>(@"{ ""Value"": null }", _jsonOptions); - Assert.Null(value?.Value); - } - - [Fact] - public void Deserialize_Empty_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = Array.Empty() - }; - - var value = JsonSerializer.Deserialize>(@"{ ""Value"": """" }", _jsonOptions); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public void Deserialize_String_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = ["a", "b", "c"] - }; - - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public void Deserialize_String_Space_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = ["a", "b", "c"] - }; - - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public void Deserialize_GenericCommandType_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] - }; - - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public void Deserialize_GenericCommandType_EmptyEntry_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] - }; - - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", _jsonOptions); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public void Deserialize_GenericCommandType_Invalid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] - }; - - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,TotallyNotAValidCommand,MoveDown"" }", _jsonOptions); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public void Deserialize_GenericCommandType_Space_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] - }; - - var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public void Deserialize_String_Array_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = ["a", "b", "c"] - }; - - var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions); - Assert.Equal(desiredValue.Value, value?.Value); - } - - [Fact] - public void Deserialize_GenericCommandType_Array_Valid_Success() - { - var desiredValue = new GenericBodyArrayModel - { - Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] - }; - - var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); - Assert.Equal(desiredValue.Value, value?.Value); - } - } -} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedCollectionTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedCollectionTests.cs new file mode 100644 index 000000000..83f917c17 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedCollectionTests.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Jellyfin.Extensions.Tests.Json.Models; +using MediaBrowser.Model.Session; +using Xunit; + +namespace Jellyfin.Extensions.Tests.Json.Converters +{ + public class JsonCommaDelimitedCollectionTests + { + private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions() + { + Converters = + { + new JsonStringEnumConverter() + } + }; + + [Fact] + public void Deserialize_String_Null_Success() + { + var value = JsonSerializer.Deserialize>(@"{ ""Value"": null }", _jsonOptions); + Assert.Null(value?.Value); + } + + [Fact] + public void Deserialize_Empty_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = Array.Empty() + }; + + var value = JsonSerializer.Deserialize>(@"{ ""Value"": """" }", _jsonOptions); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public void Deserialize_EmptyList_Success() + { + var desiredValue = new GenericBodyListModel + { + Value = [] + }; + + Assert.Throws(() => JsonSerializer.Deserialize>(@"{ ""Value"": """" }", _jsonOptions)); + } + + [Fact] + public void Deserialize_EmptyIReadOnlyList_Success() + { + var desiredValue = new GenericBodyIReadOnlyListModel + { + Value = [] + }; + + var value = JsonSerializer.Deserialize>(@"{ ""Value"": """" }", _jsonOptions); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public void Deserialize_String_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = ["a", "b", "c"] + }; + + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public void Deserialize_StringList_Valid_Success() + { + var desiredValue = new GenericBodyListModel + { + Value = ["a", "b", "c"] + }; + + Assert.Throws(() => JsonSerializer.Deserialize>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions)); + } + + [Fact] + public void Deserialize_String_Space_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = ["a", "b", "c"] + }; + + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public void Deserialize_GenericCommandType_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] + }; + + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public void Deserialize_GenericCommandType_EmptyEntry_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] + }; + + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", _jsonOptions); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public void Deserialize_GenericCommandType_Invalid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] + }; + + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp,TotallyNotAValidCommand,MoveDown"" }", _jsonOptions); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public void Deserialize_GenericCommandType_Space_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] + }; + + var value = JsonSerializer.Deserialize>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public void Deserialize_String_Array_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = ["a", "b", "c"] + }; + + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public void Deserialize_GenericCommandType_Array_Valid_Success() + { + var desiredValue = new GenericBodyArrayModel + { + Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown] + }; + + var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); + Assert.Equal(desiredValue.Value, value?.Value); + } + + [Fact] + public void Serialize_GenericCommandType_ReadOnlyArray_Valid_Success() + { + var valueToSerialize = new GenericBodyIReadOnlyCollectionModel + { + Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }.AsReadOnly() + }; + + string value = JsonSerializer.Serialize>(valueToSerialize, _jsonOptions); + Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value); + } + + [Fact] + public void Serialize_GenericCommandType_ImmutableArrayArray_Valid_Success() + { + var valueToSerialize = new GenericBodyIReadOnlyCollectionModel + { + Value = ImmutableArray.Create(new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }) + }; + + string value = JsonSerializer.Serialize>(valueToSerialize, _jsonOptions); + Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value); + } + + [Fact] + public void Serialize_GenericCommandType_List_Valid_Success() + { + var valueToSerialize = new GenericBodyIReadOnlyListModel + { + Value = new List { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + string value = JsonSerializer.Serialize>(valueToSerialize, _jsonOptions); + Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value); + } + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs index 9b977b9a5..26989d59b 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Jellyfin.Extensions.Tests.Json.Models; @@ -87,5 +88,17 @@ namespace Jellyfin.Extensions.Tests.Json.Converters var value = JsonSerializer.Deserialize>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions); Assert.Equal(desiredValue.Value, value?.Value); } + + [Fact] + public void Serialize_GenericCommandType_IReadOnlyList_Valid_Success() + { + var valueToSerialize = new GenericBodyIReadOnlyListModel + { + Value = new List { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown } + }; + + string value = JsonSerializer.Serialize>(valueToSerialize, _jsonOptions); + Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value); + } } } diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs index 76669ea19..a698c9c92 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyArrayModel.cs @@ -14,7 +14,7 @@ namespace Jellyfin.Extensions.Tests.Json.Models /// Gets or sets the value. /// [SuppressMessage("Microsoft.Performance", "CA1819:Properties should not return arrays", MessageId = "Value", Justification = "Imported from ServiceStack")] - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public T[] Value { get; set; } = default!; } } diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyCollectionModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyCollectionModel.cs new file mode 100644 index 000000000..14cbc0f50 --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyCollectionModel.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Jellyfin.Extensions.Json.Converters; + +namespace Jellyfin.Extensions.Tests.Json.Models +{ + /// + /// The generic body IReadOnlyCollection model. + /// + /// The value type. + public sealed class GenericBodyIReadOnlyCollectionModel + { + /// + /// Gets or sets the value. + /// + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] + public IReadOnlyCollection Value { get; set; } = default!; + } +} diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs index 7e6b97afe..eaa06a5dd 100644 --- a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs +++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyIReadOnlyListModel.cs @@ -13,7 +13,7 @@ namespace Jellyfin.Extensions.Tests.Json.Models /// /// Gets or sets the value. /// - [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] public IReadOnlyList Value { get; set; } = default!; } } diff --git a/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyListModel.cs b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyListModel.cs new file mode 100644 index 000000000..463f9922f --- /dev/null +++ b/tests/Jellyfin.Extensions.Tests/Json/Models/GenericBodyListModel.cs @@ -0,0 +1,22 @@ +#pragma warning disable CA1002 // Do not expose generic lists +#pragma warning disable CA2227 // Collection properties should be read only + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Jellyfin.Extensions.Json.Converters; + +namespace Jellyfin.Extensions.Tests.Json.Models +{ + /// + /// The generic body List model. + /// + /// The value type. + public sealed class GenericBodyListModel + { + /// + /// Gets or sets the value. + /// + [JsonConverter(typeof(JsonCommaDelimitedCollectionConverterFactory))] + public List Value { get; set; } = default!; + } +} -- cgit v1.2.3