< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.ItemsController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/ItemsController.cs
Line coverage
60%
Covered lines: 197
Uncovered lines: 127
Coverable lines: 324
Total lines: 1181
Line coverage: 60.8%
Branch coverage
42%
Covered branches: 79
Total branches: 186
Branch coverage: 42.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/26/2026 - 12:14:14 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: 11736/18/2026 - 12:16:24 AM Line coverage: 61.4% (196/319) Branch coverage: 42.3% (78/184) Total lines: 11726/30/2026 - 12:15:32 AM Line coverage: 61.3% (197/321) Branch coverage: 42.4% (79/186) Total lines: 11757/6/2026 - 12:16:28 AM Line coverage: 60.8% (197/324) Branch coverage: 42.4% (79/186) Total lines: 1181 3/26/2026 - 12:14:14 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: 11736/18/2026 - 12:16:24 AM Line coverage: 61.4% (196/319) Branch coverage: 42.3% (78/184) Total lines: 11726/30/2026 - 12:15:32 AM Line coverage: 61.3% (197/321) Branch coverage: 42.4% (79/186) Total lines: 11757/6/2026 - 12:16:28 AM Line coverage: 60.8% (197/324) Branch coverage: 42.4% (79/186) Total lines: 1181

Coverage delta

Coverage delta 34 -34

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetItems()44.51%186516460.16%
GetResumeItems(...)60%111075.51%
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 = [];
 290
 3291        includeItemTypes ??= [];
 3292        if (includeItemTypes.Length == 1
 3293            && (includeItemTypes[0] == BaseItemKind.BoxSet || includeItemTypes[0] == BaseItemKind.Playlist)
 3294            && item is not BoxSet
 3295            && item is not Playlist)
 296        {
 0297            var itemCollectionType = item is IHasCollectionType hct ? hct.CollectionType : null;
 0298            var targetCollectionType = includeItemTypes[0] == BaseItemKind.BoxSet
 0299                ? CollectionType.boxsets
 0300                : CollectionType.playlists;
 0301            if (parentId.HasValue && item is not UserRootFolder && itemCollectionType != targetCollectionType)
 302            {
 0303                linkedChildAncestorIds = [parentId.Value];
 304            }
 305
 0306            parentId = null;
 0307            item = _libraryManager.GetUserRootFolder();
 308        }
 309
 3310        if (item is not Folder folder)
 311        {
 0312            folder = _libraryManager.GetUserRootFolder();
 313        }
 314
 3315        CollectionType? collectionType = null;
 3316        if (folder is IHasCollectionType hasCollectionType)
 317        {
 0318            collectionType = hasCollectionType.CollectionType;
 0319            includeItemTypes = [.. includeItemTypes.Union(DtoExtensions.GetBaseItemKindsForCollectionType(collectionType
 320        }
 321
 3322        if (collectionType == CollectionType.playlists)
 323        {
 0324            recursive = true;
 0325            includeItemTypes = [BaseItemKind.Playlist];
 326        }
 3327        else if (folder is ICollectionFolder && includeItemTypes.Length == 0)
 328        {
 0329            includeItemTypes = collectionType switch
 0330            {
 0331                CollectionType.boxsets => [BaseItemKind.BoxSet],
 0332                null => [BaseItemKind.Movie, BaseItemKind.Series],
 0333                _ => []
 0334            };
 335        }
 336
 337        // includeItemTypes on a library lists its contents recursively rather than just its
 338        // immediate children, so default to a recursive query when the client didn't choose.
 3339        if (folder is ICollectionFolder && includeItemTypes.Length > 0)
 340        {
 0341            recursive ??= true;
 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
 354        // Build the query up front so the dispatch below can decide the path from it.
 355        // Use search providers when searchTerm is provided. Providers return only IDs and scores;
 356        // items are loaded server-side via folder.GetItems below, which applies user-access filtering.
 3357        Dictionary<Guid, float>? searchResultScores = null;
 3358        Guid[] itemIds = ids;
 359
 3360        if (!string.IsNullOrWhiteSpace(searchTerm))
 361        {
 0362            var searchProviderQuery = new SearchProviderQuery
 0363            {
 0364                SearchTerm = searchTerm,
 0365                UserId = userId,
 0366                IncludeItemTypes = includeItemTypes,
 0367                ExcludeItemTypes = excludeItemTypes,
 0368                MediaTypes = mediaTypes,
 0369                Limit = limit.HasValue ? limit.Value * 3 : null,
 0370                ParentId = parentId
 0371            };
 372
 0373            var searchResults = await _searchManager.GetSearchResultsAsync(searchProviderQuery, HttpContext.RequestAbort
 0374            if (searchResults.Count > 0)
 375            {
 0376                searchResultScores = searchResults.ToDictionary(r => r.ItemId, r => r.Score);
 0377                itemIds = ids.Length > 0
 0378                    ? ids.Concat(searchResultScores.Keys).Distinct().ToArray()
 0379                    : searchResultScores.Keys.ToArray();
 380            }
 381        }
 382
 3383        var query = new InternalItemsQuery(user)
 3384        {
 3385            IsPlayed = isPlayed,
 3386            MediaTypes = mediaTypes,
 3387            IncludeItemTypes = includeItemTypes,
 3388            ExcludeItemTypes = excludeItemTypes,
 3389            Recursive = recursive ?? false,
 3390            OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
 3391            IsFavorite = isFavorite,
 3392            Limit = searchResultScores is null ? limit : null,
 3393            StartIndex = searchResultScores is null ? startIndex : null,
 3394            IsMissing = isMissing,
 3395            IsUnaired = isUnaired,
 3396            CollapseBoxSetItems = collapseBoxSetItems,
 3397            NameLessThan = nameLessThan,
 3398            NameStartsWith = nameStartsWith,
 3399            NameStartsWithOrGreater = nameStartsWithOrGreater,
 3400            HasImdbId = hasImdbId,
 3401            IsPlaceHolder = isPlaceHolder,
 3402            IsLocked = isLocked,
 3403            MinWidth = minWidth,
 3404            MinHeight = minHeight,
 3405            MaxWidth = maxWidth,
 3406            MaxHeight = maxHeight,
 3407            Is3D = is3D,
 3408            HasTvdbId = hasTvdbId,
 3409            HasTmdbId = hasTmdbId,
 3410            IsMovie = isMovie,
 3411            IsSeries = isSeries,
 3412            IsNews = isNews,
 3413            IsKids = isKids,
 3414            IsSports = isSports,
 3415            HasOverview = hasOverview,
 3416            HasOfficialRating = hasOfficialRating,
 3417            HasParentalRating = hasParentalRating,
 3418            HasSpecialFeature = hasSpecialFeature,
 3419            HasSubtitles = hasSubtitles,
 3420            HasThemeSong = hasThemeSong,
 3421            HasThemeVideo = hasThemeVideo,
 3422            HasTrailer = hasTrailer,
 3423            IsHD = isHd,
 3424            Is4K = is4K,
 3425            Tags = tags,
 3426            OfficialRatings = officialRatings,
 3427            Genres = genres,
 3428            ArtistIds = artistIds,
 3429            AlbumArtistIds = albumArtistIds,
 3430            ContributingArtistIds = contributingArtistIds,
 3431            GenreIds = genreIds,
 3432            StudioIds = studioIds,
 3433            Person = person,
 3434            PersonIds = personIds,
 3435            PersonTypes = personTypes,
 3436            Years = years,
 3437            ImageTypes = imageTypes,
 3438            VideoTypes = videoTypes,
 3439            AdjacentTo = adjacentTo,
 3440            ItemIds = itemIds,
 3441            MinCommunityRating = minCommunityRating,
 3442            MinCriticRating = minCriticRating,
 3443            ParentId = parentId ?? Guid.Empty,
 3444            IndexNumber = indexNumber,
 3445            ParentIndexNumber = parentIndexNumber,
 3446            EnableTotalRecordCount = enableTotalRecordCount,
 3447            ExcludeItemIds = excludeItemIds,
 3448            DtoOptions = dtoOptions,
 3449            SearchTerm = searchResultScores is null ? searchTerm : null,
 3450            MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
 3451            MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
 3452            MinPremiereDate = minPremiereDate?.ToUniversalTime(),
 3453            MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
 3454            AudioLanguages = audioLanguages,
 3455            SubtitleLanguages = subtitleLanguages,
 3456            LinkedChildAncestorIds = linkedChildAncestorIds,
 3457        };
 458
 3459        if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
 460        {
 0461            query.CollapseBoxSetItems = false;
 462        }
 463
 3464        if (query.SubtitleLanguages.Count > 0 && query.HasSubtitles.HasValue)
 465        {
 0466            if (query.HasSubtitles.Value)
 467            {
 468                // if we check for specific subtitles we don't need a separate check for subtitle existence
 0469                query.HasSubtitles = null;
 470            }
 471            else
 472            {
 473                // if we search for items without subtitles, we don't need to check for subtitles of a specific language
 0474                query.SubtitleLanguages = [];
 475            }
 476        }
 477
 478        // for filter values that rely on media streams, we need to include alternative and linked versions
 3479        if (query.HasSubtitles.HasValue
 3480            || query.SubtitleLanguages.Count > 0
 3481            || query.AudioLanguages.Count > 0
 3482            || query.Is3D.HasValue
 3483            || query.IsHD.HasValue
 3484            || query.Is4K.HasValue
 3485            || query.VideoTypes.Length > 0
 3486        )
 487        {
 0488            query.IncludeOwnedItems = true;
 489        }
 490
 3491        query.ApplyFilters(filters);
 492
 493        // Filter by Series Status
 3494        if (seriesStatus.Length != 0)
 495        {
 0496            query.SeriesStatuses = seriesStatus;
 497        }
 498
 499        // Exclude Blocked Unrated Items
 3500        var blockedUnratedItems = user?.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems);
 3501        if (blockedUnratedItems is not null)
 502        {
 3503            query.BlockUnratedItems = blockedUnratedItems;
 504        }
 505
 506        // ExcludeLocationTypes
 3507        if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
 508        {
 0509            query.IsVirtualItem = false;
 510        }
 511
 3512        if (locationTypes.Length > 0 && locationTypes.Length < 4)
 513        {
 0514            query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
 515        }
 516
 517        // Min official rating
 3518        if (!string.IsNullOrWhiteSpace(minOfficialRating))
 519        {
 0520            query.MinParentalRating = _localization.GetRatingScore(minOfficialRating);
 521        }
 522
 523        // Max official rating
 3524        if (!string.IsNullOrWhiteSpace(maxOfficialRating))
 525        {
 0526            query.MaxParentalRating = _localization.GetRatingScore(maxOfficialRating);
 527        }
 528
 529        // Artists
 3530        if (artists.Length != 0)
 531        {
 0532            query.ArtistIds = artists.Select(i =>
 0533            {
 0534                try
 0535                {
 0536                    return _libraryManager.GetArtist(i, new DtoOptions(false));
 0537                }
 0538                catch
 0539                {
 0540                    return null;
 0541                }
 0542            }).Where(i => i is not null).Select(i => i!.Id).ToArray();
 543        }
 544
 545        // ExcludeArtistIds
 3546        if (excludeArtistIds.Length != 0)
 547        {
 0548            query.ExcludeArtistIds = excludeArtistIds;
 549        }
 550
 3551        if (albumIds.Length != 0)
 552        {
 0553            query.AlbumIds = albumIds;
 554        }
 555
 556        // Albums
 3557        if (albums.Length != 0)
 558        {
 0559            query.AlbumIds = albums.SelectMany(i =>
 0560            {
 0561                return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum],
 0562            }).ToArray();
 563        }
 564
 565        // Studios
 3566        if (studios.Length != 0)
 567        {
 0568            query.StudioIds = studios.Select(i =>
 0569            {
 0570                try
 0571                {
 0572                    return _libraryManager.GetStudio(i);
 0573                }
 0574                catch
 0575                {
 0576                    return null;
 0577                }
 0578            }).Where(i => i is not null).Select(i => i!.Id).ToArray();
 579        }
 580
 581        // Apply default sorting if none requested
 3582        if (query.OrderBy.Count == 0)
 583        {
 584            // Albums by artist
 3585            if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseIte
 586            {
 0587                query.OrderBy = [(ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Asce
 588            }
 589        }
 590
 3591        query.Parent = null;
 592
 593        // At the user root an unfiltered, non-recursive request is a plain listing of the user's libraries
 3594        if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder || query.HasFilters
 595        {
 596            // folder.GetItems applies user-access filtering via the InternalItemsQuery's User.
 0597            result = folder.GetItems(query);
 0598            if (searchResultScores is not null && searchResultScores.Count > 0)
 599            {
 0600                var orderedItems = result.Items
 0601                    .OrderByDescending(item => searchResultScores.GetValueOrDefault(item.Id, 0f))
 0602                    .ThenBy(item => item.SortName)
 0603                    .ToArray();
 604
 0605                var totalCount = orderedItems.Length;
 0606                if (startIndex.HasValue && startIndex.Value > 0)
 607                {
 0608                    orderedItems = orderedItems.Skip(startIndex.Value).ToArray();
 609                }
 610
 0611                if (limit.HasValue)
 612                {
 0613                    orderedItems = orderedItems.Take(limit.Value).ToArray();
 614                }
 615
 0616                return new QueryResult<BaseItemDto>(
 0617                    startIndex,
 0618                    totalCount,
 0619                    _dtoService.GetBaseItemDtos(orderedItems, dtoOptions, user));
 620            }
 621        }
 622        else
 623        {
 3624            var itemsArray = folder.GetChildren(user, true);
 3625            result = new QueryResult<BaseItem>(itemsArray);
 626        }
 627
 3628        return new QueryResult<BaseItemDto>(
 3629            startIndex,
 3630            result.TotalRecordCount,
 3631            _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user, skipVisibilityCheck: true));
 3632    }
 633
 634    /// <summary>
 635    /// Gets items based on a query.
 636    /// </summary>
 637    /// <param name="userId">The user id supplied as query parameter.</param>
 638    /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
 639    /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
 640    /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
 641    /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
 642    /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
 643    /// <param name="hasTrailer">Optional filter by items with trailers.</param>
 644    /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
 645    /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
 646    /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
 647    /// <param name="isHd">Optional filter by items that are HD or not.</param>
 648    /// <param name="is4K">Optional filter by items that are 4K or not.</param>
 649    /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows 
 650    /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. T
 651    /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
 652    /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
 653    /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
 654    /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
 655    /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
 656    /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
 657    /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.<
 658    /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
 659    /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
 660    /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
 661    /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
 662    /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
 663    /// <param name="isMovie">Optional filter for live tv movies.</param>
 664    /// <param name="isSeries">Optional filter for live tv series.</param>
 665    /// <param name="isNews">Optional filter for live tv news.</param>
 666    /// <param name="isKids">Optional filter for live tv kids.</param>
 667    /// <param name="isSports">Optional filter for live tv sports.</param>
 668    /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows
 669    /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped fr
 670    /// <param name="limit">Optional. The maximum number of records to return.</param>
 671    /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursi
 672    /// <param name="searchTerm">Optional. Filter based on a search term.</param>
 673    /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
 674    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 675    /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows mul
 676    /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 677    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This all
 678    /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Opti
 679    /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
 680    /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
 681    /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types.
 682    /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Ar
 683    /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
 684    /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe
 685    /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This all
 686    /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe del
 687    /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multi
 688    /// <param name="enableUserData">Optional, include user data.</param>
 689    /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
 690    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 691    /// <param name="person">Optional. If specified, results will be filtered to include only those containing the speci
 692    /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the sp
 693    /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only th
 694    /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pi
 695    /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, p
 696    /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows 
 697    /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the sp
 698    /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing t
 699    /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those conta
 700    /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe
 701    /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple,
 702    /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows m
 703    /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma deli
 704    /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
 705    /// <param name="isLocked">Optional filter by items that are locked.</param>
 706    /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
 707    /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
 708    /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
 709    /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
 710    /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
 711    /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
 712    /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
 713    /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
 714    /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
 715    /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a gi
 716    /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</p
 717    /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</
 718    /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multipl
 719    /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple,
 720    /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
 721    /// <param name="enableImages">Optional, include image information in output.</param>
 722    /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
 723    [HttpGet("Users/{userId}/Items")]
 724    [Obsolete("Kept for backwards compatibility")]
 725    [ApiExplorerSettings(IgnoreApi = true)]
 726    [ProducesResponseType(StatusCodes.Status200OK)]
 727    public async Task<ActionResult<QueryResult<BaseItemDto>>> GetItemsByUserIdLegacy(
 728        [FromRoute] Guid userId,
 729        [FromQuery] string? maxOfficialRating,
 730        [FromQuery] bool? hasThemeSong,
 731        [FromQuery] bool? hasThemeVideo,
 732        [FromQuery] bool? hasSubtitles,
 733        [FromQuery] bool? hasSpecialFeature,
 734        [FromQuery] bool? hasTrailer,
 735        [FromQuery] Guid? adjacentTo,
 736        [FromQuery] int? parentIndexNumber,
 737        [FromQuery] bool? hasParentalRating,
 738        [FromQuery] bool? isHd,
 739        [FromQuery] bool? is4K,
 740        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] locationTypes,
 741        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] LocationType[] excludeLocationTypes,
 742        [FromQuery] bool? isMissing,
 743        [FromQuery] bool? isUnaired,
 744        [FromQuery] double? minCommunityRating,
 745        [FromQuery] double? minCriticRating,
 746        [FromQuery] DateTime? minPremiereDate,
 747        [FromQuery] DateTime? minDateLastSaved,
 748        [FromQuery] DateTime? minDateLastSavedForUser,
 749        [FromQuery] DateTime? maxPremiereDate,
 750        [FromQuery] bool? hasOverview,
 751        [FromQuery] bool? hasImdbId,
 752        [FromQuery] bool? hasTmdbId,
 753        [FromQuery] bool? hasTvdbId,
 754        [FromQuery] bool? isMovie,
 755        [FromQuery] bool? isSeries,
 756        [FromQuery] bool? isNews,
 757        [FromQuery] bool? isKids,
 758        [FromQuery] bool? isSports,
 759        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeItemIds,
 760        [FromQuery] int? startIndex,
 761        [FromQuery] int? limit,
 762        [FromQuery] bool? recursive,
 763        [FromQuery] string? searchTerm,
 764        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SortOrder[] sortOrder,
 765        [FromQuery] Guid? parentId,
 766        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 767        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
 768        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 769        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
 770        [FromQuery] bool? isFavorite,
 771        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
 772        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] imageTypes,
 773        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemSortBy[] sortBy,
 774        [FromQuery] bool? isPlayed,
 775        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] genres,
 776        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] officialRatings,
 777        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] tags,
 778        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] int[] years,
 779        [FromQuery] bool? enableUserData,
 780        [FromQuery] int? imageTypeLimit,
 781        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 782        [FromQuery] string? person,
 783        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] personIds,
 784        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes,
 785        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] studios,
 786        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] artists,
 787        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
 788        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] artistIds,
 789        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumArtistIds,
 790        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] contributingArtistIds,
 791        [FromQuery, ModelBinder(typeof(PipeDelimitedCollectionModelBinder))] string[] albums,
 792        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] albumIds,
 793        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
 794        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] VideoType[] videoTypes,
 795        [FromQuery] string? minOfficialRating,
 796        [FromQuery] bool? isLocked,
 797        [FromQuery] bool? isPlaceHolder,
 798        [FromQuery] bool? hasOfficialRating,
 799        [FromQuery] bool? collapseBoxSetItems,
 800        [FromQuery] int? minWidth,
 801        [FromQuery] int? minHeight,
 802        [FromQuery] int? maxWidth,
 803        [FromQuery] int? maxHeight,
 804        [FromQuery] bool? is3D,
 805        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] SeriesStatus[] seriesStatus,
 806        [FromQuery] string? nameStartsWithOrGreater,
 807        [FromQuery] string? nameStartsWith,
 808        [FromQuery] string? nameLessThan,
 809        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
 810        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
 811        [FromQuery] bool enableTotalRecordCount = true,
 812        [FromQuery] bool? enableImages = true)
 813        => await GetItems(
 814            userId,
 815            maxOfficialRating,
 816            hasThemeSong,
 817            hasThemeVideo,
 818            hasSubtitles,
 819            hasSpecialFeature,
 820            hasTrailer,
 821            adjacentTo,
 822            null,
 823            parentIndexNumber,
 824            hasParentalRating,
 825            isHd,
 826            is4K,
 827            locationTypes,
 828            excludeLocationTypes,
 829            isMissing,
 830            isUnaired,
 831            minCommunityRating,
 832            minCriticRating,
 833            minPremiereDate,
 834            minDateLastSaved,
 835            minDateLastSavedForUser,
 836            maxPremiereDate,
 837            hasOverview,
 838            hasImdbId,
 839            hasTmdbId,
 840            hasTvdbId,
 841            isMovie,
 842            isSeries,
 843            isNews,
 844            isKids,
 845            isSports,
 846            excludeItemIds,
 847            startIndex,
 848            limit,
 849            recursive,
 850            searchTerm,
 851            sortOrder,
 852            parentId,
 853            fields,
 854            excludeItemTypes,
 855            includeItemTypes,
 856            filters,
 857            isFavorite,
 858            mediaTypes,
 859            imageTypes,
 860            sortBy,
 861            isPlayed,
 862            genres,
 863            officialRatings,
 864            tags,
 865            years,
 866            enableUserData,
 867            imageTypeLimit,
 868            enableImageTypes,
 869            person,
 870            personIds,
 871            personTypes,
 872            studios,
 873            artists,
 874            excludeArtistIds,
 875            artistIds,
 876            albumArtistIds,
 877            contributingArtistIds,
 878            albums,
 879            albumIds,
 880            ids,
 881            videoTypes,
 882            minOfficialRating,
 883            isLocked,
 884            isPlaceHolder,
 885            hasOfficialRating,
 886            collapseBoxSetItems,
 887            minWidth,
 888            minHeight,
 889            maxWidth,
 890            maxHeight,
 891            is3D,
 892            seriesStatus,
 893            nameStartsWithOrGreater,
 894            nameStartsWith,
 895            nameLessThan,
 896            studioIds,
 897            genreIds,
 898            [],
 899            [],
 900            enableTotalRecordCount,
 901            enableImages).ConfigureAwait(false);
 902
 903    /// <summary>
 904    /// Gets items based on a query.
 905    /// </summary>
 906    /// <param name="userId">The user id.</param>
 907    /// <param name="startIndex">The start index.</param>
 908    /// <param name="limit">The item limit.</param>
 909    /// <param name="searchTerm">The search term.</param>
 910    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 911    /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows mul
 912    /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
 913    /// <param name="enableUserData">Optional. Include user data.</param>
 914    /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 915    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 916    /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 917    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This all
 918    /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
 919    /// <param name="enableImages">Optional. Include image information in output.</param>
 920    /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
 921    /// <response code="200">Items returned.</response>
 922    /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
 923    [HttpGet("UserItems/Resume")]
 924    [ProducesResponseType(StatusCodes.Status200OK)]
 925    public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
 926        [FromQuery] Guid? userId,
 927        [FromQuery] int? startIndex,
 928        [FromQuery] int? limit,
 929        [FromQuery] string? searchTerm,
 930        [FromQuery] Guid? parentId,
 931        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 932        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
 933        [FromQuery] bool? enableUserData,
 934        [FromQuery] int? imageTypeLimit,
 935        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 936        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
 937        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 938        [FromQuery] bool enableTotalRecordCount = true,
 939        [FromQuery] bool? enableImages = true,
 940        [FromQuery] bool excludeActiveSessions = false)
 941    {
 2942        var requestUserId = RequestHelpers.GetUserId(User, userId);
 2943        var user = _userManager.GetUserById(requestUserId);
 2944        if (user is null)
 945        {
 1946            return NotFound();
 947        }
 948
 1949        var parentIdGuid = parentId ?? Guid.Empty;
 1950        var dtoOptions = new DtoOptions { Fields = fields }
 1951            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 952
 1953        var ancestorIds = Array.Empty<Guid>();
 954
 1955        var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
 1956        if (parentIdGuid.IsEmpty() && excludeFolderIds.Length > 0)
 957        {
 0958            ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
 0959                .Where(i => i is Folder)
 0960                .Where(i => !excludeFolderIds.Contains(i.Id))
 0961                .Select(i => i.Id)
 0962                .ToArray();
 963        }
 964
 1965        var excludeItemIds = Array.Empty<Guid>();
 1966        if (excludeActiveSessions)
 967        {
 968            // NowPlayingItem.Id is the displayed/primary id, but resume queries surface the actually-played
 969            // alternate version's own id. Expand each active session to every version id so an in-progress
 970            // alternate is excluded too, instead of leaking back into the resume list.
 0971            excludeItemIds = _sessionManager.Sessions
 0972                .Where(s => s.UserId.Equals(requestUserId) && s.NowPlayingItem is not null)
 0973                .SelectMany(s => _libraryManager.GetItemById(s.NowPlayingItem.Id) is Video video
 0974                    ? video.GetAllVersions().Select(v => v.Id)
 0975                    : [s.NowPlayingItem.Id])
 0976                .Distinct()
 0977                .ToArray();
 978        }
 979
 1980        var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
 1981        {
 1982            OrderBy = [(ItemSortBy.DatePlayed, SortOrder.Descending)],
 1983            IsResumable = true,
 1984            StartIndex = startIndex,
 1985            Limit = limit,
 1986            ParentId = parentIdGuid,
 1987            Recursive = true,
 1988            DtoOptions = dtoOptions,
 1989            MediaTypes = mediaTypes,
 1990            IsVirtualItem = false,
 1991            CollapseBoxSetItems = false,
 1992            IncludeOwnedItems = true,
 1993            EnableTotalRecordCount = enableTotalRecordCount,
 1994            AncestorIds = ancestorIds,
 1995            IncludeItemTypes = includeItemTypes,
 1996            ExcludeItemTypes = excludeItemTypes,
 1997            SearchTerm = searchTerm,
 1998            ExcludeItemIds = excludeItemIds
 1999        });
 1000
 11001        var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
 1002
 11003        return new QueryResult<BaseItemDto>(
 11004            startIndex,
 11005            itemsResult.TotalRecordCount,
 11006            returnItems);
 1007    }
 1008
 1009    /// <summary>
 1010    /// Gets items based on a query.
 1011    /// </summary>
 1012    /// <param name="userId">The user id.</param>
 1013    /// <param name="startIndex">The start index.</param>
 1014    /// <param name="limit">The item limit.</param>
 1015    /// <param name="searchTerm">The search term.</param>
 1016    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 1017    /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows mul
 1018    /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
 1019    /// <param name="enableUserData">Optional. Include user data.</param>
 1020    /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 1021    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 1022    /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 1023    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This all
 1024    /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
 1025    /// <param name="enableImages">Optional. Include image information in output.</param>
 1026    /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
 1027    /// <response code="200">Items returned.</response>
 1028    /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
 1029    [HttpGet("Users/{userId}/Items/Resume")]
 1030    [Obsolete("Kept for backwards compatibility")]
 1031    [ApiExplorerSettings(IgnoreApi = true)]
 1032    [ProducesResponseType(StatusCodes.Status200OK)]
 1033    public ActionResult<QueryResult<BaseItemDto>> GetResumeItemsLegacy(
 1034        [FromRoute, Required] Guid userId,
 1035        [FromQuery] int? startIndex,
 1036        [FromQuery] int? limit,
 1037        [FromQuery] string? searchTerm,
 1038        [FromQuery] Guid? parentId,
 1039        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 1040        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
 1041        [FromQuery] bool? enableUserData,
 1042        [FromQuery] int? imageTypeLimit,
 1043        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 1044        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
 1045        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 1046        [FromQuery] bool enableTotalRecordCount = true,
 1047        [FromQuery] bool? enableImages = true,
 1048        [FromQuery] bool excludeActiveSessions = false)
 1049    => GetResumeItems(
 1050        userId,
 1051        startIndex,
 1052        limit,
 1053        searchTerm,
 1054        parentId,
 1055        fields,
 1056        mediaTypes,
 1057        enableUserData,
 1058        imageTypeLimit,
 1059        enableImageTypes,
 1060        excludeItemTypes,
 1061        includeItemTypes,
 1062        enableTotalRecordCount,
 1063        enableImages,
 1064        excludeActiveSessions);
 1065
 1066    /// <summary>
 1067    /// Get Item User Data.
 1068    /// </summary>
 1069    /// <param name="userId">The user id.</param>
 1070    /// <param name="itemId">The item id.</param>
 1071    /// <response code="200">return item user data.</response>
 1072    /// <response code="404">Item is not found.</response>
 1073    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1074    [HttpGet("UserItems/{itemId}/UserData")]
 1075    [ProducesResponseType(StatusCodes.Status200OK)]
 1076    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1077    [Tags("UserData")]
 1078    public ActionResult<UserItemDataDto?> GetItemUserData(
 1079        [FromQuery] Guid? userId,
 1080        [FromRoute, Required] Guid itemId)
 1081    {
 01082        var requestUserId = RequestHelpers.GetUserId(User, userId);
 01083        var user = _userManager.GetUserById(requestUserId);
 01084        if (user is null)
 1085        {
 01086            return NotFound();
 1087        }
 1088
 01089        if (!RequestHelpers.AssertCanUpdateUser(User, user, true))
 1090        {
 01091            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to view this item user data.");
 1092        }
 1093
 01094        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
 01095        if (item is null)
 1096        {
 01097            return NotFound();
 1098        }
 1099
 01100        return _userDataRepository.GetUserDataDto(item, user);
 1101    }
 1102
 1103    /// <summary>
 1104    /// Get Item User Data.
 1105    /// </summary>
 1106    /// <param name="userId">The user id.</param>
 1107    /// <param name="itemId">The item id.</param>
 1108    /// <response code="200">return item user data.</response>
 1109    /// <response code="404">Item is not found.</response>
 1110    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1111    [HttpGet("Users/{userId}/Items/{itemId}/UserData")]
 1112    [ProducesResponseType(StatusCodes.Status200OK)]
 1113    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1114    [Obsolete("Kept for backwards compatibility")]
 1115    [ApiExplorerSettings(IgnoreApi = true)]
 1116    public ActionResult<UserItemDataDto?> GetItemUserDataLegacy(
 1117        [FromRoute, Required] Guid userId,
 1118        [FromRoute, Required] Guid itemId)
 1119        => GetItemUserData(userId, itemId);
 1120
 1121    /// <summary>
 1122    /// Update Item User Data.
 1123    /// </summary>
 1124    /// <param name="userId">The user id.</param>
 1125    /// <param name="itemId">The item id.</param>
 1126    /// <param name="userDataDto">New user data object.</param>
 1127    /// <response code="200">return updated user item data.</response>
 1128    /// <response code="404">Item is not found.</response>
 1129    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1130    [HttpPost("UserItems/{itemId}/UserData")]
 1131    [ProducesResponseType(StatusCodes.Status200OK)]
 1132    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1133    [Tags("UserData")]
 1134    public ActionResult<UserItemDataDto?> UpdateItemUserData(
 1135        [FromQuery] Guid? userId,
 1136        [FromRoute, Required] Guid itemId,
 1137        [FromBody, Required] UpdateUserItemDataDto userDataDto)
 1138    {
 01139        var requestUserId = RequestHelpers.GetUserId(User, userId);
 01140        var user = _userManager.GetUserById(requestUserId);
 01141        if (user is null)
 1142        {
 01143            return NotFound();
 1144        }
 1145
 01146        if (!RequestHelpers.AssertCanUpdateUser(User, user, true))
 1147        {
 01148            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update this item user data.");
 1149        }
 1150
 01151        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
 01152        if (item is null)
 1153        {
 01154            return NotFound();
 1155        }
 1156
 01157        _userDataRepository.SaveUserData(user, item, userDataDto, UserDataSaveReason.UpdateUserData);
 1158
 01159        return _userDataRepository.GetUserDataDto(item, user);
 1160    }
 1161
 1162    /// <summary>
 1163    /// Update Item User Data.
 1164    /// </summary>
 1165    /// <param name="userId">The user id.</param>
 1166    /// <param name="itemId">The item id.</param>
 1167    /// <param name="userDataDto">New user data object.</param>
 1168    /// <response code="200">return updated user item data.</response>
 1169    /// <response code="404">Item is not found.</response>
 1170    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1171    [HttpPost("Users/{userId}/Items/{itemId}/UserData")]
 1172    [ProducesResponseType(StatusCodes.Status200OK)]
 1173    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1174    [Obsolete("Kept for backwards compatibility")]
 1175    [ApiExplorerSettings(IgnoreApi = true)]
 1176    public ActionResult<UserItemDataDto?> UpdateItemUserDataLegacy(
 1177        [FromRoute, Required] Guid userId,
 1178        [FromRoute, Required] Guid itemId,
 1179        [FromBody, Required] UpdateUserItemDataDto userDataDto)
 1180        => UpdateItemUserData(userId, itemId, userDataDto);
 1181}