< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.ItemsController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/ItemsController.cs
Line coverage
61%
Covered lines: 197
Uncovered lines: 125
Coverable lines: 322
Total lines: 1179
Line coverage: 61.1%
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 4/15/2026 - 12:14:34 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: 11817/18/2026 - 12:15:19 AM Line coverage: 61.1% (197/322) Branch coverage: 42.4% (79/186) Total lines: 1179 4/15/2026 - 12:14:34 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: 11817/18/2026 - 12:15:19 AM Line coverage: 61.1% (197/322) Branch coverage: 42.4% (79/186) Total lines: 1179

Coverage delta

Coverage delta 34 -34

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetItems()44.51%179916460.68%
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;
 319        }
 320
 3321        if (collectionType == CollectionType.playlists)
 322        {
 0323            recursive = true;
 0324            includeItemTypes = [BaseItemKind.Playlist];
 325        }
 3326        else if (folder is ICollectionFolder && includeItemTypes.Length == 0)
 327        {
 0328            includeItemTypes = collectionType switch
 0329            {
 0330                CollectionType.boxsets => [BaseItemKind.BoxSet],
 0331                _ => []
 0332            };
 333        }
 334
 335        // includeItemTypes on a library lists its contents recursively rather than just its
 336        // immediate children, so default to a recursive query when the client didn't choose.
 3337        if (folder is ICollectionFolder && includeItemTypes.Length > 0)
 338        {
 0339            recursive ??= true;
 340        }
 341
 3342        if (item is not UserRootFolder
 3343            // api keys can always access all folders
 3344            && !isApiKey
 3345            // check the item is visible for the user
 3346            && !item.IsVisible(user))
 347        {
 0348            _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user!.Username, item.Name);
 0349            return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
 350        }
 351
 352        // Build the query up front so the dispatch below can decide the path from it.
 353        // Use search providers when searchTerm is provided. Providers return only IDs and scores;
 354        // items are loaded server-side via folder.GetItems below, which applies user-access filtering.
 3355        Dictionary<Guid, float>? searchResultScores = null;
 3356        Guid[] itemIds = ids;
 357
 3358        if (!string.IsNullOrWhiteSpace(searchTerm))
 359        {
 0360            var searchProviderQuery = new SearchProviderQuery
 0361            {
 0362                SearchTerm = searchTerm,
 0363                UserId = userId,
 0364                IncludeItemTypes = includeItemTypes,
 0365                ExcludeItemTypes = excludeItemTypes,
 0366                MediaTypes = mediaTypes,
 0367                Limit = limit.HasValue ? limit.Value * 3 : null,
 0368                ParentId = parentId
 0369            };
 370
 0371            var searchResults = await _searchManager.GetSearchResultsAsync(searchProviderQuery, HttpContext.RequestAbort
 0372            if (searchResults.Count > 0)
 373            {
 0374                searchResultScores = searchResults.ToDictionary(r => r.ItemId, r => r.Score);
 0375                itemIds = ids.Length > 0
 0376                    ? ids.Concat(searchResultScores.Keys).Distinct().ToArray()
 0377                    : searchResultScores.Keys.ToArray();
 378            }
 379        }
 380
 3381        var query = new InternalItemsQuery(user)
 3382        {
 3383            IsPlayed = isPlayed,
 3384            MediaTypes = mediaTypes,
 3385            IncludeItemTypes = includeItemTypes,
 3386            ExcludeItemTypes = excludeItemTypes,
 3387            Recursive = recursive ?? false,
 3388            OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
 3389            IsFavorite = isFavorite,
 3390            Limit = searchResultScores is null ? limit : null,
 3391            StartIndex = searchResultScores is null ? startIndex : null,
 3392            IsMissing = isMissing,
 3393            IsUnaired = isUnaired,
 3394            CollapseBoxSetItems = collapseBoxSetItems,
 3395            NameLessThan = nameLessThan,
 3396            NameStartsWith = nameStartsWith,
 3397            NameStartsWithOrGreater = nameStartsWithOrGreater,
 3398            HasImdbId = hasImdbId,
 3399            IsPlaceHolder = isPlaceHolder,
 3400            IsLocked = isLocked,
 3401            MinWidth = minWidth,
 3402            MinHeight = minHeight,
 3403            MaxWidth = maxWidth,
 3404            MaxHeight = maxHeight,
 3405            Is3D = is3D,
 3406            HasTvdbId = hasTvdbId,
 3407            HasTmdbId = hasTmdbId,
 3408            IsMovie = isMovie,
 3409            IsSeries = isSeries,
 3410            IsNews = isNews,
 3411            IsKids = isKids,
 3412            IsSports = isSports,
 3413            HasOverview = hasOverview,
 3414            HasOfficialRating = hasOfficialRating,
 3415            HasParentalRating = hasParentalRating,
 3416            HasSpecialFeature = hasSpecialFeature,
 3417            HasSubtitles = hasSubtitles,
 3418            HasThemeSong = hasThemeSong,
 3419            HasThemeVideo = hasThemeVideo,
 3420            HasTrailer = hasTrailer,
 3421            IsHD = isHd,
 3422            Is4K = is4K,
 3423            Tags = tags,
 3424            OfficialRatings = officialRatings,
 3425            Genres = genres,
 3426            ArtistIds = artistIds,
 3427            AlbumArtistIds = albumArtistIds,
 3428            ContributingArtistIds = contributingArtistIds,
 3429            GenreIds = genreIds,
 3430            StudioIds = studioIds,
 3431            Person = person,
 3432            PersonIds = personIds,
 3433            PersonTypes = personTypes,
 3434            Years = years,
 3435            ImageTypes = imageTypes,
 3436            VideoTypes = videoTypes,
 3437            AdjacentTo = adjacentTo,
 3438            ItemIds = itemIds,
 3439            MinCommunityRating = minCommunityRating,
 3440            MinCriticRating = minCriticRating,
 3441            ParentId = parentId ?? Guid.Empty,
 3442            IndexNumber = indexNumber,
 3443            ParentIndexNumber = parentIndexNumber,
 3444            EnableTotalRecordCount = enableTotalRecordCount,
 3445            ExcludeItemIds = excludeItemIds,
 3446            DtoOptions = dtoOptions,
 3447            SearchTerm = searchResultScores is null ? searchTerm : null,
 3448            MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
 3449            MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
 3450            MinPremiereDate = minPremiereDate?.ToUniversalTime(),
 3451            MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
 3452            AudioLanguages = audioLanguages,
 3453            SubtitleLanguages = subtitleLanguages,
 3454            LinkedChildAncestorIds = linkedChildAncestorIds,
 3455        };
 456
 3457        if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
 458        {
 0459            query.CollapseBoxSetItems = false;
 460        }
 461
 3462        if (query.SubtitleLanguages.Count > 0 && query.HasSubtitles.HasValue)
 463        {
 0464            if (query.HasSubtitles.Value)
 465            {
 466                // if we check for specific subtitles we don't need a separate check for subtitle existence
 0467                query.HasSubtitles = null;
 468            }
 469            else
 470            {
 471                // if we search for items without subtitles, we don't need to check for subtitles of a specific language
 0472                query.SubtitleLanguages = [];
 473            }
 474        }
 475
 476        // for filter values that rely on media streams, we need to include alternative and linked versions
 3477        if (query.HasSubtitles.HasValue
 3478            || query.SubtitleLanguages.Count > 0
 3479            || query.AudioLanguages.Count > 0
 3480            || query.Is3D.HasValue
 3481            || query.IsHD.HasValue
 3482            || query.Is4K.HasValue
 3483            || query.VideoTypes.Length > 0
 3484        )
 485        {
 0486            query.IncludeOwnedItems = true;
 487        }
 488
 3489        query.ApplyFilters(filters);
 490
 491        // Filter by Series Status
 3492        if (seriesStatus.Length != 0)
 493        {
 0494            query.SeriesStatuses = seriesStatus;
 495        }
 496
 497        // Exclude Blocked Unrated Items
 3498        var blockedUnratedItems = user?.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems);
 3499        if (blockedUnratedItems is not null)
 500        {
 3501            query.BlockUnratedItems = blockedUnratedItems;
 502        }
 503
 504        // ExcludeLocationTypes
 3505        if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
 506        {
 0507            query.IsVirtualItem = false;
 508        }
 509
 3510        if (locationTypes.Length > 0 && locationTypes.Length < 4)
 511        {
 0512            query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
 513        }
 514
 515        // Min official rating
 3516        if (!string.IsNullOrWhiteSpace(minOfficialRating))
 517        {
 0518            query.MinParentalRating = _localization.GetRatingScore(minOfficialRating);
 519        }
 520
 521        // Max official rating
 3522        if (!string.IsNullOrWhiteSpace(maxOfficialRating))
 523        {
 0524            query.MaxParentalRating = _localization.GetRatingScore(maxOfficialRating);
 525        }
 526
 527        // Artists
 3528        if (artists.Length != 0)
 529        {
 0530            query.ArtistIds = artists.Select(i =>
 0531            {
 0532                try
 0533                {
 0534                    return _libraryManager.GetArtist(i, new DtoOptions(false));
 0535                }
 0536                catch
 0537                {
 0538                    return null;
 0539                }
 0540            }).Where(i => i is not null).Select(i => i!.Id).ToArray();
 541        }
 542
 543        // ExcludeArtistIds
 3544        if (excludeArtistIds.Length != 0)
 545        {
 0546            query.ExcludeArtistIds = excludeArtistIds;
 547        }
 548
 3549        if (albumIds.Length != 0)
 550        {
 0551            query.AlbumIds = albumIds;
 552        }
 553
 554        // Albums
 3555        if (albums.Length != 0)
 556        {
 0557            query.AlbumIds = albums.SelectMany(i =>
 0558            {
 0559                return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum],
 0560            }).ToArray();
 561        }
 562
 563        // Studios
 3564        if (studios.Length != 0)
 565        {
 0566            query.StudioIds = studios.Select(i =>
 0567            {
 0568                try
 0569                {
 0570                    return _libraryManager.GetStudio(i);
 0571                }
 0572                catch
 0573                {
 0574                    return null;
 0575                }
 0576            }).Where(i => i is not null).Select(i => i!.Id).ToArray();
 577        }
 578
 579        // Apply default sorting if none requested
 3580        if (query.OrderBy.Count == 0)
 581        {
 582            // Albums by artist
 3583            if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseIte
 584            {
 0585                query.OrderBy = [(ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Asce
 586            }
 587        }
 588
 3589        query.Parent = null;
 590
 591        // At the user root an unfiltered, non-recursive request is a plain listing of the user's libraries
 3592        if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder || query.HasFilters
 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            }
 619        }
 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        {
 966            // NowPlayingItem.Id is the displayed/primary id, but resume queries surface the actually-played
 967            // alternate version's own id. Expand each active session to every version id so an in-progress
 968            // alternate is excluded too, instead of leaking back into the resume list.
 0969            excludeItemIds = _sessionManager.Sessions
 0970                .Where(s => s.UserId.Equals(requestUserId) && s.NowPlayingItem is not null)
 0971                .SelectMany(s => _libraryManager.GetItemById(s.NowPlayingItem.Id) is Video video
 0972                    ? video.GetAllVersions().Select(v => v.Id)
 0973                    : [s.NowPlayingItem.Id])
 0974                .Distinct()
 0975                .ToArray();
 976        }
 977
 1978        var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
 1979        {
 1980            OrderBy = [(ItemSortBy.DatePlayed, SortOrder.Descending)],
 1981            IsResumable = true,
 1982            StartIndex = startIndex,
 1983            Limit = limit,
 1984            ParentId = parentIdGuid,
 1985            Recursive = true,
 1986            DtoOptions = dtoOptions,
 1987            MediaTypes = mediaTypes,
 1988            IsVirtualItem = false,
 1989            CollapseBoxSetItems = false,
 1990            IncludeOwnedItems = true,
 1991            EnableTotalRecordCount = enableTotalRecordCount,
 1992            AncestorIds = ancestorIds,
 1993            IncludeItemTypes = includeItemTypes,
 1994            ExcludeItemTypes = excludeItemTypes,
 1995            SearchTerm = searchTerm,
 1996            ExcludeItemIds = excludeItemIds
 1997        });
 998
 1999        var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
 1000
 11001        return new QueryResult<BaseItemDto>(
 11002            startIndex,
 11003            itemsResult.TotalRecordCount,
 11004            returnItems);
 1005    }
 1006
 1007    /// <summary>
 1008    /// Gets items based on a query.
 1009    /// </summary>
 1010    /// <param name="userId">The user id.</param>
 1011    /// <param name="startIndex">The start index.</param>
 1012    /// <param name="limit">The item limit.</param>
 1013    /// <param name="searchTerm">The search term.</param>
 1014    /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</
 1015    /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows mul
 1016    /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
 1017    /// <param name="enableUserData">Optional. Include user data.</param>
 1018    /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 1019    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 1020    /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows 
 1021    /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This all
 1022    /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
 1023    /// <param name="enableImages">Optional. Include image information in output.</param>
 1024    /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
 1025    /// <response code="200">Items returned.</response>
 1026    /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
 1027    [HttpGet("Users/{userId}/Items/Resume")]
 1028    [Obsolete("Kept for backwards compatibility")]
 1029    [ApiExplorerSettings(IgnoreApi = true)]
 1030    [ProducesResponseType(StatusCodes.Status200OK)]
 1031    public ActionResult<QueryResult<BaseItemDto>> GetResumeItemsLegacy(
 1032        [FromRoute, Required] Guid userId,
 1033        [FromQuery] int? startIndex,
 1034        [FromQuery] int? limit,
 1035        [FromQuery] string? searchTerm,
 1036        [FromQuery] Guid? parentId,
 1037        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 1038        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] MediaType[] mediaTypes,
 1039        [FromQuery] bool? enableUserData,
 1040        [FromQuery] int? imageTypeLimit,
 1041        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes,
 1042        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] excludeItemTypes,
 1043        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] BaseItemKind[] includeItemTypes,
 1044        [FromQuery] bool enableTotalRecordCount = true,
 1045        [FromQuery] bool? enableImages = true,
 1046        [FromQuery] bool excludeActiveSessions = false)
 1047    => GetResumeItems(
 1048        userId,
 1049        startIndex,
 1050        limit,
 1051        searchTerm,
 1052        parentId,
 1053        fields,
 1054        mediaTypes,
 1055        enableUserData,
 1056        imageTypeLimit,
 1057        enableImageTypes,
 1058        excludeItemTypes,
 1059        includeItemTypes,
 1060        enableTotalRecordCount,
 1061        enableImages,
 1062        excludeActiveSessions);
 1063
 1064    /// <summary>
 1065    /// Get Item User Data.
 1066    /// </summary>
 1067    /// <param name="userId">The user id.</param>
 1068    /// <param name="itemId">The item id.</param>
 1069    /// <response code="200">return item user data.</response>
 1070    /// <response code="404">Item is not found.</response>
 1071    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1072    [HttpGet("UserItems/{itemId}/UserData")]
 1073    [ProducesResponseType(StatusCodes.Status200OK)]
 1074    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1075    [Tags("UserData")]
 1076    public ActionResult<UserItemDataDto?> GetItemUserData(
 1077        [FromQuery] Guid? userId,
 1078        [FromRoute, Required] Guid itemId)
 1079    {
 01080        var requestUserId = RequestHelpers.GetUserId(User, userId);
 01081        var user = _userManager.GetUserById(requestUserId);
 01082        if (user is null)
 1083        {
 01084            return NotFound();
 1085        }
 1086
 01087        if (!RequestHelpers.AssertCanUpdateUser(User, user, true))
 1088        {
 01089            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to view this item user data.");
 1090        }
 1091
 01092        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
 01093        if (item is null)
 1094        {
 01095            return NotFound();
 1096        }
 1097
 01098        return _userDataRepository.GetUserDataDto(item, user);
 1099    }
 1100
 1101    /// <summary>
 1102    /// Get Item User Data.
 1103    /// </summary>
 1104    /// <param name="userId">The user id.</param>
 1105    /// <param name="itemId">The item id.</param>
 1106    /// <response code="200">return item user data.</response>
 1107    /// <response code="404">Item is not found.</response>
 1108    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1109    [HttpGet("Users/{userId}/Items/{itemId}/UserData")]
 1110    [ProducesResponseType(StatusCodes.Status200OK)]
 1111    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1112    [Obsolete("Kept for backwards compatibility")]
 1113    [ApiExplorerSettings(IgnoreApi = true)]
 1114    public ActionResult<UserItemDataDto?> GetItemUserDataLegacy(
 1115        [FromRoute, Required] Guid userId,
 1116        [FromRoute, Required] Guid itemId)
 1117        => GetItemUserData(userId, itemId);
 1118
 1119    /// <summary>
 1120    /// Update Item User Data.
 1121    /// </summary>
 1122    /// <param name="userId">The user id.</param>
 1123    /// <param name="itemId">The item id.</param>
 1124    /// <param name="userDataDto">New user data object.</param>
 1125    /// <response code="200">return updated user item data.</response>
 1126    /// <response code="404">Item is not found.</response>
 1127    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1128    [HttpPost("UserItems/{itemId}/UserData")]
 1129    [ProducesResponseType(StatusCodes.Status200OK)]
 1130    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1131    [Tags("UserData")]
 1132    public ActionResult<UserItemDataDto?> UpdateItemUserData(
 1133        [FromQuery] Guid? userId,
 1134        [FromRoute, Required] Guid itemId,
 1135        [FromBody, Required] UpdateUserItemDataDto userDataDto)
 1136    {
 01137        var requestUserId = RequestHelpers.GetUserId(User, userId);
 01138        var user = _userManager.GetUserById(requestUserId);
 01139        if (user is null)
 1140        {
 01141            return NotFound();
 1142        }
 1143
 01144        if (!RequestHelpers.AssertCanUpdateUser(User, user, true))
 1145        {
 01146            return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update this item user data.");
 1147        }
 1148
 01149        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
 01150        if (item is null)
 1151        {
 01152            return NotFound();
 1153        }
 1154
 01155        _userDataRepository.SaveUserData(user, item, userDataDto, UserDataSaveReason.UpdateUserData);
 1156
 01157        return _userDataRepository.GetUserDataDto(item, user);
 1158    }
 1159
 1160    /// <summary>
 1161    /// Update Item User Data.
 1162    /// </summary>
 1163    /// <param name="userId">The user id.</param>
 1164    /// <param name="itemId">The item id.</param>
 1165    /// <param name="userDataDto">New user data object.</param>
 1166    /// <response code="200">return updated user item data.</response>
 1167    /// <response code="404">Item is not found.</response>
 1168    /// <returns>Return <see cref="UserItemDataDto"/>.</returns>
 1169    [HttpPost("Users/{userId}/Items/{itemId}/UserData")]
 1170    [ProducesResponseType(StatusCodes.Status200OK)]
 1171    [ProducesResponseType(StatusCodes.Status404NotFound)]
 1172    [Obsolete("Kept for backwards compatibility")]
 1173    [ApiExplorerSettings(IgnoreApi = true)]
 1174    public ActionResult<UserItemDataDto?> UpdateItemUserDataLegacy(
 1175        [FromRoute, Required] Guid userId,
 1176        [FromRoute, Required] Guid itemId,
 1177        [FromBody, Required] UpdateUserItemDataDto userDataDto)
 1178        => UpdateItemUserData(userId, itemId, userDataDto);
 1179}