< 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: 62
Coverable lines: 62
Total lines: 524
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 32
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
GetPlaylistUsers(...)0%2040%
GetPlaylistUser(...)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.Linq;
 5using System.Threading.Tasks;
 6using Jellyfin.Api.Attributes;
 7using Jellyfin.Api.Extensions;
 8using Jellyfin.Api.Helpers;
 9using Jellyfin.Api.ModelBinders;
 10using Jellyfin.Api.Models.PlaylistDtos;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Extensions;
 13using MediaBrowser.Controller.Dto;
 14using MediaBrowser.Controller.Library;
 15using MediaBrowser.Controller.Playlists;
 16using MediaBrowser.Model.Dto;
 17using MediaBrowser.Model.Entities;
 18using MediaBrowser.Model.Playlists;
 19using MediaBrowser.Model.Querying;
 20using Microsoft.AspNetCore.Authorization;
 21using Microsoft.AspNetCore.Http;
 22using Microsoft.AspNetCore.Mvc;
 23using Microsoft.AspNetCore.Mvc.ModelBinding;
 24
 25namespace Jellyfin.Api.Controllers;
 26
 27/// <summary>
 28/// Playlists controller.
 29/// </summary>
 30[Authorize]
 31public class PlaylistsController : BaseJellyfinApiController
 32{
 33    private readonly IPlaylistManager _playlistManager;
 34    private readonly IDtoService _dtoService;
 35    private readonly IUserManager _userManager;
 36    private readonly ILibraryManager _libraryManager;
 37
 38    /// <summary>
 39    /// Initializes a new instance of the <see cref="PlaylistsController"/> class.
 40    /// </summary>
 41    /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
 42    /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
 43    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 44    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 045    public PlaylistsController(
 046        IDtoService dtoService,
 047        IPlaylistManager playlistManager,
 048        IUserManager userManager,
 049        ILibraryManager libraryManager)
 50    {
 051        _dtoService = dtoService;
 052        _playlistManager = playlistManager;
 053        _userManager = userManager;
 054        _libraryManager = libraryManager;
 055    }
 56
 57    /// <summary>
 58    /// Creates a new playlist.
 59    /// </summary>
 60    /// <remarks>
 61    /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
 62    /// Query parameters are obsolete.
 63    /// </remarks>
 64    /// <param name="name">The playlist name.</param>
 65    /// <param name="ids">The item ids.</param>
 66    /// <param name="userId">The user id.</param>
 67    /// <param name="mediaType">The media type.</param>
 68    /// <param name="createPlaylistRequest">The create playlist payload.</param>
 69    /// <response code="200">Playlist created.</response>
 70    /// <returns>
 71    /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
 72    /// The task result contains an <see cref="OkResult"/> indicating success.
 73    /// </returns>
 74    [HttpPost]
 75    [ProducesResponseType(StatusCodes.Status200OK)]
 76    public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
 77        [FromQuery, ParameterObsolete] string? name,
 78        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
 79        [FromQuery, ParameterObsolete] Guid? userId,
 80        [FromQuery, ParameterObsolete] MediaType? mediaType,
 81        [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
 82    {
 83        if (ids.Count == 0)
 84        {
 85            ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
 86        }
 87
 88        userId ??= createPlaylistRequest?.UserId ?? default;
 89        userId = RequestHelpers.GetUserId(User, userId);
 90        var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
 91        {
 92            Name = name ?? createPlaylistRequest?.Name,
 93            ItemIdList = ids,
 94            UserId = userId.Value,
 95            MediaType = mediaType ?? createPlaylistRequest?.MediaType,
 96            Users = createPlaylistRequest?.Users.ToArray() ?? [],
 97            Public = createPlaylistRequest?.IsPublic
 98        }).ConfigureAwait(false);
 99
 100        return result;
 101    }
 102
 103    /// <summary>
 104    /// Updates a playlist.
 105    /// </summary>
 106    /// <param name="playlistId">The playlist id.</param>
 107    /// <param name="updatePlaylistRequest">The <see cref="UpdatePlaylistDto"/> id.</param>
 108    /// <response code="204">Playlist updated.</response>
 109    /// <response code="403">Access forbidden.</response>
 110    /// <response code="404">Playlist not found.</response>
 111    /// <returns>
 112    /// A <see cref="Task" /> that represents the asynchronous operation to update a playlist.
 113    /// The task result contains an <see cref="OkResult"/> indicating success.
 114    /// </returns>
 115    [HttpPost("{playlistId}")]
 116    [ProducesResponseType(StatusCodes.Status204NoContent)]
 117    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 118    [ProducesResponseType(StatusCodes.Status404NotFound)]
 119    public async Task<ActionResult> UpdatePlaylist(
 120        [FromRoute, Required] Guid playlistId,
 121        [FromBody, Required] UpdatePlaylistDto updatePlaylistRequest)
 122    {
 123        var callingUserId = User.GetUserId();
 124
 125        var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
 126        if (playlist is null)
 127        {
 128            return NotFound("Playlist not found");
 129        }
 130
 131        var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
 132            || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
 133
 134        if (!isPermitted)
 135        {
 136            return Forbid();
 137        }
 138
 139        await _playlistManager.UpdatePlaylist(new PlaylistUpdateRequest
 140        {
 141            UserId = callingUserId,
 142            Id = playlistId,
 143            Name = updatePlaylistRequest.Name,
 144            Ids = updatePlaylistRequest.Ids,
 145            Users = updatePlaylistRequest.Users,
 146            Public = updatePlaylistRequest.IsPublic
 147        }).ConfigureAwait(false);
 148
 149        return NoContent();
 150    }
 151
 152    /// <summary>
 153    /// Get a playlist's users.
 154    /// </summary>
 155    /// <param name="playlistId">The playlist id.</param>
 156    /// <response code="200">Found shares.</response>
 157    /// <response code="403">Access forbidden.</response>
 158    /// <response code="404">Playlist not found.</response>
 159    /// <returns>
 160    /// A list of <see cref="PlaylistUserPermissions"/> objects.
 161    /// </returns>
 162    [HttpGet("{playlistId}/Users")]
 163    [ProducesResponseType(StatusCodes.Status200OK)]
 164    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 165    [ProducesResponseType(StatusCodes.Status404NotFound)]
 166    public ActionResult<IReadOnlyList<PlaylistUserPermissions>> GetPlaylistUsers(
 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        var isPermitted = playlist.OwnerUserId.Equals(userId);
 178
 0179        return isPermitted ? playlist.Shares.ToList() : Forbid();
 180    }
 181
 182    /// <summary>
 183    /// Get a playlist user.
 184    /// </summary>
 185    /// <param name="playlistId">The playlist id.</param>
 186    /// <param name="userId">The user id.</param>
 187    /// <response code="200">User permission found.</response>
 188    /// <response code="403">Access forbidden.</response>
 189    /// <response code="404">Playlist not found.</response>
 190    /// <returns>
 191    /// <see cref="PlaylistUserPermissions"/>.
 192    /// </returns>
 193    [HttpGet("{playlistId}/Users/{userId}")]
 194    [ProducesResponseType(StatusCodes.Status200OK)]
 195    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 196    [ProducesResponseType(StatusCodes.Status404NotFound)]
 197    public ActionResult<PlaylistUserPermissions?> GetPlaylistUser(
 198        [FromRoute, Required] Guid playlistId,
 199        [FromRoute, Required] Guid userId)
 200    {
 0201        var callingUserId = User.GetUserId();
 202
 0203        var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
 0204        if (playlist is null)
 205        {
 0206            return NotFound("Playlist not found");
 207        }
 208
 0209        if (playlist.OwnerUserId.Equals(callingUserId))
 210        {
 0211            return new PlaylistUserPermissions(callingUserId, true);
 212        }
 213
 0214        var userPermission = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId));
 0215        var isPermitted = playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId))
 0216            || userId.Equals(callingUserId);
 217
 0218        if (!isPermitted)
 219        {
 0220            return Forbid();
 221        }
 222
 0223        if (userPermission is not null)
 224        {
 0225            return userPermission;
 226        }
 227
 0228        return NotFound("User permissions not found");
 229    }
 230
 231    /// <summary>
 232    /// Modify a user of a playlist's users.
 233    /// </summary>
 234    /// <param name="playlistId">The playlist id.</param>
 235    /// <param name="userId">The user id.</param>
 236    /// <param name="updatePlaylistUserRequest">The <see cref="UpdatePlaylistUserDto"/>.</param>
 237    /// <response code="204">User's permissions modified.</response>
 238    /// <response code="403">Access forbidden.</response>
 239    /// <response code="404">Playlist not found.</response>
 240    /// <returns>
 241    /// A <see cref="Task" /> that represents the asynchronous operation to modify an user's playlist permissions.
 242    /// The task result contains an <see cref="OkResult"/> indicating success.
 243    /// </returns>
 244    [HttpPost("{playlistId}/Users/{userId}")]
 245    [ProducesResponseType(StatusCodes.Status204NoContent)]
 246    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 247    [ProducesResponseType(StatusCodes.Status404NotFound)]
 248    public async Task<ActionResult> UpdatePlaylistUser(
 249        [FromRoute, Required] Guid playlistId,
 250        [FromRoute, Required] Guid userId,
 251        [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow), Required] UpdatePlaylistUserDto updatePlaylistUserReques
 252    {
 253        var callingUserId = User.GetUserId();
 254
 255        var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
 256        if (playlist is null)
 257        {
 258            return NotFound("Playlist not found");
 259        }
 260
 261        var isPermitted = playlist.OwnerUserId.Equals(callingUserId);
 262
 263        if (!isPermitted)
 264        {
 265            return Forbid();
 266        }
 267
 268        await _playlistManager.AddUserToShares(new PlaylistUserUpdateRequest
 269        {
 270            Id = playlistId,
 271            UserId = userId,
 272            CanEdit = updatePlaylistUserRequest.CanEdit
 273        }).ConfigureAwait(false);
 274
 275        return NoContent();
 276    }
 277
 278    /// <summary>
 279    /// Remove a user from a playlist's users.
 280    /// </summary>
 281    /// <param name="playlistId">The playlist id.</param>
 282    /// <param name="userId">The user id.</param>
 283    /// <response code="204">User permissions removed from playlist.</response>
 284    /// <response code="401">Unauthorized access.</response>
 285    /// <response code="404">No playlist or user permissions found.</response>
 286    /// <returns>
 287    /// A <see cref="Task" /> that represents the asynchronous operation to delete a user from a playlist's shares.
 288    /// The task result contains an <see cref="OkResult"/> indicating success.
 289    /// </returns>
 290    [HttpDelete("{playlistId}/Users/{userId}")]
 291    [ProducesResponseType(StatusCodes.Status204NoContent)]
 292    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 293    [ProducesResponseType(StatusCodes.Status404NotFound)]
 294    public async Task<ActionResult> RemoveUserFromPlaylist(
 295        [FromRoute, Required] Guid playlistId,
 296        [FromRoute, Required] Guid userId)
 297    {
 298        var callingUserId = User.GetUserId();
 299
 300        var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
 301        if (playlist is null)
 302        {
 303            return NotFound("Playlist not found");
 304        }
 305
 306        var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
 307            || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
 308
 309        if (!isPermitted)
 310        {
 311            return Forbid();
 312        }
 313
 314        var share = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId));
 315        if (share is null)
 316        {
 317            return NotFound("User permissions not found");
 318        }
 319
 320        await _playlistManager.RemoveUserFromShares(playlistId, callingUserId, share).ConfigureAwait(false);
 321
 322        return NoContent();
 323    }
 324
 325    /// <summary>
 326    /// Adds items to a playlist.
 327    /// </summary>
 328    /// <param name="playlistId">The playlist id.</param>
 329    /// <param name="ids">Item id, comma delimited.</param>
 330    /// <param name="userId">The userId.</param>
 331    /// <response code="204">Items added to playlist.</response>
 332    /// <response code="403">Access forbidden.</response>
 333    /// <response code="404">Playlist not found.</response>
 334    /// <returns>An <see cref="NoContentResult"/> on success.</returns>
 335    [HttpPost("{playlistId}/Items")]
 336    [ProducesResponseType(StatusCodes.Status204NoContent)]
 337    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 338    [ProducesResponseType(StatusCodes.Status404NotFound)]
 339    public async Task<ActionResult> AddItemToPlaylist(
 340        [FromRoute, Required] Guid playlistId,
 341        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
 342        [FromQuery] Guid? userId)
 343    {
 344        userId = RequestHelpers.GetUserId(User, userId);
 345        var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId.Value);
 346        if (playlist is null)
 347        {
 348            return NotFound("Playlist not found");
 349        }
 350
 351        var isPermitted = playlist.OwnerUserId.Equals(userId.Value)
 352            || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(userId.Value));
 353
 354        if (!isPermitted)
 355        {
 356            return Forbid();
 357        }
 358
 359        await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false);
 360        return NoContent();
 361    }
 362
 363    /// <summary>
 364    /// Moves a playlist item.
 365    /// </summary>
 366    /// <param name="playlistId">The playlist id.</param>
 367    /// <param name="itemId">The item id.</param>
 368    /// <param name="newIndex">The new index.</param>
 369    /// <response code="204">Item moved to new index.</response>
 370    /// <response code="403">Access forbidden.</response>
 371    /// <response code="404">Playlist not found.</response>
 372    /// <returns>An <see cref="NoContentResult"/> on success.</returns>
 373    [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
 374    [ProducesResponseType(StatusCodes.Status204NoContent)]
 375    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 376    [ProducesResponseType(StatusCodes.Status404NotFound)]
 377    public async Task<ActionResult> MoveItem(
 378        [FromRoute, Required] string playlistId,
 379        [FromRoute, Required] string itemId,
 380        [FromRoute, Required] int newIndex)
 381    {
 382        var callingUserId = User.GetUserId();
 383
 384        var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId);
 385        if (playlist is null)
 386        {
 387            return NotFound("Playlist not found");
 388        }
 389
 390        var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
 391            || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
 392
 393        if (!isPermitted)
 394        {
 395            return Forbid();
 396        }
 397
 398        await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
 399        return NoContent();
 400    }
 401
 402    /// <summary>
 403    /// Removes items from a playlist.
 404    /// </summary>
 405    /// <param name="playlistId">The playlist id.</param>
 406    /// <param name="entryIds">The item ids, comma delimited.</param>
 407    /// <response code="204">Items removed.</response>
 408    /// <response code="403">Access forbidden.</response>
 409    /// <response code="404">Playlist not found.</response>
 410    /// <returns>An <see cref="NoContentResult"/> on success.</returns>
 411    [HttpDelete("{playlistId}/Items")]
 412    [ProducesResponseType(StatusCodes.Status204NoContent)]
 413    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 414    [ProducesResponseType(StatusCodes.Status404NotFound)]
 415    public async Task<ActionResult> RemoveItemFromPlaylist(
 416        [FromRoute, Required] string playlistId,
 417        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
 418    {
 419        var callingUserId = User.GetUserId();
 420
 421        var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId);
 422        if (playlist is null)
 423        {
 424            return NotFound("Playlist not found");
 425        }
 426
 427        var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
 428            || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
 429
 430        if (!isPermitted)
 431        {
 432            return Forbid();
 433        }
 434
 435        await _playlistManager.RemoveItemFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
 436        return NoContent();
 437    }
 438
 439    /// <summary>
 440    /// Gets the original items of a playlist.
 441    /// </summary>
 442    /// <param name="playlistId">The playlist id.</param>
 443    /// <param name="userId">User id.</param>
 444    /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped fr
 445    /// <param name="limit">Optional. The maximum number of records to return.</param>
 446    /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
 447    /// <param name="enableImages">Optional. Include image information in output.</param>
 448    /// <param name="enableUserData">Optional. Include user data.</param>
 449    /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
 450    /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
 451    /// <response code="200">Original playlist returned.</response>
 452    /// <response code="404">Access forbidden.</response>
 453    /// <response code="404">Playlist not found.</response>
 454    /// <returns>The original playlist items.</returns>
 455    [HttpGet("{playlistId}/Items")]
 456    [ProducesResponseType(StatusCodes.Status200OK)]
 457    [ProducesResponseType(StatusCodes.Status403Forbidden)]
 458    [ProducesResponseType(StatusCodes.Status404NotFound)]
 459    public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
 460        [FromRoute, Required] Guid playlistId,
 461        [FromQuery] Guid? userId,
 462        [FromQuery] int? startIndex,
 463        [FromQuery] int? limit,
 464        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
 465        [FromQuery] bool? enableImages,
 466        [FromQuery] bool? enableUserData,
 467        [FromQuery] int? imageTypeLimit,
 468        [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
 469    {
 0470        userId = RequestHelpers.GetUserId(User, userId);
 0471        var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId.Value);
 0472        if (playlist is null)
 473        {
 0474            return NotFound("Playlist not found");
 475        }
 476
 0477        var isPermitted = playlist.OpenAccess
 0478            || playlist.OwnerUserId.Equals(userId.Value)
 0479            || playlist.Shares.Any(s => s.UserId.Equals(userId.Value));
 480
 0481        if (!isPermitted)
 482        {
 0483            return Forbid();
 484        }
 485
 0486        var user = userId.IsNullOrEmpty()
 0487            ? null
 0488            : _userManager.GetUserById(userId.Value);
 0489        var item = _libraryManager.GetItemById<Playlist>(playlistId, user);
 0490        if (item is null)
 491        {
 0492            return NotFound();
 493        }
 494
 0495        var items = item.GetManageableItems().ToArray();
 0496        var count = items.Length;
 0497        if (startIndex.HasValue)
 498        {
 0499            items = items.Skip(startIndex.Value).ToArray();
 500        }
 501
 0502        if (limit.HasValue)
 503        {
 0504            items = items.Take(limit.Value).ToArray();
 505        }
 506
 0507        var dtoOptions = new DtoOptions { Fields = fields }
 0508            .AddClientFields(User)
 0509            .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 510
 0511        var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
 0512        for (int index = 0; index < dtos.Count; index++)
 513        {
 0514            dtos[index].PlaylistItemId = items[index].Item1.Id;
 515        }
 516
 0517        var result = new QueryResult<BaseItemDto>(
 0518            startIndex,
 0519            count,
 0520            dtos);
 521
 0522        return result;
 523    }
 524}