From 9e13003fbb4e7e66fcff197778659c981f21e979 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 9 Feb 2025 12:22:48 -0500 Subject: Backport pull request #13469 from jellyfin/release-10.10.z Fix SchedulesDirect image prefetching Original-merge: 21e398ba0c1c1715d26ba988fabc576bb8e48514 Merged-by: crobibero Backported-by: Bond_009 --- src/Jellyfin.LiveTv/Guide/GuideManager.cs | 104 ++++++++++++------------------ 1 file changed, 43 insertions(+), 61 deletions(-) (limited to 'src') diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs index b75cc0fb2..1b896133f 100644 --- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs +++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities.Libraries; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using Jellyfin.LiveTv.Configuration; @@ -210,7 +209,7 @@ public class GuideManager : IGuideManager progress.Report(15); numComplete = 0; - var programs = new List(); + var programIds = new List(); var channels = new List(); var guideDays = GetGuideDays(); @@ -243,8 +242,8 @@ public class GuideManager : IGuideManager DtoOptions = new DtoOptions(true) }).Cast().ToDictionary(i => i.Id); - var newPrograms = new List(); - var updatedPrograms = new List(); + var newPrograms = new List(); + var updatedPrograms = new List(); foreach (var program in channelPrograms) { @@ -252,14 +251,14 @@ public class GuideManager : IGuideManager var id = programItem.Id; if (isNew) { - newPrograms.Add(id); + newPrograms.Add(programItem); } else if (isUpdated) { - updatedPrograms.Add(id); + updatedPrograms.Add(programItem); } - programs.Add(programItem); + programIds.Add(programItem.Id); isMovie |= program.IsMovie; isSeries |= program.IsSeries; @@ -276,21 +275,21 @@ public class GuideManager : IGuideManager if (newPrograms.Count > 0) { - var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList(); - _libraryManager.CreateOrUpdateItems(newProgramDtos, null, cancellationToken); + _libraryManager.CreateItems(newPrograms, currentChannel, cancellationToken); + + await PreCacheImages(newPrograms, maxCacheDate).ConfigureAwait(false); } if (updatedPrograms.Count > 0) { - var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList(); await _libraryManager.UpdateItemsAsync( - updatedProgramDtos, + updatedPrograms, currentChannel, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); - } - await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false); + await PreCacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false); + } currentChannel.IsMovie = isMovie; currentChannel.IsNews = isNews; @@ -326,7 +325,6 @@ public class GuideManager : IGuideManager } progress.Report(100); - var programIds = programs.Select(p => p.Id).ToList(); return new Tuple, List>(channels, programIds); } @@ -502,35 +500,27 @@ public class GuideManager : IGuideManager forceUpdate = true; } - var seriesId = info.SeriesId; - - if (!item.ParentId.Equals(channel.Id)) + var channelId = channel.Id; + if (!item.ParentId.Equals(channelId)) { + item.ParentId = channel.Id; forceUpdate = true; } - item.ParentId = channel.Id; - item.Audio = info.Audio; - item.ChannelId = channel.Id; - item.CommunityRating ??= info.CommunityRating; - if ((item.CommunityRating ?? 0).Equals(0)) - { - item.CommunityRating = null; - } - + item.ChannelId = channelId; + item.CommunityRating = info.CommunityRating; item.EpisodeTitle = info.EpisodeTitle; item.ExternalId = info.Id; - if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal)) + var seriesId = info.SeriesId; + if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.OrdinalIgnoreCase)) { + item.ExternalSeriesId = seriesId; forceUpdate = true; } - item.ExternalSeriesId = seriesId; - var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle); - if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle)) { item.SeriesName = info.Name; @@ -578,7 +568,6 @@ public class GuideManager : IGuideManager } item.Tags = tags.ToArray(); - item.Genres = info.Genres.ToArray(); if (info.IsHD ?? false) @@ -589,41 +578,35 @@ public class GuideManager : IGuideManager item.IsMovie = info.IsMovie; item.IsRepeat = info.IsRepeat; - if (item.IsSeries != isSeries) { + item.IsSeries = isSeries; forceUpdate = true; } - item.IsSeries = isSeries; - item.Name = info.Name; - item.OfficialRating ??= info.OfficialRating; - item.Overview ??= info.Overview; + item.OfficialRating = info.OfficialRating; + item.Overview = info.Overview; item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; - item.ProviderIds = info.ProviderIds; - foreach (var providerId in info.SeriesProviderIds) { info.ProviderIds["Series" + providerId.Key] = providerId.Value; } + item.ProviderIds = info.ProviderIds; if (item.StartDate != info.StartDate) { + item.StartDate = info.StartDate; forceUpdate = true; } - item.StartDate = info.StartDate; - if (item.EndDate != info.EndDate) { + item.EndDate = info.EndDate; forceUpdate = true; } - item.EndDate = info.EndDate; - item.ProductionYear = info.ProductionYear; - if (!isSeries || info.IsRepeat) { item.PremiereDate = info.OriginalAirDate; @@ -632,37 +615,35 @@ public class GuideManager : IGuideManager item.IndexNumber = info.EpisodeNumber; item.ParentIndexNumber = info.SeasonNumber; - forceUpdate = forceUpdate || UpdateImages(item, info); + forceUpdate |= UpdateImages(item, info); if (isNew) { item.OnMetadataChanged(); - return (item, isNew, false); + return (item, true, false); } - var isUpdated = false; - if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag)) + var isUpdated = forceUpdate; + var etag = info.Etag; + if (string.IsNullOrWhiteSpace(etag)) { isUpdated = true; } - else + else if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase)) { - var etag = info.Etag; - - if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase)) - { - item.SetProviderId(EtagKey, etag); - isUpdated = true; - } + item.SetProviderId(EtagKey, etag); + isUpdated = true; } if (isUpdated) { item.OnMetadataChanged(); + + return (item, false, true); } - return (item, isNew, isUpdated); + return (item, false, false); } private static bool UpdateImages(BaseItem item, ProgramInfo info) @@ -689,7 +670,7 @@ public class GuideManager : IGuideManager var newImagePath = imageType switch { ImageType.Primary => info.ImagePath, - _ => string.Empty + _ => null }; var newImageUrl = imageType switch { @@ -697,12 +678,12 @@ public class GuideManager : IGuideManager ImageType.Logo => info.LogoImageUrl, ImageType.Primary => info.ImageUrl, ImageType.Thumb => info.ThumbImageUrl, - _ => string.Empty + _ => null }; - var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false - || newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false; - if (!differentImage) + var sameImage = (currentImagePath?.Equals(newImageUrl, StringComparison.OrdinalIgnoreCase) ?? false) + || (currentImagePath?.Equals(newImagePath, StringComparison.OrdinalIgnoreCase) ?? false); + if (sameImage) { return false; } @@ -757,6 +738,7 @@ public class GuideManager : IGuideManager var imageInfo = program.ImageInfos[i]; if (!imageInfo.IsLocalFile) { + _logger.LogDebug("Caching image locally: {Url}", imageInfo.Path); try { program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal( -- cgit v1.2.3 From c9c90050d9cf7824e8b6341a8e1318ca62f0d0b4 Mon Sep 17 00:00:00 2001 From: IDisposable Date: Sun, 9 Feb 2025 12:22:49 -0500 Subject: Backport pull request #13504 from jellyfin/release-10.10.z Fix LiveTV Guide Backdrop image not updating Original-merge: 8544e7fc72a758ad3b89aafba197f5f25c5808b1 Merged-by: crobibero Backported-by: Bond_009 --- src/Jellyfin.LiveTv/Guide/GuideManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs index 1b896133f..ac59a6d12 100644 --- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs +++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs @@ -660,7 +660,9 @@ public class GuideManager : IGuideManager updated |= UpdateImage(ImageType.Logo, item, info); // Backdrop - return updated || UpdateImage(ImageType.Backdrop, item, info); + updated |= UpdateImage(ImageType.Backdrop, item, info); + + return updated; } private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info) -- cgit v1.2.3 From 1ebef57508dbc1628048b878b580a51ceb593ffd Mon Sep 17 00:00:00 2001 From: gnattu Date: Thu, 13 Feb 2025 09:49:58 -0500 Subject: Backport pull request #13532 from jellyfin/release-10.10.z Fix image encoding concurrency limit Original-merge: 3f539472f3a7c216a6c7d34fb947a144e249f154 Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 8 +++++++- src/Jellyfin.Drawing/ImageProcessor.cs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 1eef181cb..14cf869f9 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -122,7 +122,13 @@ namespace MediaBrowser.MediaEncoding.Encoder _jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options); _jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter()); - var semaphoreCount = 2 * Environment.ProcessorCount; + // Although the type is not nullable, this might still be null during unit tests + var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0; + if (semaphoreCount < 1) + { + semaphoreCount = Environment.ProcessorCount; + } + _thumbnailResourcePool = new(semaphoreCount); } diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 0bd3b8920..fcb315b3a 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -68,7 +68,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable var semaphoreCount = config.Configuration.ParallelImageEncodingLimit; if (semaphoreCount < 1) { - semaphoreCount = 2 * Environment.ProcessorCount; + semaphoreCount = Environment.ProcessorCount; } _parallelEncodingLimit = new(semaphoreCount); -- cgit v1.2.3 From 2db0750abbcb3994a6d6163652566fe5e0e7c7b7 Mon Sep 17 00:00:00 2001 From: Bond-009 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 'src') 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 From d18066f0f23f819efdef145a74ffb173df636b58 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sat, 22 Feb 2025 10:27:42 +0100 Subject: Remove GetMacAddresses from NetworkManager --- MediaBrowser.Common/Net/INetworkManager.cs | 6 ------ src/Jellyfin.Networking/Manager/NetworkManager.cs | 23 ----------------------- 2 files changed, 29 deletions(-) (limited to 'src') diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 78a391d36..d838144ff 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -94,12 +94,6 @@ namespace MediaBrowser.Common.Net /// IP address to use, or loopback address if all else fails. string GetBindAddress(string source, out int? port); - /// - /// Get a list of all the MAC addresses associated with active interfaces. - /// - /// List of MAC addresses. - IReadOnlyList GetMacAddresses(); - /// /// Returns true if the address is part of the user defined LAN. /// diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index dd01e9533..2fbcbf79c 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -49,11 +49,6 @@ public class NetworkManager : INetworkManager, IDisposable /// private bool _eventfire; - /// - /// List of all interface MAC addresses. - /// - private IReadOnlyList _macAddresses; - /// /// Dictionary containing interface addresses and their subnets. /// @@ -91,7 +86,6 @@ public class NetworkManager : INetworkManager, IDisposable _startupConfig = startupConfig; _initLock = new(); _interfaces = new List(); - _macAddresses = new List(); _publishedServerUrls = new List(); _networkEventLock = new(); _remoteAddressFilter = new List(); @@ -215,7 +209,6 @@ public class NetworkManager : INetworkManager, IDisposable /// /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. - /// Generate a list of all active mac addresses that aren't loopback addresses. /// private void InitializeInterfaces() { @@ -224,7 +217,6 @@ public class NetworkManager : INetworkManager, IDisposable _logger.LogDebug("Refreshing interfaces."); var interfaces = new List(); - var macAddresses = new List(); try { @@ -236,13 +228,6 @@ public class NetworkManager : INetworkManager, IDisposable try { var ipProperties = adapter.GetIPProperties(); - var mac = adapter.GetPhysicalAddress(); - - // Populate MAC list - if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && !PhysicalAddress.None.Equals(mac)) - { - macAddresses.Add(mac); - } // Populate interface list foreach (var info in ipProperties.UnicastAddresses) @@ -302,7 +287,6 @@ public class NetworkManager : INetworkManager, IDisposable _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count); _logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString())); - _macAddresses = macAddresses; _interfaces = interfaces; } } @@ -711,13 +695,6 @@ public class NetworkManager : INetworkManager, IDisposable return true; } - /// - public IReadOnlyList GetMacAddresses() - { - // Populated in construction - so always has values. - return _macAddresses; - } - /// public IReadOnlyList GetLoopbacks() { -- cgit v1.2.3