< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.ItemsController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/ItemsController.cs
Line coverage
28%
Covered lines: 90
Uncovered lines: 231
Coverable lines: 321
Total lines: 1173
Line coverage: 28%
Branch coverage
18%
Covered branches: 33
Total branches: 180
Branch coverage: 18.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/6/2026 - 12:14:09 AM Line coverage: 31% (82/264) Branch coverage: 23.9% (29/121) Total lines: 10723/14/2026 - 12:13:58 AM Line coverage: 33.1% (82/247) Branch coverage: 26.8% (29/108) Total lines: 10404/6/2026 - 12:13:55 AM Line coverage: 33.3% (83/249) Branch coverage: 27.2% (30/110) Total lines: 10434/27/2026 - 12:15:04 AM Line coverage: 33.3% (83/249) Branch coverage: 27.2% (30/110) Total lines: 10445/4/2026 - 12:15:16 AM Line coverage: 32.8% (85/259) Branch coverage: 26.6% (32/120) Total lines: 10605/13/2026 - 12:15:27 AM Line coverage: 32.8% (85/259) Branch coverage: 26.6% (32/120) Total lines: 10625/16/2026 - 12:15:55 AM Line coverage: 31% (85/274) Branch coverage: 22.8% (32/140) Total lines: 10975/20/2026 - 12:15:44 AM Line coverage: 31% (85/274) Branch coverage: 22.1% (31/140) Total lines: 10975/22/2026 - 12:15:17 AM Line coverage: 30.8% (86/279) Branch coverage: 20.9% (31/148) Total lines: 11055/24/2026 - 12:15:27 AM Line coverage: 30.7% (87/283) Branch coverage: 20.7% (32/154) Total lines: 11106/4/2026 - 12:15:59 AM Line coverage: 30.6% (87/284) Branch coverage: 21.1% (33/156) Total lines: 11146/8/2026 - 12:16:15 AM Line coverage: 27.8% (89/320) Branch coverage: 18.3% (33/180) Total lines: 11726/9/2026 - 12:16:23 AM Line coverage: 28% (90/321) Branch coverage: 18.3% (33/180) Total lines: 1173 3/6/2026 - 12:14:09 AM Line coverage: 31% (82/264) Branch coverage: 23.9% (29/121) Total lines: 10723/14/2026 - 12:13:58 AM Line coverage: 33.1% (82/247) Branch coverage: 26.8% (29/108) Total lines: 10404/6/2026 - 12:13:55 AM Line coverage: 33.3% (83/249) Branch coverage: 27.2% (30/110) Total lines: 10434/27/2026 - 12:15:04 AM Line coverage: 33.3% (83/249) Branch coverage: 27.2% (30/110) Total lines: 10445/4/2026 - 12:15:16 AM Line coverage: 32.8% (85/259) Branch coverage: 26.6% (32/120) Total lines: 10605/13/2026 - 12:15:27 AM Line coverage: 32.8% (85/259) Branch coverage: 26.6% (32/120) Total lines: 10625/16/2026 - 12:15:55 AM Line coverage: 31% (85/274) Branch coverage: 22.8% (32/140) Total lines: 10975/20/2026 - 12:15:44 AM Line coverage: 31% (85/274) Branch coverage: 22.1% (31/140) Total lines: 10975/22/2026 - 12:15:17 AM Line coverage: 30.8% (86/279) Branch coverage: 20.9% (31/148) Total lines: 11055/24/2026 - 12:15:27 AM Line coverage: 30.7% (87/283) Branch coverage: 20.7% (32/154) Total lines: 11106/4/2026 - 12:15:59 AM Line coverage: 30.6% (87/284) Branch coverage: 21.1% (33/156) Total lines: 11146/8/2026 - 12:16:15 AM Line coverage: 27.8% (89/320) Branch coverage: 18.3% (33/180) Total lines: 11726/9/2026 - 12:16:23 AM Line coverage: 28% (90/321) Branch coverage: 18.3% (33/180) Total lines: 1173

Coverage delta

Coverage delta 4 -4

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetItems()17.08%1558115814.83%
GetResumeItems(...)60%111080.43%
GetItemUserData(...)0%4260%
UpdateItemUserData(...)0%4260%

File(s)

/srv/git/jellyfin/Jellyfin.Api/Controllers/ItemsController.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.ComponentModel.DataAnnotations;
 4using System.Linq;
 5using System.Threading.Tasks;
 6using Jellyfin.Api.Extensions;
 7using Jellyfin.Api.Helpers;
 8using Jellyfin.Api.ModelBinders;
 9using Jellyfin.Data;
 10using Jellyfin.Data.Enums;
 11using Jellyfin.Database.Implementations.Enums;
 12using Jellyfin.Extensions;
 13using MediaBrowser.Common.Extensions;
 14using MediaBrowser.Controller.Dto;
 15using MediaBrowser.Controller.Entities;
 16using MediaBrowser.Controller.Entities.Movies;
 17using MediaBrowser.Controller.Library;
 18using MediaBrowser.Controller.Playlists;
 19using MediaBrowser.Controller.Session;
 20using MediaBrowser.Model.Dto;
 21using MediaBrowser.Model.Entities;
 22using MediaBrowser.Model.Globalization;
 23using MediaBrowser.Model.Querying;
 24using Microsoft.AspNetCore.Authorization;
 25using Microsoft.AspNetCore.Http;
 26using Microsoft.AspNetCore.Mvc;
 27using Microsoft.Extensions.Logging;
 28
 29namespace Jellyfin.Api.Controllers;
 30
 31/// <summary>
 32/// The items controller.
 33/// </summary>
 34[Route("")]
 35[Authorize]
 36[Tags("Library")]
 37public class ItemsController : BaseJellyfinApiController
 38{
 39    private readonly IUserManager _userManager;
 40    private readonly ILibraryManager _libraryManager;
 41    private readonly ILocalizationManager _localization;
 42    private readonly IDtoService _dtoService;
 43    private readonly ILogger<ItemsController> _logger;
 44    private readonly ISessionManager _sessionManager;
 45    private readonly IUserDataManager _userDataRepository;
 46    private readonly ISearchManager _searchManager;
 47
 48    /// <summary>
 49    /// Initializes a new instance of the <see cref="ItemsController"/> class.
 50    /// </summary>
 51    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 52    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 53    /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
 54    /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
 55    /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
 56    /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
 57    /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
 58    /// <param name="searchManager">Instance of the <see cref="ISearchManager"/> interface.</param>
 659    public ItemsController(
 660        IUserManager userManager,
 661        ILibraryManager libraryManager,
 662        ILocalizationManager localization,
 663        IDtoService dtoService,
 664        ILogger<ItemsController> logger,
 665        ISessionManager sessionManager,
 666        IUserDataManager userDataRepository,
 667        ISearchManager searchManager)
 68    {
 669        _userManager = userManager;
 670        _libraryManager = libraryManager;
 671        _localization = localization;
 672        _dtoService = dtoService;
 673        _logger = logger;
 674        _sessionManager = sessionManager;
 675        _userDataRepository = userDataRepository;
 676        _searchManager = searchManager;
 677    }
 78
 79    /// <summary>
 80    /// Gets items based on a query.
 81    /// </summary>
 82    /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param
 83    /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
 84    /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
 85    /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
 86    /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
 87    /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
 88    /// <param name="hasTrailer">Optional filter by items with trailers.</param>
 89    /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
 90    /// <param name="indexNumber">Optional filter by index number.</param>
 91    /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
 92    /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
 93    /// <param name="isHd">Optional filter by items that are HD or not.</param>
 94    /// <param name="is4K">Optional filter by items that are 4K or not.</param>
 95    /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows 
 96    /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. T
 97    /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
 98    /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
 99    /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
 100    /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
 101    /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
 102    /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
 103    /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.<
 104    /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
 105    /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
 106    /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
 107    /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
 108    /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
 109    /// <param name="isMovie">Optional filter for live tv movies.</param>
 110    /// <param name="isSeries">Optional filter for live tv series.</param>
 111    /// <param name="isNews">Optional filter for live tv news.</param>
 112    /// <param name="isKids">Optional filter for live tv kids.</param>
 113    /// <param name="isSports">Optional filter for live tv sports.</param>
 114    /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows
 115    /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped fr
 116    /// <param name="limit">Optional. The maximum number of records to return.</param>
 117    /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursi
 118    /// <param name="searchTerm">Optional. Filter based on a search term.</param>
 119    /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
 120    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 121    /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows mul
 122    /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 123    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This all
 124    /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Opti
 125    /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
 126    /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
 127    /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types.
 128    /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Ar
 129    /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
 130    /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe
 131    /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This all
 132    /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe del
 133    /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multi
 134    /// <param name="enableUserData">Optional, include user data.</param>
 135    /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
 136    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 137    /// <param name="person">Optional. If specified, results will be filtered to include only those containing the speci
 138    /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the sp
 139    /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only th
 140    /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pi
 141    /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, p
 142    /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows 
 143    /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the sp
 144    /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing t
 145    /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those conta
 146    /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe
 147    /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple,
 148    /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows m
 149    /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma deli
 150    /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
 151    /// <param name="isLocked">Optional filter by items that are locked.</param>
 152    /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
 153    /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
 154    /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
 155    /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
 156    /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
 157    /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
 158    /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
 159    /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
 160    /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
 161    /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a gi
 162    /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</p
 163    /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</
 164    /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multipl
 165    /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple,
 166    /// <param name="audioLanguages">Optional. If specified, results will be filtered based on audio language. This allo
 167    /// <param name="subtitleLanguages">Optional. If specified, results will be filtered based on subtitle language. Thi
 168    /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
 169    /// <param name="enableImages">Optional, include image information in output.</param>
 170    /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
 171    [HttpGet("Items")]
 172    [ProducesResponseType(StatusCodes.Status200OK)]
 173    public async Task<ActionResult<QueryResult<BaseItemDto>>> GetItems(
 174        [FromQuery] Guid? userId,
 175        [FromQuery] string? maxOfficialRating,
 176        [FromQuery] bool? hasThemeSong,
 177        [FromQuery] bool? hasThemeVideo,
 178        [FromQuery] bool? hasSubtitles,
 179        [FromQuery] bool? hasSpecialFeature,
 180        [FromQuery] bool? hasTrailer,
 181        [FromQuery] Guid? adjacentTo,
 182        [FromQuery] int? indexNumber,
 183        [FromQuery] int? parentIndexNumber,
 184        [FromQuery] bool? hasParentalRating,
 185        [FromQuery] bool? isHd,
 186        [FromQuery] bool? is4K,
 187        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes,
 188        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes,
 189        [FromQuery] bool? isMissing,
 190        [FromQuery] bool? isUnaired,
 191        [FromQuery] double? minCommunityRating,
 192        [FromQuery] double? minCriticRating,
 193        [FromQuery] DateTime? minPremiereDate,
 194        [FromQuery] DateTime? minDateLastSaved,
 195        [FromQuery] DateTime? minDateLastSavedForUser,
 196        [FromQuery] DateTime? maxPremiereDate,
 197        [FromQuery] bool? hasOverview,
 198        [FromQuery] bool? hasImdbId,
 199        [FromQuery] bool? hasTmdbId,
 200        [FromQuery] bool? hasTvdbId,
 201        [FromQuery] bool? isMovie,
 202        [FromQuery] bool? isSeries,
 203        [FromQuery] bool? isNews,
 204        [FromQuery] bool? isKids,
 205        [FromQuery] bool? isSports,
 206        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds,
 207        [FromQuery] int? startIndex,
 208        [FromQuery] int? limit,
 209        [FromQuery] bool? recursive,
 210        [FromQuery] string? searchTerm,
 211        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
 212        [FromQuery] Guid? parentId,
 213        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 214        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
 215        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 216        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
 217        [FromQuery] bool? isFavorite,
 218        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
 219        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes,
 220        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
 221        [FromQuery] bool? isPlayed,
 222        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
 223        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
 224        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
 225        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
 226        [FromQuery] bool? enableUserData,
 227        [FromQuery] int? imageTypeLimit,
 228        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 229        [FromQuery] string? person,
 230        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
 231        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
 232        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios,
 233        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] artists,
 234        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
 235        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds,
 236        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds,
 237        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds,
 238        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] albums,
 239        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds,
 240        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
 241        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes,
 242        [FromQuery] string? minOfficialRating,
 243        [FromQuery] bool? isLocked,
 244        [FromQuery] bool? isPlaceHolder,
 245        [FromQuery] bool? hasOfficialRating,
 246        [FromQuery] bool? collapseBoxSetItems,
 247        [FromQuery] int? minWidth,
 248        [FromQuery] int? minHeight,
 249        [FromQuery] int? maxWidth,
 250        [FromQuery] int? maxHeight,
 251        [FromQuery] bool? is3D,
 252        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus,
 253        [FromQuery] string? nameStartsWithOrGreater,
 254        [FromQuery] string? nameStartsWith,
 255        [FromQuery] string? nameLessThan,
 256        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
 257        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
 258        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] audioLanguages,
 259        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] subtitleLanguages,
 260        [FromQuery] bool enableTotalRecordCount = true,
 261        [FromQuery] bool? enableImages = true)
 262    {
 4263        var isApiKey = User.GetIsApiKey();
 264        // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
 4265        userId = RequestHelpers.GetUserId(User, userId);
 4266        var user = userId.IsNullOrEmpty()
 4267            ? null
 4268            : _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException();
 269
 270        // beyond this point, we're either using an api key or we have a valid user
 3271        if (!isApiKey && user is null)
 272        {
 0273            return BadRequest("userId is required");
 274        }
 275
 3276        if (user is not null
 3277            && user.GetPreference(PreferenceKind.AllowedTags).Length != 0
 3278            && !fields.Contains(ItemFields.Tags))
 279        {
 0280            fields = [.. fields, ItemFields.Tags];
 281        }
 282
 3283        var dtoOptions = new DtoOptions { Fields = fields }
 3284            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 285
 3286        var item = _libraryManager.GetParentItem(parentId, userId);
 287        QueryResult<BaseItem> result;
 288
 3289        Guid[] linkedChildAncestorIds = [];
 3290        if (includeItemTypes.Length == 1
 3291            && (includeItemTypes[0] == BaseItemKind.BoxSet || includeItemTypes[0] == BaseItemKind.Playlist)
 3292            && item is not BoxSet
 3293            && item is not Playlist)
 294        {
 0295            var itemCollectionType = item is IHasCollectionType hct ? hct.CollectionType : null;
 0296            var targetCollectionType = includeItemTypes[0] == BaseItemKind.BoxSet
 0297                ? CollectionType.boxsets
 0298                : CollectionType.playlists;
 0299            if (parentId.HasValue && item is not UserRootFolder && itemCollectionType != targetCollectionType)
 300            {
 0301                linkedChildAncestorIds = [parentId.Value];
 302            }
 303
 0304            parentId = null;
 0305            item = _libraryManager.GetUserRootFolder();
 306        }
 307
 3308        if (item is not Folder folder)
 309        {
 0310            folder = _libraryManager.GetUserRootFolder();
 311        }
 312
 3313        CollectionType? collectionType = null;
 3314        if (folder is IHasCollectionType hasCollectionType)
 315        {
 0316            collectionType = hasCollectionType.CollectionType;
 317        }
 318
 3319        if (collectionType == CollectionType.playlists)
 320        {
 0321            recursive = true;
 0322            includeItemTypes = [BaseItemKind.Playlist];
 323        }
 3324        else if (folder is ICollectionFolder)
 325        {
 0326            if (includeItemTypes.Length == 0)
 327            {
 0328                includeItemTypes = collectionType switch
 0329                {
 0330                    CollectionType.boxsets => [BaseItemKind.BoxSet],
 0331                    null => [BaseItemKind.Movie, BaseItemKind.Series],
 0332                    _ => []
 0333                };
 334            }
 335
 336            // When the client doesn't specify recursive/includeItemTypes, force the query
 337            // through the database path where all filters (IsHD, genres, etc.) are applied.
 0338            if (includeItemTypes.Length > 0)
 339            {
 0340                recursive ??= true;
 341            }
 342        }
 343
 3344        if (item is not UserRootFolder
 3345            // api keys can always access all folders
 3346            && !isApiKey
 3347            // check the item is visible for the user
 3348            && !item.IsVisible(user))
 349        {
 0350            _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user!.Username, item.Name);
 0351            return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
 352        }
 353
 3354        if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder)
 355        {
 356            // Use search providers when searchTerm is provided. Providers return only IDs and scores;
 357            // items are loaded server-side via folder.GetItems below, which applies user-access filtering.
 0358            Dictionary<Guid, float>? searchResultScores = null;
 0359            Guid[] itemIds = ids;
 360
 0361            if (!string.IsNullOrWhiteSpace(searchTerm))
 362            {
 0363                var searchProviderQuery = new SearchProviderQuery
 0364                {
 0365                    SearchTerm = searchTerm,
 0366                    UserId = userId,
 0367                    IncludeItemTypes = includeItemTypes,
 0368                    ExcludeItemTypes = excludeItemTypes,
 0369                    MediaTypes = mediaTypes,
 0370                    Limit = limit.HasValue ? limit.Value * 3 : null,
 0371                    ParentId = parentId
 0372                };
 373
 0374                var searchResults = await _searchManager.GetSearchResultsAsync(searchProviderQuery, HttpContext.RequestA
 0375                if (searchResults.Count > 0)
 376                {
 0377                    searchResultScores = searchResults.ToDictionary(r => r.ItemId, r => r.Score);
 0378                    itemIds = ids.Length > 0
 0379                        ? ids.Concat(searchResultScores.Keys).Distinct().ToArray()
 0380                        : searchResultScores.Keys.ToArray();
 381                }
 382            }
 383
 0384            var query = new InternalItemsQuery(user)
 0385            {
 0386                IsPlayed = isPlayed,
 0387                MediaTypes = mediaTypes,
 0388                IncludeItemTypes = includeItemTypes,
 0389                ExcludeItemTypes = excludeItemTypes,
 0390                Recursive = recursive ?? false,
 0391                OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
 0392                IsFavorite = isFavorite,
 0393                Limit = searchResultScores is null ? limit : null,
 0394                StartIndex = searchResultScores is null ? startIndex : null,
 0395                IsMissing = isMissing,
 0396                IsUnaired = isUnaired,
 0397                CollapseBoxSetItems = collapseBoxSetItems,
 0398                NameLessThan = nameLessThan,
 0399                NameStartsWith = nameStartsWith,
 0400                NameStartsWithOrGreater = nameStartsWithOrGreater,
 0401                HasImdbId = hasImdbId,
 0402                IsPlaceHolder = isPlaceHolder,
 0403                IsLocked = isLocked,
 0404                MinWidth = minWidth,
 0405                MinHeight = minHeight,
 0406                MaxWidth = maxWidth,
 0407                MaxHeight = maxHeight,
 0408                Is3D = is3D,
 0409                HasTvdbId = hasTvdbId,
 0410                HasTmdbId = hasTmdbId,
 0411                IsMovie = isMovie,
 0412                IsSeries = isSeries,
 0413                IsNews = isNews,
 0414                IsKids = isKids,
 0415                IsSports = isSports,
 0416                HasOverview = hasOverview,
 0417                HasOfficialRating = hasOfficialRating,
 0418                HasParentalRating = hasParentalRating,
 0419                HasSpecialFeature = hasSpecialFeature,
 0420                HasSubtitles = hasSubtitles,
 0421                HasThemeSong = hasThemeSong,
 0422                HasThemeVideo = hasThemeVideo,
 0423                HasTrailer = hasTrailer,
 0424                IsHD = isHd,
 0425                Is4K = is4K,
 0426                Tags = tags,
 0427                OfficialRatings = officialRatings,
 0428                Genres = genres,
 0429                ArtistIds = artistIds,
 0430                AlbumArtistIds = albumArtistIds,
 0431                ContributingArtistIds = contributingArtistIds,
 0432                GenreIds = genreIds,
 0433                StudioIds = studioIds,
 0434                Person = person,
 0435                PersonIds = personIds,
 0436                PersonTypes = personTypes,
 0437                Years = years,
 0438                ImageTypes = imageTypes,
 0439                VideoTypes = videoTypes,
 0440                AdjacentTo = adjacentTo,
 0441                ItemIds = itemIds,
 0442                MinCommunityRating = minCommunityRating,
 0443                MinCriticRating = minCriticRating,
 0444                ParentId = parentId ?? Guid.Empty,
 0445                IndexNumber = indexNumber,
 0446                ParentIndexNumber = parentIndexNumber,
 0447                EnableTotalRecordCount = enableTotalRecordCount,
 0448                ExcludeItemIds = excludeItemIds,
 0449                DtoOptions = dtoOptions,
 0450                SearchTerm = searchResultScores is null ? searchTerm : null,
 0451                MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
 0452                MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
 0453                MinPremiereDate = minPremiereDate?.ToUniversalTime(),
 0454                MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
 0455                AudioLanguages = audioLanguages,
 0456                SubtitleLanguages = subtitleLanguages,
 0457                LinkedChildAncestorIds = linkedChildAncestorIds,
 0458            };
 459
 0460            if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
 461            {
 0462                query.CollapseBoxSetItems = false;
 463            }
 464
 0465            if (query.SubtitleLanguages.Count > 0 && query.HasSubtitles.HasValue)
 466            {
 0467                if (query.HasSubtitles.Value)
 468                {
 469                    // if we check for specific subtitles we don't need a separate check for subtitle existence
 0470                    query.HasSubtitles = null;
 471                }
 472                else
 473                {
 474                    // if we search for items without subtitles, we don't need to check for subtitles of a specific lang
 0475                    query.SubtitleLanguages = [];
 476                }
 477            }
 478
 479            // for filter values that rely on media streams, we need to include alternative and linked versions
 0480            if (query.HasSubtitles.HasValue
 0481                || query.SubtitleLanguages.Count > 0
 0482                || query.AudioLanguages.Count > 0
 0483                || query.Is3D.HasValue
 0484                || query.IsHD.HasValue
 0485                || query.Is4K.HasValue
 0486                || query.VideoTypes.Length > 0
 0487            )
 488            {
 0489                query.IncludeOwnedItems = true;
 490            }
 491
 0492            query.ApplyFilters(filters);
 493
 494            // Filter by Series Status
 0495            if (seriesStatus.Length != 0)
 496            {
 0497                query.SeriesStatuses = seriesStatus;
 498            }
 499
 500            // Exclude Blocked Unrated Items
 0501            var blockedUnratedItems = user?.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems);
 0502            if (blockedUnratedItems is not null)
 503            {
 0504                query.BlockUnratedItems = blockedUnratedItems;
 505            }
 506
 507            // ExcludeLocationTypes
 0508            if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
 509            {
 0510                query.IsVirtualItem = false;
 511            }
 512
 0513            if (locationTypes.Length > 0 && locationTypes.Length < 4)
 514            {
 0515                query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
 516            }
 517
 518            // Min official rating
 0519            if (!string.IsNullOrWhiteSpace(minOfficialRating))
 520            {
 0521                query.MinParentalRating = _localization.GetRatingScore(minOfficialRating);
 522            }
 523
 524            // Max official rating
 0525            if (!string.IsNullOrWhiteSpace(maxOfficialRating))
 526            {
 0527                query.MaxParentalRating = _localization.GetRatingScore(maxOfficialRating);
 528            }
 529
 530            // Artists
 0531            if (artists.Length != 0)
 532            {
 0533                query.ArtistIds = artists.Select(i =>
 0534                {
 0535                    try
 0536                    {
 0537                        return _libraryManager.GetArtist(i, new DtoOptions(false));
 0538                    }
 0539                    catch
 0540                    {
 0541                        return null;
 0542                    }
 0543                }).Where(i => i is not null).Select(i => i!.Id).ToArray();
 544            }
 545
 546            // ExcludeArtistIds
 0547            if (excludeArtistIds.Length != 0)
 548            {
 0549                query.ExcludeArtistIds = excludeArtistIds;
 550            }
 551
 0552            if (albumIds.Length != 0)
 553            {
 0554                query.AlbumIds = albumIds;
 555            }
 556
 557            // Albums
 0558            if (albums.Length != 0)
 559            {
 0560                query.AlbumIds = albums.SelectMany(i =>
 0561                {
 0562                    return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlb
 0563                }).ToArray();
 564            }
 565
 566            // Studios
 0567            if (studios.Length != 0)
 568            {
 0569                query.StudioIds = studios.Select(i =>
 0570                {
 0571                    try
 0572                    {
 0573                        return _libraryManager.GetStudio(i);
 0574                    }
 0575                    catch
 0576                    {
 0577                        return null;
 0578                    }
 0579                }).Where(i => i is not null).Select(i => i!.Id).ToArray();
 580            }
 581
 582            // Apply default sorting if none requested
 0583            if (query.OrderBy.Count == 0)
 584            {
 585                // Albums by artist
 0586                if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == Bas
 587                {
 0588                    query.OrderBy = [(ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.
 589                }
 590            }
 591
 0592            query.Parent = null;
 593
 594            // folder.GetItems applies user-access filtering via the InternalItemsQuery's User.
 0595            result = folder.GetItems(query);
 0596            if (searchResultScores is not null && searchResultScores.Count > 0)
 597            {
 0598                var orderedItems = result.Items
 0599                    .OrderByDescending(item => searchResultScores.GetValueOrDefault(item.Id, 0f))
 0600                    .ThenBy(item => item.SortName)
 0601                    .ToArray();
 602
 0603                var totalCount = orderedItems.Length;
 0604                if (startIndex.HasValue && startIndex.Value > 0)
 605                {
 0606                    orderedItems = orderedItems.Skip(startIndex.Value).ToArray();
 607                }
 608
 0609                if (limit.HasValue)
 610                {
 0611                    orderedItems = orderedItems.Take(limit.Value).ToArray();
 612                }
 613
 0614                return new QueryResult<BaseItemDto>(
 0615                    startIndex,
 0616                    totalCount,
 0617                    _dtoService.GetBaseItemDtos(orderedItems, dtoOptions, user));
 618            }
 0619        }
 620        else
 621        {
 3622            var itemsArray = folder.GetChildren(user, true);
 3623            result = new QueryResult<BaseItem>(itemsArray);
 624        }
 625
 3626        return new QueryResult<BaseItemDto>(
 3627            startIndex,
 3628            result.TotalRecordCount,
 3629            _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user, skipVisibilityCheck: true));
 3630    }
 631
 632    /// <summary>
 633    /// Gets items based on a query.
 634    /// </summary>
 635    /// <param name="userId">The user id supplied as query parameter.</param>
 636    /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
 637    /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
 638    /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
 639    /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
 640    /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
 641    /// <param name="hasTrailer">Optional filter by items with trailers.</param>
 642    /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
 643    /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
 644    /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
 645    /// <param name="isHd">Optional filter by items that are HD or not.</param>
 646    /// <param name="is4K">Optional filter by items that are 4K or not.</param>
 647    /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows 
 648    /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. T
 649    /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
 650    /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
 651    /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
 652    /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
 653    /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
 654    /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
 655    /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.<
 656    /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
 657    /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
 658    /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
 659    /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
 660    /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
 661    /// <param name="isMovie">Optional filter for live tv movies.</param>
 662    /// <param name="isSeries">Optional filter for live tv series.</param>
 663    /// <param name="isNews">Optional filter for live tv news.</param>
 664    /// <param name="isKids">Optional filter for live tv kids.</param>
 665    /// <param name="isSports">Optional filter for live tv sports.</param>
 666    /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows
 667    /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped fr
 668    /// <param name="limit">Optional. The maximum number of records to return.</param>
 669    /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursi
 670    /// <param name="searchTerm">Optional. Filter based on a search term.</param>
 671    /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
 672    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 673    /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows mul
 674    /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 675    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This all
 676    /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Opti
 677    /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
 678    /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
 679    /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types.
 680    /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Ar
 681    /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
 682    /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe
 683    /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This all
 684    /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe del
 685    /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multi
 686    /// <param name="enableUserData">Optional, include user data.</param>
 687    /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
 688    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 689    /// <param name="person">Optional. If specified, results will be filtered to include only those containing the speci
 690    /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the sp
 691    /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only th
 692    /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pi
 693    /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, p
 694    /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows 
 695    /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the sp
 696    /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing t
 697    /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those conta
 698    /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe
 699    /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple,
 700    /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows m
 701    /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma deli
 702    /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
 703    /// <param name="isLocked">Optional filter by items that are locked.</param>
 704    /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
 705    /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
 706    /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
 707    /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
 708    /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
 709    /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
 710    /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
 711    /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
 712    /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
 713    /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a gi
 714    /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</p
 715    /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</
 716    /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multipl
 717    /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple,
 718    /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
 719    /// <param name="enableImages">Optional, include image information in output.</param>
 720    /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
 721    [HttpGet("Users/{userId}/Items")]
 722    [Obsolete("Kept for backwards compatibility")]
 723    [ApiExplorerSettings(IgnoreApi = true)]
 724    [ProducesResponseType(StatusCodes.Status200OK)]
 725    public async Task<ActionResult<QueryResult<BaseItemDto>>> GetItemsByUserIdLegacy(
 726        [FromRoute] Guid userId,
 727        [FromQuery] string? maxOfficialRating,
 728        [FromQuery] bool? hasThemeSong,
 729        [FromQuery] bool? hasThemeVideo,
 730        [FromQuery] bool? hasSubtitles,
 731        [FromQuery] bool? hasSpecialFeature,
 732        [FromQuery] bool? hasTrailer,
 733        [FromQuery] Guid? adjacentTo,
 734        [FromQuery] int? parentIndexNumber,
 735        [FromQuery] bool? hasParentalRating,
 736        [FromQuery] bool? isHd,
 737        [FromQuery] bool? is4K,
 738        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes,
 739        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes,
 740        [FromQuery] bool? isMissing,
 741        [FromQuery] bool? isUnaired,
 742        [FromQuery] double? minCommunityRating,
 743        [FromQuery] double? minCriticRating,
 744        [FromQuery] DateTime? minPremiereDate,
 745        [FromQuery] DateTime? minDateLastSaved,
 746        [FromQuery] DateTime? minDateLastSavedForUser,
 747        [FromQuery] DateTime? maxPremiereDate,
 748        [FromQuery] bool? hasOverview,
 749        [FromQuery] bool? hasImdbId,
 750        [FromQuery] bool? hasTmdbId,
 751        [FromQuery] bool? hasTvdbId,
 752        [FromQuery] bool? isMovie,
 753        [FromQuery] bool? isSeries,
 754        [FromQuery] bool? isNews,
 755        [FromQuery] bool? isKids,
 756        [FromQuery] bool? isSports,
 757        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds,
 758        [FromQuery] int? startIndex,
 759        [FromQuery] int? limit,
 760        [FromQuery] bool? recursive,
 761        [FromQuery] string? searchTerm,
 762        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
 763        [FromQuery] Guid? parentId,
 764        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 765        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
 766        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 767        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
 768        [FromQuery] bool? isFavorite,
 769        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
 770        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes,
 771        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
 772        [FromQuery] bool? isPlayed,
 773        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
 774        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
 775        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
 776        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
 777        [FromQuery] bool? enableUserData,
 778        [FromQuery] int? imageTypeLimit,
 779        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 780        [FromQuery] string? person,
 781        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
 782        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
 783        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios,
 784        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] artists,
 785        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
 786        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds,
 787        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds,
 788        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds,
 789        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] albums,
 790        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds,
 791        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
 792        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes,
 793        [FromQuery] string? minOfficialRating,
 794        [FromQuery] bool? isLocked,
 795        [FromQuery] bool? isPlaceHolder,
 796        [FromQuery] bool? hasOfficialRating,
 797        [FromQuery] bool? collapseBoxSetItems,
 798        [FromQuery] int? minWidth,
 799        [FromQuery] int? minHeight,
 800        [FromQuery] int? maxWidth,
 801        [FromQuery] int? maxHeight,
 802        [FromQuery] bool? is3D,
 803        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus,
 804        [FromQuery] string? nameStartsWithOrGreater,
 805        [FromQuery] string? nameStartsWith,
 806        [FromQuery] string? nameLessThan,
 807        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
 808        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
 809        [FromQuery] bool enableTotalRecordCount = true,
 810        [FromQuery] bool? enableImages = true)
 811        => await GetItems(
 812            userId,
 813            maxOfficialRating,
 814            hasThemeSong,
 815            hasThemeVideo,
 816            hasSubtitles,
 817            hasSpecialFeature,
 818            hasTrailer,
 819            adjacentTo,
 820            null,
 821            parentIndexNumber,
 822            hasParentalRating,
 823            isHd,
 824            is4K,
 825            locationTypes,
 826            excludeLocationTypes,
 827            isMissing,
 828            isUnaired,
 829            minCommunityRating,
 830            minCriticRating,
 831            minPremiereDate,
 832            minDateLastSaved,
 833            minDateLastSavedForUser,
 834            maxPremiereDate,
 835            hasOverview,
 836            hasImdbId,
 837            hasTmdbId,
 838            hasTvdbId,
 839            isMovie,
 840            isSeries,
 841            isNews,
 842            isKids,
 843            isSports,
 844            excludeItemIds,
 845            startIndex,
 846            limit,
 847            recursive,
 848            searchTerm,
 849            sortOrder,
 850            parentId,
 851            fields,
 852            excludeItemTypes,
 853            includeItemTypes,
 854            filters,
 855            isFavorite,
 856            mediaTypes,
 857            imageTypes,
 858            sortBy,
 859            isPlayed,
 860            genres,
 861            officialRatings,
 862            tags,
 863            years,
 864            enableUserData,
 865            imageTypeLimit,
 866            enableImageTypes,
 867            person,
 868            personIds,
 869            personTypes,
 870            studios,
 871            artists,
 872            excludeArtistIds,
 873            artistIds,
 874            albumArtistIds,
 875            contributingArtistIds,
 876            albums,
 877            albumIds,
 878            ids,
 879            videoTypes,
 880            minOfficialRating,
 881            isLocked,
 882            isPlaceHolder,
 883            hasOfficialRating,
 884            collapseBoxSetItems,
 885            minWidth,
 886            minHeight,
 887            maxWidth,
 888            maxHeight,
 889            is3D,
 890            seriesStatus,
 891            nameStartsWithOrGreater,
 892            nameStartsWith,
 893            nameLessThan,
 894            studioIds,
 895            genreIds,
 896            [],
 897            [],
 898            enableTotalRecordCount,
 899            enableImages).ConfigureAwait(false);
 900
 901    /// <summary>
 902    /// Gets items based on a query.
 903    /// </summary>
 904    /// <param name="userId">The user id.</param>
 905    /// <param name="startIndex">The start index.</param>
 906    /// <param name="limit">The item limit.</param>
 907    /// <param name="searchTerm">The search term.</param>
 908    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 909    /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows mul
 910    /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
 911    /// <param name="enableUserData">Optional. Include user data.</param>
 912    /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 913    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 914    /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 915    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This all
 916    /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
 917    /// <param name="enableImages">Optional. Include image information in output.</param>
 918    /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
 919    /// <response code="200">Items returned.</response>
 920    /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
 921    [HttpGet("UserItems/Resume")]
 922    [ProducesResponseType(StatusCodes.Status200OK)]
 923    public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
 924        [FromQuery] Guid? userId,
 925        [FromQuery] int? startIndex,
 926        [FromQuery] int? limit,
 927        [FromQuery] string? searchTerm,
 928        [FromQuery] Guid? parentId,
 929        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 930        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
 931        [FromQuery] bool? enableUserData,
 932        [FromQuery] int? imageTypeLimit,
 933        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 934        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
 935        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 936        [FromQuery] bool enableTotalRecordCount = true,
 937        [FromQuery] bool? enableImages = true,
 938        [FromQuery] bool excludeActiveSessions = false)
 939    {
 2940        var requestUserId = RequestHelpers.GetUserId(User, userId);
 2941        var user = _userManager.GetUserById(requestUserId);
 2942        if (user is null)
 943        {
 1944            return NotFound();
 945        }
 946
 1947        var parentIdGuid = parentId ?? Guid.Empty;
 1948        var dtoOptions = new DtoOptions { Fields = fields }
 1949            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 950
 1951        var ancestorIds = Array.Empty<Guid>();
 952
 1953        var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
 1954        if (parentIdGuid.IsEmpty() && excludeFolderIds.Length > 0)
 955        {
 0956            ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
 0957                .Where(i => i is Folder)
 0958                .Where(i => !excludeFolderIds.Contains(i.Id))
 0959                .Select(i => i.Id)
 0960                .ToArray();
 961        }
 962
 1963        var excludeItemIds = Array.Empty<Guid>();
 1964        if (excludeActiveSessions)
 965        {
 0966            excludeItemIds = _sessionManager.Sessions
 0967                .Where(s => s.UserId.Equals(requestUserId) && s.NowPlayingItem is not null)
 0968                .Select(s => s.NowPlayingItem.Id)
 0969                .ToArray();
 970        }
 971
 1972        var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
 1973        {
 1974            OrderBy = [(ItemSortBy.DatePlayed, SortOrder.Descending)],
 1975            IsResumable = true,
 1976            StartIndex = startIndex,
 1977            Limit = limit,
 1978            ParentId = parentIdGuid,
 1979            Recursive = true,
 1980            DtoOptions = dtoOptions,
 1981            MediaTypes = mediaTypes,
 1982            IsVirtualItem = false,
 1983            CollapseBoxSetItems = false,
 1984            IncludeOwnedItems = true,
 1985            EnableTotalRecordCount = enableTotalRecordCount,
 1986            AncestorIds = ancestorIds,
 1987            IncludeItemTypes = includeItemTypes,
 1988            ExcludeItemTypes = excludeItemTypes,
 1989            SearchTerm = searchTerm,
 1990            ExcludeItemIds = excludeItemIds
 1991        });
 992
 1993        var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
 994
 1995        return new QueryResult<BaseItemDto>(
 1996            startIndex,
 1997            itemsResult.TotalRecordCount,
 1998            returnItems);
 999    }
 1000
 1001    /// <summary>
 1002    /// Gets items based on a query.
 1003    /// </summary>
 1004    /// <param name="userId">The user id.</param>
 1005    /// <param name="startIndex">The start index.</param>
 1006    /// <param name="limit">The item limit.</param>
 1007    /// <param name="searchTerm">The search term.</param>
 1008    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 1009    /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows mul
 1010    /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
 1011    /// <param name="enableUserData">Optional. Include user data.</param>
 1012    /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 1013    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 1014    /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 1015    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This all
 1016    /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
 1017    /// <param name="enableImages">Optional. Include image information in output.</param>
 1018    /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
 1019    /// <response code="200">Items returned.</response>
 1020    /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
 1021    [HttpGet("Users/{userId}/Items/Resume")]
 1022    [Obsolete("Kept for backwards compatibility")]
 1023    [ApiExplorerSettings(IgnoreApi = true)]
 1024    [ProducesResponseType(StatusCodes.Status200OK)]
 1025    public ActionResult<QueryResult<BaseItemDto>> GetResumeItemsLegacy(
 1026        [FromRoute, Required] Guid userId,
 1027        [FromQuery] int? startIndex,
 1028        [FromQuery] int? limit,
 1029        [FromQuery] string? searchTerm,
 1030        [FromQuery] Guid? parentId,
 1031        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 1032        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
 1033        [FromQuery] bool? enableUserData,
 1034        [FromQuery] int? imageTypeLimit,
 1035        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 1036        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
 1037        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 1038        [FromQuery] bool enableTotalRecordCount = true,
 1039        [FromQuery] bool? enableImages = true,
 1040        [FromQuery] bool excludeActiveSessions = false)
 1041    => GetResumeItems(
 1042        userId,
 1043        startIndex,
 1044        limit,
 1045        searchTerm,
 1046        parentId,
 1047        fields,
 1048        mediaTypes,
 1049        enableUserData,
 1050        imageTypeLimit,
 1051        enableImageTypes,
 1052        excludeItemTypes,
 1053        includeItemTypes,
 1054        enableTotalRecordCount,
 1055        enableImages,
 1056        excludeActiveSessions);
 1057
 1058    /// <summary>
 1059    /// Get Item User Data.
 1060    /// </summary>
 1061    /// <param name="userId">The user id.</param>
 1062    /// <param name="itemId">The item id.</param>
 1063    /// <response code="200">return item user data.</response>
 1064    /// <response code="404">Item is not found.</response>
 1065    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1066    [HttpGet("UserItems/{itemId}/UserData")]
 1067    [ProducesResponseType(StatusCodes.Status200OK)]
 1068    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1069    [Tags("UserData")]
 1070    public ActionResult<UserItemDataDto?> GetItemUserData(
 1071        [FromQuery] Guid? userId,
 1072        [FromRoute, Required] Guid itemId)
 1073    {
 01074        var requestUserId = RequestHelpers.GetUserId(User, userId);
 01075        var user = _userManager.GetUserById(requestUserId);
 01076        if (user is null)
 1077        {
 01078            return NotFound();
 1079        }
 1080
 01081        if (!RequestHelpers.AssertCanUpdateUser(User, user, true))
 1082        {
 01083            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to view this item user data.");
 1084        }
 1085
 01086        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
 01087        if (item is null)
 1088        {
 01089            return NotFound();
 1090        }
 1091
 01092        return _userDataRepository.GetUserDataDto(item, user);
 1093    }
 1094
 1095    /// <summary>
 1096    /// Get Item User Data.
 1097    /// </summary>
 1098    /// <param name="userId">The user id.</param>
 1099    /// <param name="itemId">The item id.</param>
 1100    /// <response code="200">return item user data.</response>
 1101    /// <response code="404">Item is not found.</response>
 1102    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1103    [HttpGet("Users/{userId}/Items/{itemId}/UserData")]
 1104    [ProducesResponseType(StatusCodes.Status200OK)]
 1105    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1106    [Obsolete("Kept for backwards compatibility")]
 1107    [ApiExplorerSettings(IgnoreApi = true)]
 1108    public ActionResult<UserItemDataDto?> GetItemUserDataLegacy(
 1109        [FromRoute, Required] Guid userId,
 1110        [FromRoute, Required] Guid itemId)
 1111        => GetItemUserData(userId, itemId);
 1112
 1113    /// <summary>
 1114    /// Update Item User Data.
 1115    /// </summary>
 1116    /// <param name="userId">The user id.</param>
 1117    /// <param name="itemId">The item id.</param>
 1118    /// <param name="userDataDto">New user data object.</param>
 1119    /// <response code="200">return updated user item data.</response>
 1120    /// <response code="404">Item is not found.</response>
 1121    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1122    [HttpPost("UserItems/{itemId}/UserData")]
 1123    [ProducesResponseType(StatusCodes.Status200OK)]
 1124    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1125    [Tags("UserData")]
 1126    public ActionResult<UserItemDataDto?> UpdateItemUserData(
 1127        [FromQuery] Guid? userId,
 1128        [FromRoute, Required] Guid itemId,
 1129        [FromBody, Required] UpdateUserItemDataDto userDataDto)
 1130    {
 01131        var requestUserId = RequestHelpers.GetUserId(User, userId);
 01132        var user = _userManager.GetUserById(requestUserId);
 01133        if (user is null)
 1134        {
 01135            return NotFound();
 1136        }
 1137
 01138        if (!RequestHelpers.AssertCanUpdateUser(User, user, true))
 1139        {
 01140            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update this item user data.");
 1141        }
 1142
 01143        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
 01144        if (item is null)
 1145        {
 01146            return NotFound();
 1147        }
 1148
 01149        _userDataRepository.SaveUserData(user, item, userDataDto, UserDataSaveReason.UpdateUserData);
 1150
 01151        return _userDataRepository.GetUserDataDto(item, user);
 1152    }
 1153
 1154    /// <summary>
 1155    /// Update Item User Data.
 1156    /// </summary>
 1157    /// <param name="userId">The user id.</param>
 1158    /// <param name="itemId">The item id.</param>
 1159    /// <param name="userDataDto">New user data object.</param>
 1160    /// <response code="200">return updated user item data.</response>
 1161    /// <response code="404">Item is not found.</response>
 1162    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1163    [HttpPost("Users/{userId}/Items/{itemId}/UserData")]
 1164    [ProducesResponseType(StatusCodes.Status200OK)]
 1165    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1166    [Obsolete("Kept for backwards compatibility")]
 1167    [ApiExplorerSettings(IgnoreApi = true)]
 1168    public ActionResult<UserItemDataDto?> UpdateItemUserDataLegacy(
 1169        [FromRoute, Required] Guid userId,
 1170        [FromRoute, Required] Guid itemId,
 1171        [FromBody, Required] UpdateUserItemDataDto userDataDto)
 1172        => UpdateItemUserData(userId, itemId, userDataDto);
 1173}