< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.PlaylistsController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/PlaylistsController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 168
Coverable lines: 168
Total lines: 569
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 98
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 0% (0/66) Branch coverage: 0% (0/34) Total lines: 5662/15/2026 - 12:13:43 AM Line coverage: 0% (0/66) Branch coverage: 0% (0/34) Total lines: 5684/19/2026 - 12:14:27 AM Line coverage: 0% (0/168) Branch coverage: 0% (0/98) Total lines: 5684/27/2026 - 12:15:04 AM Line coverage: 0% (0/168) Branch coverage: 0% (0/98) Total lines: 569 1/23/2026 - 12:11:06 AM Line coverage: 0% (0/66) Branch coverage: 0% (0/34) Total lines: 5662/15/2026 - 12:13:43 AM Line coverage: 0% (0/66) Branch coverage: 0% (0/34) Total lines: 5684/19/2026 - 12:14:27 AM Line coverage: 0% (0/168) Branch coverage: 0% (0/98) Total lines: 5684/27/2026 - 12:15:04 AM Line coverage: 0% (0/168) Branch coverage: 0% (0/98) Total lines: 569

Coverage delta

Coverage delta 1 -1

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
CreatePlaylist()0%600240%
UpdatePlaylist()0%4260%
GetPlaylist(...)0%620%
GetPlaylistUsers(...)0%2040%
GetPlaylistUser(...)0%110100%
UpdatePlaylistUser()0%2040%
RemoveUserFromPlaylist()0%7280%
AddItemToPlaylist()0%4260%
MoveItem()0%4260%
RemoveItemFromPlaylist()0%110100%
GetPlaylistItems(...)0%342180%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.ComponentModel.DataAnnotations;
 4using System.Globalization;
 5using System.Linq;
 6using System.Threading.Tasks;
 7using Jellyfin.Api.Attributes;
 8using Jellyfin.Api.Extensions;
 9using Jellyfin.Api.Helpers;
 10using Jellyfin.Api.ModelBinders;
 11using Jellyfin.Api.Models.PlaylistDtos;
 12using Jellyfin.Data.Enums;
 13using Jellyfin.Extensions;
 14using MediaBrowser.Controller.Dto;
 15using MediaBrowser.Controller.Library;
 16using MediaBrowser.Controller.Playlists;
 17using MediaBrowser.Model.Dto;
 18using MediaBrowser.Model.Entities;
 19using MediaBrowser.Model.Playlists;
 20using MediaBrowser.Model.Querying;
 21using Microsoft.AspNetCore.Authorization;
 22using Microsoft.AspNetCore.Http;
 23using Microsoft.AspNetCore.Mvc;
 24using Microsoft.AspNetCore.Mvc.ModelBinding;
 25
 26namespace Jellyfin.Api.Controllers;
 27
 28/// <summary>
 29/// Playlists controller.
 30/// </summary>
 31[Authorize]
 32[Tags("Playlist")]
 33public class PlaylistsController : BaseJellyfinApiController
 34{
 35    private readonly IPlaylistManager _playlistManager;
 36    private readonly IDtoService _dtoService;
 37    private readonly IUserManager _userManager;
 38    private readonly ILibraryManager _libraryManager;
 39
 40    /// <summary>
 41    /// Initializes a new instance of the <see cref="PlaylistsController"/> class.
 42    /// </summary>
 43    /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
 44    /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
 45    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 46    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 047    public PlaylistsController(
 048        IDtoService dtoService,
 049        IPlaylistManager playlistManager,
 050        IUserManager userManager,
 051        ILibraryManager libraryManager)
 52    {
 053        _dtoService = dtoService;
 054        _playlistManager = playlistManager;
 055        _userManager = userManager;
 056        _libraryManager = libraryManager;
 057    }
 58
 59    /// <summary>
 60    /// Creates a new playlist.
 61    /// </summary>
 62    /// <remarks>
 63    /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
 64    /// Query parameters are obsolete.
 65    /// </remarks>
 66    /// <param name="name">The playlist name.</param>
 67    /// <param name="ids">The item ids.</param>
 68    /// <param name="userId">The user id.</param>
 69    /// <param name="mediaType">The media type.</param>
 70    /// <param name="createPlaylistRequest">The create playlist payload.</param>
 71    /// <response code="200">Playlist created.</response>
 72    /// <returns>
 73    /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
 74    /// The task result contains an <see cref="OkResult"/> indicating success.
 75    /// </returns>
 76    [HttpPost]
 77    [ProducesResponseType(StatusCodes.Status200OK)]
 78    public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
 79        [FromQuery, ParameterObsolete] string? name,
 80        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids
 81        [FromQuery, ParameterObsolete] Guid? userId,
 82        [FromQuery, ParameterObsolete] MediaType? mediaType,
 83        [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
 84    {
 085        if (ids.Count == 0)
 86        {
 087            ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
 88        }
 89
 090        userId ??= createPlaylistRequest?.UserId ?? default;
 091        userId = RequestHelpers.GetUserId(User, userId);
 092        var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
 093        {
 094            Name = name ?? createPlaylistRequest?.Name,
 095            ItemIdList = ids,
 096            UserId = userId.Value,
 097            MediaType = mediaType ?? createPlaylistRequest?.MediaType,
 098            Users = createPlaylistRequest?.Users.ToArray() ?? [],
 099            Public = createPlaylistRequest?.IsPublic
 0100        }).ConfigureAwait(false);
 101
 0102        return result;
 0103    }
 104
 105    /// <summary>
 106    /// Updates a playlist.
 107    /// </summary>
 108    /// <param name="playlistId">The playlist id.</param>
 109    /// <param name="updatePlaylistRequest">The <see cref="UpdatePlaylistDto"/> id.</param>
 110    /// <response code="204">Playlist updated.</response>
 111    /// <response code="403">Access forbidden.</response>
 112    /// <response code="404">Playlist not found.</response>
 113    /// <returns>
 114    /// A <see cref="Task" /> that represents the asynchronous operation to update a playlist.
 115    /// The task result contains an <see cref="OkResult"/> indicating success.
 116    /// </returns>
 117    [HttpPost("{playlistId}")]
 118    [ProducesResponseType(StatusCodes.Status204NoContent)]
 119    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 120    [ProducesResponseType(StatusCodes.Status404NotFound)]
 121    public async Task<ActionResult> UpdatePlaylist(
 122        [FromRoute, Required] Guid playlistId,
 123        [FromBody, Required] UpdatePlaylistDto updatePlaylistRequest)
 124    {
 0125        var callingUserId = User.GetUserId();
 126
 0127        var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
 0128        if (playlist is null)
 129        {
 0130            return NotFound("Playlist not found");
 131        }
 132
 0133        var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
 0134            || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
 135
 0136        if (!isPermitted)
 137        {
 0138            return Forbid();
 139        }
 140
 0141        await _playlistManager.UpdatePlaylist(new PlaylistUpdateRequest
 0142        {
 0143            UserId = callingUserId,
 0144            Id = playlistId,
 0145            Name = updatePlaylistRequest.Name,
 0146            Ids = updatePlaylistRequest.Ids,
 0147            Users = updatePlaylistRequest.Users,
 0148            Public = updatePlaylistRequest.IsPublic
 0149        }).ConfigureAwait(false);
 150
 0151        return NoContent();
 0152    }
 153
 154    /// <summary>
 155    /// Get a playlist.
 156    /// </summary>
 157    /// <param name="playlistId">The playlist id.</param>
 158    /// <response code="200">The playlist.</response>
 159    /// <response code="404">Playlist not found.</response>
 160    /// <returns>
 161    /// A <see cref="Playlist"/> objects.
 162    /// </returns>
 163    [HttpGet("{playlistId}")]
 164    [ProducesResponseType(StatusCodes.Status200OK)]
 165    [ProducesResponseType(StatusCodes.Status404NotFound)]
 166    public ActionResult<PlaylistDto> GetPlaylist(
 167        [FromRoute, Required] Guid playlistId)
 168    {
 0169        var userId = User.GetUserId();
 170
 0171        var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId);
 0172        if (playlist is null)
 173        {
 0174            return NotFound("Playlist not found");
 175        }
 176
 0177        return new PlaylistDto()
 0178        {
 0179            Shares = playlist.Shares,
 0180            OpenAccess = playlist.OpenAccess,
 0181            ItemIds = playlist.GetManageableItems().Select(t => t.Item2.Id).ToList()
 0182        };
 183    }
 184
 185    /// <summary>
 186    /// Get a playlist's users.
 187    /// </summary>
 188    /// <param name="playlistId">The playlist id.</param>
 189    /// <response code="200">Found shares.</response>
 190    /// <response code="403">Access forbidden.</response>
 191    /// <response code="404">Playlist not found.</response>
 192    /// <returns>
 193    /// A list of <see cref="PlaylistUserPermissions"/> objects.
 194    /// </returns>
 195    [HttpGet("{playlistId}/Users")]
 196    [ProducesResponseType(StatusCodes.Status200OK)]
 197    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 198    [ProducesResponseType(StatusCodes.Status404NotFound)]
 199    public ActionResult<IReadOnlyList<PlaylistUserPermissions>> GetPlaylistUsers(
 200        [FromRoute, Required] Guid playlistId)
 201    {
 0202        var userId = User.GetUserId();
 203
 0204        var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId);
 0205        if (playlist is null)
 206        {
 0207            return NotFound("Playlist not found");
 208        }
 209
 0210        var isPermitted = playlist.OwnerUserId.Equals(userId);
 211
 0212        return isPermitted ? playlist.Shares.ToList() : Forbid();
 213    }
 214
 215    /// <summary>
 216    /// Get a playlist user.
 217    /// </summary>
 218    /// <param name="playlistId">The playlist id.</param>
 219    /// <param name="userId">The user id.</param>
 220    /// <response code="200">User permission found.</response>
 221    /// <response code="403">Access forbidden.</response>
 222    /// <response code="404">Playlist not found.</response>
 223    /// <returns>
 224    /// <see cref="PlaylistUserPermissions"/>.
 225    /// </returns>
 226    [HttpGet("{playlistId}/Users/{userId}")]
 227    [ProducesResponseType(StatusCodes.Status200OK)]
 228    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 229    [ProducesResponseType(StatusCodes.Status404NotFound)]
 230    public ActionResult<PlaylistUserPermissions?> GetPlaylistUser(
 231        [FromRoute, Required] Guid playlistId,
 232        [FromRoute, Required] Guid userId)
 233    {
 0234        var callingUserId = User.GetUserId();
 235
 0236        var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
 0237        if (playlist is null)
 238        {
 0239            return NotFound("Playlist not found");
 240        }
 241
 0242        if (playlist.OwnerUserId.Equals(callingUserId))
 243        {
 0244            return new PlaylistUserPermissions(callingUserId, true);
 245        }
 246
 0247        var userPermission = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId));
 0248        var isPermitted = playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId))
 0249            || userId.Equals(callingUserId);
 250
 0251        if (!isPermitted)
 252        {
 0253            return Forbid();
 254        }
 255
 0256        if (userPermission is not null)
 257        {
 0258            return userPermission;
 259        }
 260
 0261        return NotFound("User permissions not found");
 262    }
 263
 264    /// <summary>
 265    /// Modify a user of a playlist's users.
 266    /// </summary>
 267    /// <param name="playlistId">The playlist id.</param>
 268    /// <param name="userId">The user id.</param>
 269    /// <param name="updatePlaylistUserRequest">The <see cref="UpdatePlaylistUserDto"/>.</param>
 270    /// <response code="204">User's permissions modified.</response>
 271    /// <response code="403">Access forbidden.</response>
 272    /// <response code="404">Playlist not found.</response>
 273    /// <returns>
 274    /// A <see cref="Task" /> that represents the asynchronous operation to modify an user's playlist permissions.
 275    /// The task result contains an <see cref="OkResult"/> indicating success.
 276    /// </returns>
 277    [HttpPost("{playlistId}/Users/{userId}")]
 278    [ProducesResponseType(StatusCodes.Status204NoContent)]
 279    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 280    [ProducesResponseType(StatusCodes.Status404NotFound)]
 281    public async Task<ActionResult> UpdatePlaylistUser(
 282        [FromRoute, Required] Guid playlistId,
 283        [FromRoute, Required] Guid userId,
 284        [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow), Required] UpdatePlaylistUserDto updatePlaylistUserReques
 285    {
 0286        var callingUserId = User.GetUserId();
 287
 0288        var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
 0289        if (playlist is null)
 290        {
 0291            return NotFound("Playlist not found");
 292        }
 293
 0294        var isPermitted = playlist.OwnerUserId.Equals(callingUserId);
 295
 0296        if (!isPermitted)
 297        {
 0298            return Forbid();
 299        }
 300
 0301        await _playlistManager.AddUserToShares(new PlaylistUserUpdateRequest
 0302        {
 0303            Id = playlistId,
 0304            UserId = userId,
 0305            CanEdit = updatePlaylistUserRequest.CanEdit
 0306        }).ConfigureAwait(false);
 307
 0308        return NoContent();
 0309    }
 310
 311    /// <summary>
 312    /// Remove a user from a playlist's users.
 313    /// </summary>
 314    /// <param name="playlistId">The playlist id.</param>
 315    /// <param name="userId">The user id.</param>
 316    /// <response code="204">User permissions removed from playlist.</response>
 317    /// <response code="401">Unauthorized access.</response>
 318    /// <response code="404">No playlist or user permissions found.</response>
 319    /// <returns>
 320    /// A <see cref="Task" /> that represents the asynchronous operation to delete a user from a playlist's shares.
 321    /// The task result contains an <see cref="OkResult"/> indicating success.
 322    /// </returns>
 323    [HttpDelete("{playlistId}/Users/{userId}")]
 324    [ProducesResponseType(StatusCodes.Status204NoContent)]
 325    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 326    [ProducesResponseType(StatusCodes.Status404NotFound)]
 327    public async Task<ActionResult> RemoveUserFromPlaylist(
 328        [FromRoute, Required] Guid playlistId,
 329        [FromRoute, Required] Guid userId)
 330    {
 0331        var callingUserId = User.GetUserId();
 332
 0333        var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
 0334        if (playlist is null)
 335        {
 0336            return NotFound("Playlist not found");
 337        }
 338
 0339        var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
 0340            || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
 341
 0342        if (!isPermitted)
 343        {
 0344            return Forbid();
 345        }
 346
 0347        var share = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId));
 0348        if (share is null)
 349        {
 0350            return NotFound("User permissions not found");
 351        }
 352
 0353        await _playlistManager.RemoveUserFromShares(playlistId, callingUserId, share).ConfigureAwait(false);
 354
 0355        return NoContent();
 0356    }
 357
 358    /// <summary>
 359    /// Adds items to a playlist.
 360    /// </summary>
 361    /// <param name="playlistId">The playlist id.</param>
 362    /// <param name="ids">Item id, comma delimited.</param>
 363    /// <param name="position">Optional. 0-based index where to place the items or at the end if <c>null</c>.</param>
 364    /// <param name="userId">The userId.</param>
 365    /// <response code="204">Items added to playlist.</response>
 366    /// <response code="403">Access forbidden.</response>
 367    /// <response code="404">Playlist not found.</response>
 368    /// <returns>An <see cref="NoContentResult"/> on success.</returns>
 369    [HttpPost("{playlistId}/Items")]
 370    [ProducesResponseType(StatusCodes.Status204NoContent)]
 371    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 372    [ProducesResponseType(StatusCodes.Status404NotFound)]
 373    public async Task<ActionResult> AddItemToPlaylist(
 374        [FromRoute, Required] Guid playlistId,
 375        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids,
 376        [FromQuery] int? position,
 377        [FromQuery] Guid? userId)
 378    {
 0379        userId = RequestHelpers.GetUserId(User, userId);
 0380        var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId.Value);
 0381        if (playlist is null)
 382        {
 0383            return NotFound("Playlist not found");
 384        }
 385
 0386        var isPermitted = playlist.OwnerUserId.Equals(userId.Value)
 0387            || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(userId.Value));
 388
 0389        if (!isPermitted)
 390        {
 0391            return Forbid();
 392        }
 393
 0394        await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, position, userId.Value).ConfigureAwait(false);
 0395        return NoContent();
 0396    }
 397
 398    /// <summary>
 399    /// Moves a playlist item.
 400    /// </summary>
 401    /// <param name="playlistId">The playlist id.</param>
 402    /// <param name="itemId">The item id.</param>
 403    /// <param name="newIndex">The new index.</param>
 404    /// <response code="204">Item moved to new index.</response>
 405    /// <response code="403">Access forbidden.</response>
 406    /// <response code="404">Playlist not found.</response>
 407    /// <returns>An <see cref="NoContentResult"/> on success.</returns>
 408    [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
 409    [ProducesResponseType(StatusCodes.Status204NoContent)]
 410    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 411    [ProducesResponseType(StatusCodes.Status404NotFound)]
 412    public async Task<ActionResult> MoveItem(
 413        [FromRoute, Required] string playlistId,
 414        [FromRoute, Required] string itemId,
 415        [FromRoute, Required] int newIndex)
 416    {
 0417        var callingUserId = User.GetUserId();
 418
 0419        var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId);
 0420        if (playlist is null)
 421        {
 0422            return NotFound("Playlist not found");
 423        }
 424
 0425        var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
 0426            || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
 427
 0428        if (!isPermitted)
 429        {
 0430            return Forbid();
 431        }
 432
 0433        await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex, callingUserId).ConfigureAwait(false);
 0434        return NoContent();
 0435    }
 436
 437    /// <summary>
 438    /// Removes items from a playlist.
 439    /// </summary>
 440    /// <param name="playlistId">The playlist id.</param>
 441    /// <param name="entryIds">The item ids, comma delimited.</param>
 442    /// <response code="204">Items removed.</response>
 443    /// <response code="403">Access forbidden.</response>
 444    /// <response code="404">Playlist not found.</response>
 445    /// <returns>An <see cref="NoContentResult"/> on success.</returns>
 446    [HttpDelete("{playlistId}/Items")]
 447    [ProducesResponseType(StatusCodes.Status204NoContent)]
 448    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 449    [ProducesResponseType(StatusCodes.Status404NotFound)]
 450    public async Task<ActionResult> RemoveItemFromPlaylist(
 451        [FromRoute, Required] string playlistId,
 452        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] entryIds)
 453    {
 0454        var callingUserId = User.GetUserId();
 455
 0456        if (!callingUserId.IsEmpty())
 457        {
 0458            var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId);
 0459            if (playlist is null)
 460            {
 0461                return NotFound("Playlist not found");
 462            }
 463
 0464            var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
 0465                              || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
 466
 0467            if (!isPermitted)
 468            {
 0469                return Forbid();
 470            }
 471        }
 472        else
 473        {
 0474            var isApiKey = User.GetIsApiKey();
 475
 0476            if (!isApiKey)
 477            {
 0478                return Forbid();
 479            }
 480        }
 481
 482        try
 483        {
 0484            await _playlistManager.RemoveItemFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
 0485            return NoContent();
 486        }
 0487        catch (ArgumentException)
 488        {
 0489            return NotFound();
 490        }
 0491    }
 492
 493    /// <summary>
 494    /// Gets the original items of a playlist.
 495    /// </summary>
 496    /// <param name="playlistId">The playlist id.</param>
 497    /// <param name="userId">User id.</param>
 498    /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped fr
 499    /// <param name="limit">Optional. The maximum number of records to return.</param>
 500    /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 501    /// <param name="enableImages">Optional. Include image information in output.</param>
 502    /// <param name="enableUserData">Optional. Include user data.</param>
 503    /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 504    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 505    /// <response code="200">Original playlist returned.</response>
 506    /// <response code="404">Access forbidden.</response>
 507    /// <response code="404">Playlist not found.</response>
 508    /// <returns>The original playlist items.</returns>
 509    [HttpGet("{playlistId}/Items")]
 510    [ProducesResponseType(StatusCodes.Status200OK)]
 511    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 512    [ProducesResponseType(StatusCodes.Status404NotFound)]
 513    public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
 514        [FromRoute, Required] Guid playlistId,
 515        [FromQuery] Guid? userId,
 516        [FromQuery] int? startIndex,
 517        [FromQuery] int? limit,
 518        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
 519        [FromQuery] bool? enableImages,
 520        [FromQuery] bool? enableUserData,
 521        [FromQuery] int? imageTypeLimit,
 522        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes)
 523    {
 0524        var callingUserId = userId ?? User.GetUserId();
 0525        var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
 0526        if (playlist is null)
 527        {
 0528            return NotFound("Playlist not found");
 529        }
 530
 0531        var isPermitted = playlist.OpenAccess
 0532            || playlist.OwnerUserId.Equals(callingUserId)
 0533            || playlist.Shares.Any(s => s.UserId.Equals(callingUserId));
 534
 0535        if (!isPermitted)
 536        {
 0537            return Forbid();
 538        }
 539
 0540        var user = _userManager.GetUserById(callingUserId);
 0541        var items = playlist.GetManageableItems().Where(i => i.Item2.IsVisible(user)).ToArray();
 0542        var count = items.Length;
 0543        if (startIndex.HasValue)
 544        {
 0545            items = items.Skip(startIndex.Value).ToArray();
 546        }
 547
 0548        if (limit.HasValue)
 549        {
 0550            items = items.Take(limit.Value).ToArray();
 551        }
 552
 0553        var dtoOptions = new DtoOptions { Fields = fields }
 0554            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 555
 0556        var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
 0557        for (int index = 0; index < dtos.Count; index++)
 558        {
 0559            dtos[index].PlaylistItemId = items[index].Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture);
 560        }
 561
 0562        var result = new QueryResult<BaseItemDto>(
 0563            startIndex,
 0564            count,
 0565            dtos);
 566
 0567        return result;
 568    }
 569}