Move userId in API from route to optional query parameter (#11074)

* Move userId in API from route to optional query parameter

* Standardize UserViewsController

* Move userId to query in ImageController

* Move userId to query in ItemsController

* Move userId to query in PlaystateController

* Move userId to query in SuggestionsController

* Move userId from route to query in UserLibraryController

* Clean up routes

* Move userId to query in UserController

* fix bad merge

---------

Co-authored-by: Niels van Velzen <git@ndat.nl>
This commit is contained in:
Cody Robibero 2024-03-03 13:51:31 -07:00 committed by GitHub
parent 8d40d431e8
commit 6e5ec99ea1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 869 additions and 291 deletions

View File

@ -11,7 +11,9 @@ using System.Security.Cryptography;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Extensions;
using MediaBrowser.Common.Api; using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -86,31 +88,26 @@ public class ImageController : BaseJellyfinApiController
/// Sets the user image. /// Sets the user image.
/// </summary> /// </summary>
/// <param name="userId">User Id.</param> /// <param name="userId">User Id.</param>
/// <param name="imageType">(Unused) Image type.</param>
/// <param name="index">(Unused) Image index.</param>
/// <response code="204">Image updated.</response> /// <response code="204">Image updated.</response>
/// <response code="403">User does not have permission to delete the image.</response> /// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}")] [HttpPost("UserImage")]
[Authorize] [Authorize]
[AcceptsImageFile] [AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> PostUserImage( public async Task<ActionResult> PostUserImage(
[FromRoute, Required] Guid userId, [FromQuery] Guid? userId)
[FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
} }
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, requestUserId, true))
{ {
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
} }
@ -142,6 +139,28 @@ public class ImageController : BaseJellyfinApiController
} }
} }
/// <summary>
/// Sets the user image.
/// </summary>
/// <param name="userId">User Id.</param>
/// <param name="imageType">(Unused) Image type.</param>
/// <response code="204">Image updated.</response>
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}")]
[Authorize]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
public Task<ActionResult> PostUserImageLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType)
=> PostUserImage(userId);
/// <summary> /// <summary>
/// Sets the user image. /// Sets the user image.
/// </summary> /// </summary>
@ -153,53 +172,57 @@ public class ImageController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}/{index}")] [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
[Authorize] [Authorize]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[AcceptsImageFile] [AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> PostUserImageByIndex( public Task<ActionResult> PostUserImageByIndexLegacy(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute] int index) [FromRoute] int index)
=> PostUserImage(userId);
/// <summary>
/// Delete the user's image.
/// </summary>
/// <param name="userId">User Id.</param>
/// <response code="204">Image deleted.</response>
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("UserImage")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> DeleteUserImage(
[FromQuery] Guid? userId)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
if (user is null) if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, requestUserId, true))
{ {
return NotFound(); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
} }
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) var user = _userManager.GetUserById(requestUserId);
if (user?.ProfileImage is null)
{ {
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
{
return BadRequest("Incorrect ContentType.");
}
var stream = GetFromBase64Stream(Request.Body);
await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage is not null)
{
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager
.SaveImage(stream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
try
{
System.IO.File.Delete(user.ProfileImage.Path);
}
catch (IOException e)
{
_logger.LogError(e, "Error deleting user profile image:");
}
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
return NoContent();
} }
/// <summary> /// <summary>
@ -213,38 +236,17 @@ public class ImageController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Users/{userId}/Images/{imageType}")] [HttpDelete("Users/{userId}/Images/{imageType}")]
[Authorize] [Authorize]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> DeleteUserImage( public Task<ActionResult> DeleteUserImageLegacy(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null) [FromQuery] int? index = null)
{ => DeleteUserImage(userId);
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
var user = _userManager.GetUserById(userId);
if (user?.ProfileImage is null)
{
return NoContent();
}
try
{
System.IO.File.Delete(user.ProfileImage.Path);
}
catch (IOException e)
{
_logger.LogError(e, "Error deleting user profile image:");
}
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
return NoContent();
}
/// <summary> /// <summary>
/// Delete the user's image. /// Delete the user's image.
@ -257,38 +259,17 @@ public class ImageController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Users/{userId}/Images/{imageType}/{index}")] [HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
[Authorize] [Authorize]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> DeleteUserImageByIndex( public Task<ActionResult> DeleteUserImageByIndexLegacy(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute] int index) [FromRoute] int index)
{ => DeleteUserImage(userId);
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
var user = _userManager.GetUserById(userId);
if (user?.ProfileImage is null)
{
return NoContent();
}
try
{
System.IO.File.Delete(user.ProfileImage.Path);
}
catch (IOException e)
{
_logger.LogError(e, "Error deleting user profile image:");
}
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
return NoContent();
}
/// <summary> /// <summary>
/// Delete an item's image. /// Delete an item's image.
@ -541,7 +522,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param> /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param> /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param> /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param> /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
@ -571,7 +551,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery] string? tag, [FromQuery] string? tag,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] ImageFormat? format, [FromQuery] ImageFormat? format,
[FromQuery] double? percentPlayed, [FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount, [FromQuery] int? unplayedCount,
@ -622,7 +601,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param> /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param> /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param> /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param> /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
@ -652,7 +630,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery] string? tag, [FromQuery] string? tag,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] ImageFormat? format, [FromQuery] ImageFormat? format,
[FromQuery] double? percentPlayed, [FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount, [FromQuery] int? unplayedCount,
@ -701,7 +678,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param> /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param> /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param> /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param> /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
@ -731,7 +707,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromRoute, Required] string tag, [FromRoute, Required] string tag,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromRoute, Required] ImageFormat format, [FromRoute, Required] ImageFormat format,
[FromRoute, Required] double percentPlayed, [FromRoute, Required] double percentPlayed,
[FromRoute, Required] int unplayedCount, [FromRoute, Required] int unplayedCount,
@ -784,7 +759,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="blur">Optional. Blur image.</param> /// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param> /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param> /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
@ -814,7 +788,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality, [FromQuery] int? quality,
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
@ -864,7 +837,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="blur">Optional. Blur image.</param> /// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param> /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param> /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
@ -894,7 +866,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality, [FromQuery] int? quality,
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
@ -945,7 +916,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="blur">Optional. Blur image.</param> /// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param> /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param> /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
@ -975,7 +945,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality, [FromQuery] int? quality,
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer) [FromQuery] string? foregroundLayer)
@ -1024,7 +993,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="blur">Optional. Blur image.</param> /// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param> /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param> /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
@ -1054,7 +1022,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality, [FromQuery] int? quality,
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
@ -1105,7 +1072,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="blur">Optional. Blur image.</param> /// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param> /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param> /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
@ -1135,7 +1101,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality, [FromQuery] int? quality,
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer) [FromQuery] string? foregroundLayer)
@ -1184,7 +1149,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="blur">Optional. Blur image.</param> /// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param> /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param> /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
@ -1214,7 +1178,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality, [FromQuery] int? quality,
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
@ -1265,7 +1228,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="blur">Optional. Blur image.</param> /// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param> /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param> /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
@ -1295,7 +1257,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality, [FromQuery] int? quality,
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer) [FromQuery] string? foregroundLayer)
@ -1344,7 +1305,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="blur">Optional. Blur image.</param> /// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param> /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param> /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
@ -1374,7 +1334,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality, [FromQuery] int? quality,
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
@ -1425,7 +1384,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="blur">Optional. Blur image.</param> /// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param> /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param> /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
@ -1455,7 +1413,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality, [FromQuery] int? quality,
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer) [FromQuery] string? foregroundLayer)
@ -1492,7 +1449,6 @@ public class ImageController : BaseJellyfinApiController
/// Get user profile image. /// Get user profile image.
/// </summary> /// </summary>
/// <param name="userId">User id.</param> /// <param name="userId">User id.</param>
/// <param name="imageType">Image type.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param> /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param> /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
/// <param name="maxWidth">The maximum image width to return.</param> /// <param name="maxWidth">The maximum image width to return.</param>
@ -1504,25 +1460,25 @@ public class ImageController : BaseJellyfinApiController
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="blur">Optional. Blur image.</param> /// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param> /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param> /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
/// <param name="imageIndex">Image index.</param> /// <param name="imageIndex">Image index.</param>
/// <response code="200">Image stream returned.</response> /// <response code="200">Image stream returned.</response>
/// <response code="400">User id not provided.</response>
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns> /// <returns>
/// A <see cref="FileStreamResult"/> containing the file stream on success, /// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found. /// or a <see cref="NotFoundResult"/> if item not found.
/// </returns> /// </returns>
[HttpGet("Users/{userId}/Images/{imageType}")] [HttpGet("UserImage")]
[HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")] [HttpHead("UserImage", Name = "HeadUserImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile] [ProducesImageFile]
public async Task<ActionResult> GetUserImage( public async Task<ActionResult> GetUserImage(
[FromRoute, Required] Guid userId, [FromQuery] Guid? userId,
[FromRoute, Required] ImageType imageType,
[FromQuery] string? tag, [FromQuery] string? tag,
[FromQuery] ImageFormat? format, [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
@ -1534,13 +1490,18 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality, [FromQuery] int? quality,
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
[FromQuery] int? imageIndex) [FromQuery] int? imageIndex)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = userId ?? User.GetUserId();
if (requestUserId.IsEmpty())
{
return BadRequest("UserId is required if unauthenticated");
}
var user = _userManager.GetUserById(requestUserId);
if (user?.ProfileImage is null) if (user?.ProfileImage is null)
{ {
return NotFound(); return NotFound();
@ -1565,7 +1526,7 @@ public class ImageController : BaseJellyfinApiController
return await GetImageInternal( return await GetImageInternal(
user.Id, user.Id,
imageType, ImageType.Profile,
imageIndex, imageIndex,
tag, tag,
format, format,
@ -1586,6 +1547,75 @@ public class ImageController : BaseJellyfinApiController
.ConfigureAwait(false); .ConfigureAwait(false);
} }
/// <summary>
/// Get user profile image.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="imageType">Image type.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
/// <param name="maxWidth">The maximum image width to return.</param>
/// <param name="maxHeight">The maximum image height to return.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param>
/// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
/// <param name="imageIndex">Image index.</param>
/// <response code="200">Image stream returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
[HttpGet("Users/{userId}/Images/{imageType}")]
[HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImageLegacy")]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public Task<ActionResult> GetUserImageLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
[FromQuery] int? imageIndex)
=> GetUserImage(
userId,
tag,
format,
maxWidth,
maxHeight,
percentPlayed,
unplayedCount,
width,
height,
quality,
fillWidth,
fillHeight,
blur,
backgroundColor,
foregroundLayer,
imageIndex);
/// <summary> /// <summary>
/// Get user profile image. /// Get user profile image.
/// </summary> /// </summary>
@ -1603,7 +1633,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param> /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="fillWidth">Width of box to fill.</param> /// <param name="fillWidth">Width of box to fill.</param>
/// <param name="fillHeight">Height of box to fill.</param> /// <param name="fillHeight">Height of box to fill.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="blur">Optional. Blur image.</param> /// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param> /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param> /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
@ -1614,11 +1643,13 @@ public class ImageController : BaseJellyfinApiController
/// or a <see cref="NotFoundResult"/> if item not found. /// or a <see cref="NotFoundResult"/> if item not found.
/// </returns> /// </returns>
[HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")] [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")] [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndexLegacy")]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile] [ProducesImageFile]
public async Task<ActionResult> GetUserImageByIndex( public Task<ActionResult> GetUserImageByIndexLegacy(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex, [FromRoute, Required] int imageIndex,
@ -1633,56 +1664,26 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality, [FromQuery] int? quality,
[FromQuery] int? fillWidth, [FromQuery] int? fillWidth,
[FromQuery] int? fillHeight, [FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer) [FromQuery] string? foregroundLayer)
{ => GetUserImage(
var user = _userManager.GetUserById(userId); userId,
if (user?.ProfileImage is null) tag,
{ format,
return NotFound(); maxWidth,
} maxHeight,
percentPlayed,
var info = new ItemImageInfo unplayedCount,
{ width,
Path = user.ProfileImage.Path, height,
Type = ImageType.Profile, quality,
DateModified = user.ProfileImage.LastModified fillWidth,
}; fillHeight,
blur,
if (width.HasValue) backgroundColor,
{ foregroundLayer,
info.Width = width.Value; imageIndex);
}
if (height.HasValue)
{
info.Height = height.Value;
}
return await GetImageInternal(
user.Id,
imageType,
imageIndex,
tag,
format,
maxWidth,
maxHeight,
percentPlayed,
unplayedCount,
width,
height,
quality,
fillWidth,
fillHeight,
blur,
backgroundColor,
foregroundLayer,
null,
info)
.ConfigureAwait(false);
}
/// <summary> /// <summary>
/// Generates or gets the splashscreen. /// Generates or gets the splashscreen.

View File

@ -612,8 +612,10 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
[HttpGet("Users/{userId}/Items")] [HttpGet("Users/{userId}/Items")]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId( public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserIdLegacy(
[FromRoute] Guid userId, [FromRoute] Guid userId,
[FromQuery] string? maxOfficialRating, [FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong, [FromQuery] bool? hasThemeSong,
@ -699,8 +701,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
{ => GetItems(
return GetItems(
userId, userId,
maxOfficialRating, maxOfficialRating,
hasThemeSong, hasThemeSong,
@ -786,7 +787,6 @@ public class ItemsController : BaseJellyfinApiController
genreIds, genreIds,
enableTotalRecordCount, enableTotalRecordCount,
enableImages); enableImages);
}
/// <summary> /// <summary>
/// Gets items based on a query. /// Gets items based on a query.
@ -808,10 +808,10 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param> /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
/// <response code="200">Items returned.</response> /// <response code="200">Items returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
[HttpGet("Users/{userId}/Items/Resume")] [HttpGet("UserItems/Resume")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetResumeItems( public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
[FromRoute, Required] Guid userId, [FromQuery] Guid? userId,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
@ -827,7 +827,8 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? enableImages = true, [FromQuery] bool? enableImages = true,
[FromQuery] bool excludeActiveSessions = false) [FromQuery] bool excludeActiveSessions = false)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -854,7 +855,7 @@ public class ItemsController : BaseJellyfinApiController
if (excludeActiveSessions) if (excludeActiveSessions)
{ {
excludeItemIds = _sessionManager.Sessions excludeItemIds = _sessionManager.Sessions
.Where(s => s.UserId.Equals(userId) && s.NowPlayingItem is not null) .Where(s => s.UserId.Equals(requestUserId) && s.NowPlayingItem is not null)
.Select(s => s.NowPlayingItem.Id) .Select(s => s.NowPlayingItem.Id)
.ToArray(); .ToArray();
} }
@ -887,6 +888,90 @@ public class ItemsController : BaseJellyfinApiController
returnItems); returnItems);
} }
/// <summary>
/// Gets items based on a query.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="startIndex">The start index.</param>
/// <param name="limit">The item limit.</param>
/// <param name="searchTerm">The search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
/// <response code="200">Items returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
[HttpGet("Users/{userId}/Items/Resume")]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetResumeItemsLegacy(
[FromRoute, Required] Guid userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true,
[FromQuery] bool excludeActiveSessions = false)
=> GetResumeItems(
userId,
startIndex,
limit,
searchTerm,
parentId,
fields,
mediaTypes,
enableUserData,
imageTypeLimit,
enableImageTypes,
excludeItemTypes,
includeItemTypes,
enableTotalRecordCount,
enableImages,
excludeActiveSessions);
/// <summary>
/// Get Item User Data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <response code="200">return item user data.</response>
/// <response code="404">Item is not found.</response>
/// <returns>Return <see cref="UserItemDataDto"/>.</returns>
[HttpGet("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<UserItemDataDto> GetItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
var requestUserId = RequestHelpers.GetUserId(User, userId);
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, requestUserId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to view this item user data.");
}
var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
var item = _libraryManager.GetItemById(itemId);
return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user);
}
/// <summary> /// <summary>
/// Get Item User Data. /// Get Item User Data.
/// </summary> /// </summary>
@ -898,19 +983,46 @@ public class ItemsController : BaseJellyfinApiController
[HttpGet("Users/{userId}/Items/{itemId}/UserData")] [HttpGet("Users/{userId}/Items/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<UserItemDataDto> GetItemUserData( [Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> GetItemUserDataLegacy(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId) [FromRoute, Required] Guid itemId)
=> GetItemUserData(userId, itemId);
/// <summary>
/// Update Item User Data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="userDataDto">New user data object.</param>
/// <response code="200">return updated user item data.</response>
/// <response code="404">Item is not found.</response>
/// <returns>Return <see cref="UserItemDataDto"/>.</returns>
[HttpPost("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<UserItemDataDto> UpdateItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[FromBody, Required] UpdateUserItemDataDto userDataDto)
{ {
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) var requestUserId = RequestHelpers.GetUserId(User, userId);
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, requestUserId, true))
{ {
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to view this item user data."); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update this item user data.");
} }
var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException(); var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user); _userDataRepository.SaveUserData(user, item, userDataDto, UserDataSaveReason.UpdateUserData);
return _userDataRepository.GetUserDataDto(item, user);
} }
/// <summary> /// <summary>
@ -925,25 +1037,11 @@ public class ItemsController : BaseJellyfinApiController
[HttpPost("Users/{userId}/Items/{itemId}/UserData")] [HttpPost("Users/{userId}/Items/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<UserItemDataDto> UpdateItemUserData( [Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> UpdateItemUserDataLegacy(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromBody, Required] UpdateUserItemDataDto userDataDto) [FromBody, Required] UpdateUserItemDataDto userDataDto)
{ => UpdateItemUserData(userId, itemId, userDataDto);
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update this item user data.");
}
var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
_userDataRepository.SaveUserData(user, item, userDataDto, UserDataSaveReason.UpdateUserData);
return _userDataRepository.GetUserDataDto(item, user);
}
} }

View File

@ -68,15 +68,16 @@ public class PlaystateController : BaseJellyfinApiController
/// <response code="200">Item marked as played.</response> /// <response code="200">Item marked as played.</response>
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns> /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns>
[HttpPost("Users/{userId}/PlayedItems/{itemId}")] [HttpPost("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem( public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
[FromRoute, Required] Guid userId, [FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed) [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -105,6 +106,26 @@ public class PlaystateController : BaseJellyfinApiController
return dto; return dto;
} }
/// <summary>
/// Marks an item as played for user.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <param name="datePlayed">Optional. The date the item was played.</param>
/// <response code="200">Item marked as played.</response>
/// <response code="404">Item not found.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns>
[HttpPost("Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public Task<ActionResult<UserItemDataDto>> MarkPlayedItemLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
=> MarkPlayedItem(userId, itemId, datePlayed);
/// <summary> /// <summary>
/// Marks an item as unplayed for user. /// Marks an item as unplayed for user.
/// </summary> /// </summary>
@ -113,12 +134,15 @@ public class PlaystateController : BaseJellyfinApiController
/// <response code="200">Item marked as unplayed.</response> /// <response code="200">Item marked as unplayed.</response>
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns> /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns>
[HttpDelete("Users/{userId}/PlayedItems/{itemId}")] [HttpDelete("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -147,6 +171,24 @@ public class PlaystateController : BaseJellyfinApiController
return dto; return dto;
} }
/// <summary>
/// Marks an item as unplayed for user.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Item marked as unplayed.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns>
[HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public Task<ActionResult<UserItemDataDto>> MarkUnplayedItemLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> MarkUnplayedItem(userId, itemId);
/// <summary> /// <summary>
/// Reports playback has started within a session. /// Reports playback has started within a session.
/// </summary> /// </summary>
@ -215,9 +257,8 @@ public class PlaystateController : BaseJellyfinApiController
} }
/// <summary> /// <summary>
/// Reports that a user has begun playing an item. /// Reports that a session has begun playing an item.
/// </summary> /// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param> /// <param name="itemId">Item id.</param>
/// <param name="mediaSourceId">The id of the MediaSource.</param> /// <param name="mediaSourceId">The id of the MediaSource.</param>
/// <param name="audioStreamIndex">The audio stream index.</param> /// <param name="audioStreamIndex">The audio stream index.</param>
@ -228,11 +269,9 @@ public class PlaystateController : BaseJellyfinApiController
/// <param name="canSeek">Indicates if the client can seek.</param> /// <param name="canSeek">Indicates if the client can seek.</param>
/// <response code="204">Play start recorded.</response> /// <response code="204">Play start recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/PlayingItems/{itemId}")] [HttpPost("PlayingItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackStart( public async Task<ActionResult> OnPlaybackStart(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId, [FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
@ -261,11 +300,41 @@ public class PlaystateController : BaseJellyfinApiController
} }
/// <summary> /// <summary>
/// Reports a user's playback progress. /// Reports that a user has begun playing an item.
/// </summary> /// </summary>
/// <param name="userId">User id.</param> /// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param> /// <param name="itemId">Item id.</param>
/// <param name="mediaSourceId">The id of the MediaSource.</param> /// <param name="mediaSourceId">The id of the MediaSource.</param>
/// <param name="audioStreamIndex">The audio stream index.</param>
/// <param name="subtitleStreamIndex">The subtitle stream index.</param>
/// <param name="playMethod">The play method.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="canSeek">Indicates if the client can seek.</param>
/// <response code="204">Play start recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/PlayingItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public Task<ActionResult> OnPlaybackStartLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] PlayMethod? playMethod,
[FromQuery] string? liveStreamId,
[FromQuery] string? playSessionId,
[FromQuery] bool canSeek = false)
=> OnPlaybackStart(itemId, mediaSourceId, audioStreamIndex, subtitleStreamIndex, playMethod, liveStreamId, playSessionId, canSeek);
/// <summary>
/// Reports a session's playback progress.
/// </summary>
/// <param name="itemId">Item id.</param>
/// <param name="mediaSourceId">The id of the MediaSource.</param>
/// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param> /// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param>
/// <param name="audioStreamIndex">The audio stream index.</param> /// <param name="audioStreamIndex">The audio stream index.</param>
/// <param name="subtitleStreamIndex">The subtitle stream index.</param> /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
@ -278,11 +347,9 @@ public class PlaystateController : BaseJellyfinApiController
/// <param name="isMuted">Indicates if the player is muted.</param> /// <param name="isMuted">Indicates if the player is muted.</param>
/// <response code="204">Play progress recorded.</response> /// <response code="204">Play progress recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")] [HttpPost("PlayingItems/{itemId}/Progress")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackProgress( public async Task<ActionResult> OnPlaybackProgress(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId, [FromQuery] string? mediaSourceId,
[FromQuery] long? positionTicks, [FromQuery] long? positionTicks,
@ -319,22 +386,58 @@ public class PlaystateController : BaseJellyfinApiController
} }
/// <summary> /// <summary>
/// Reports that a user has stopped playing an item. /// Reports a user's playback progress.
/// </summary> /// </summary>
/// <param name="userId">User id.</param> /// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param> /// <param name="itemId">Item id.</param>
/// <param name="mediaSourceId">The id of the MediaSource.</param> /// <param name="mediaSourceId">The id of the MediaSource.</param>
/// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param>
/// <param name="audioStreamIndex">The audio stream index.</param>
/// <param name="subtitleStreamIndex">The subtitle stream index.</param>
/// <param name="volumeLevel">Scale of 0-100.</param>
/// <param name="playMethod">The play method.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="repeatMode">The repeat mode.</param>
/// <param name="isPaused">Indicates if the player is paused.</param>
/// <param name="isMuted">Indicates if the player is muted.</param>
/// <response code="204">Play progress recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public Task<ActionResult> OnPlaybackProgressLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] long? positionTicks,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? volumeLevel,
[FromQuery] PlayMethod? playMethod,
[FromQuery] string? liveStreamId,
[FromQuery] string? playSessionId,
[FromQuery] RepeatMode? repeatMode,
[FromQuery] bool isPaused = false,
[FromQuery] bool isMuted = false)
=> OnPlaybackProgress(itemId, mediaSourceId, positionTicks, audioStreamIndex, subtitleStreamIndex, volumeLevel, playMethod, liveStreamId, playSessionId, repeatMode, isPaused, isMuted);
/// <summary>
/// Reports that a session has stopped playing an item.
/// </summary>
/// <param name="itemId">Item id.</param>
/// <param name="mediaSourceId">The id of the MediaSource.</param>
/// <param name="nextMediaType">The next media type that will play.</param> /// <param name="nextMediaType">The next media type that will play.</param>
/// <param name="positionTicks">Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.</param> /// <param name="positionTicks">Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.</param>
/// <param name="liveStreamId">The live stream id.</param> /// <param name="liveStreamId">The live stream id.</param>
/// <param name="playSessionId">The play session id.</param> /// <param name="playSessionId">The play session id.</param>
/// <response code="204">Playback stop recorded.</response> /// <response code="204">Playback stop recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Users/{userId}/PlayingItems/{itemId}")] [HttpDelete("PlayingItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackStopped( public async Task<ActionResult> OnPlaybackStopped(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId, [FromQuery] string? mediaSourceId,
[FromQuery] string? nextMediaType, [FromQuery] string? nextMediaType,
@ -363,6 +466,33 @@ public class PlaystateController : BaseJellyfinApiController
return NoContent(); return NoContent();
} }
/// <summary>
/// Reports that a user has stopped playing an item.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <param name="mediaSourceId">The id of the MediaSource.</param>
/// <param name="nextMediaType">The next media type that will play.</param>
/// <param name="positionTicks">Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="playSessionId">The play session id.</param>
/// <response code="204">Playback stop recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Users/{userId}/PlayingItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public Task<ActionResult> OnPlaybackStoppedLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] string? nextMediaType,
[FromQuery] long? positionTicks,
[FromQuery] string? liveStreamId,
[FromQuery] string? playSessionId)
=> OnPlaybackStopped(itemId, mediaSourceId, nextMediaType, positionTicks, liveStreamId, playSessionId);
/// <summary> /// <summary>
/// Updates the played status. /// Updates the played status.
/// </summary> /// </summary>

View File

@ -1,7 +1,9 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
@ -53,19 +55,26 @@ public class SuggestionsController : BaseJellyfinApiController
/// <param name="enableTotalRecordCount">Whether to enable the total record count.</param> /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param>
/// <response code="200">Suggestions returned.</response> /// <response code="200">Suggestions returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns>
[HttpGet("Users/{userId}/Suggestions")] [HttpGet("Items/Suggestions")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions( public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
[FromRoute, Required] Guid userId, [FromQuery] Guid? userId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false) [FromQuery] bool enableTotalRecordCount = false)
{ {
var user = userId.IsEmpty() User? user;
? null if (userId.IsNullOrEmpty())
: _userManager.GetUserById(userId); {
user = null;
}
else
{
var requestUserId = RequestHelpers.GetUserId(User, userId);
user = _userManager.GetUserById(requestUserId);
}
var dtoOptions = new DtoOptions().AddClientFields(User); var dtoOptions = new DtoOptions().AddClientFields(User);
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
@ -88,4 +97,28 @@ public class SuggestionsController : BaseJellyfinApiController
result.TotalRecordCount, result.TotalRecordCount,
dtoList); dtoList);
} }
/// <summary>
/// Gets suggestions.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="mediaType">The media types.</param>
/// <param name="type">The type.</param>
/// <param name="startIndex">Optional. The start index.</param>
/// <param name="limit">Optional. The limit.</param>
/// <param name="enableTotalRecordCount">Whether to enable the total record count.</param>
/// <response code="200">Suggestions returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns>
[HttpGet("Users/{userId}/Suggestions")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestionsLegacy(
[FromRoute, Required] Guid userId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false)
=> GetSuggestions(userId, mediaType, type, startIndex, limit, enableTotalRecordCount);
} }

View File

@ -178,6 +178,7 @@ public class UserController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ApiExplorerSettings(IgnoreApi = true)]
[Obsolete("Authenticate with username instead")] [Obsolete("Authenticate with username instead")]
public async Task<ActionResult<AuthenticationResult>> AuthenticateUser( public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
@ -263,21 +264,22 @@ public class UserController : BaseJellyfinApiController
/// <response code="403">User is not allowed to update the password.</response> /// <response code="403">User is not allowed to update the password.</response>
/// <response code="404">User not found.</response> /// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
[HttpPost("{userId}/Password")] [HttpPost("Password")]
[Authorize] [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateUserPassword( public async Task<ActionResult> UpdateUserPassword(
[FromRoute, Required] Guid userId, [FromQuery] Guid? userId,
[FromBody, Required] UpdateUserPassword request) [FromBody, Required] UpdateUserPassword request)
{ {
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) var requestUserId = userId ?? User.GetUserId();
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, requestUserId, true))
{ {
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password."); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
} }
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
@ -290,7 +292,7 @@ public class UserController : BaseJellyfinApiController
} }
else else
{ {
if (!User.IsInRole(UserRoles.Administrator) || User.GetUserId().Equals(userId)) if (!User.IsInRole(UserRoles.Administrator) || (userId.HasValue && User.GetUserId().Equals(userId.Value)))
{ {
var success = await _userManager.AuthenticateUser( var success = await _userManager.AuthenticateUser(
user.Username, user.Username,
@ -315,6 +317,27 @@ public class UserController : BaseJellyfinApiController
return NoContent(); return NoContent();
} }
/// <summary>
/// Updates a user's password.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="request">The <see cref="UpdateUserPassword"/> request.</param>
/// <response code="204">Password successfully reset.</response>
/// <response code="403">User is not allowed to update the password.</response>
/// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
[HttpPost("{userId}/Password")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public Task<ActionResult> UpdateUserPasswordLegacy(
[FromRoute, Required] Guid userId,
[FromBody, Required] UpdateUserPassword request)
=> UpdateUserPassword(userId, request);
/// <summary> /// <summary>
/// Updates a user's easy password. /// Updates a user's easy password.
/// </summary> /// </summary>
@ -326,6 +349,7 @@ public class UserController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
[HttpPost("{userId}/EasyPassword")] [HttpPost("{userId}/EasyPassword")]
[Obsolete("Use Quick Connect instead")] [Obsolete("Use Quick Connect instead")]
[ApiExplorerSettings(IgnoreApi = true)]
[Authorize] [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
@ -346,22 +370,23 @@ public class UserController : BaseJellyfinApiController
/// <response code="400">User information was not supplied.</response> /// <response code="400">User information was not supplied.</response>
/// <response code="403">User update forbidden.</response> /// <response code="403">User update forbidden.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns>
[HttpPost("{userId}")] [HttpPost]
[Authorize] [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUser( public async Task<ActionResult> UpdateUser(
[FromRoute, Required] Guid userId, [FromQuery] Guid? userId,
[FromBody, Required] UserDto updateUser) [FromBody, Required] UserDto updateUser)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = userId ?? User.GetUserId();
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
} }
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, requestUserId, true))
{ {
return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed."); return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
} }
@ -376,6 +401,27 @@ public class UserController : BaseJellyfinApiController
return NoContent(); return NoContent();
} }
/// <summary>
/// Updates a user.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="updateUser">The updated user model.</param>
/// <response code="204">User updated.</response>
/// <response code="400">User information was not supplied.</response>
/// <response code="403">User update forbidden.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns>
[HttpPost("{userId}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public Task<ActionResult> UpdateUserLegacy(
[FromRoute, Required] Guid userId,
[FromBody, Required] UserDto updateUser)
=> UpdateUser(userId, updateUser);
/// <summary> /// <summary>
/// Updates a user policy. /// Updates a user policy.
/// </summary> /// </summary>
@ -440,24 +486,44 @@ public class UserController : BaseJellyfinApiController
/// <response code="204">User configuration updated.</response> /// <response code="204">User configuration updated.</response>
/// <response code="403">User configuration update forbidden.</response> /// <response code="403">User configuration update forbidden.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{userId}/Configuration")] [HttpPost("Configuration")]
[Authorize] [Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUserConfiguration( public async Task<ActionResult> UpdateUserConfiguration(
[FromRoute, Required] Guid userId, [FromQuery] Guid? userId,
[FromBody, Required] UserConfiguration userConfig) [FromBody, Required] UserConfiguration userConfig)
{ {
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) var requestUserId = userId ?? User.GetUserId();
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, requestUserId, true))
{ {
return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed"); return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
} }
await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false); await _userManager.UpdateConfigurationAsync(requestUserId, userConfig).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
/// <summary>
/// Updates a user configuration.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="userConfig">The new user configuration.</param>
/// <response code="204">User configuration updated.</response>
/// <response code="403">User configuration update forbidden.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{userId}/Configuration")]
[Authorize]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public Task<ActionResult> UpdateUserConfigurationLegacy(
[FromRoute, Required] Guid userId,
[FromBody, Required] UserConfiguration userConfig)
=> UpdateUserConfiguration(userId, userConfig);
/// <summary> /// <summary>
/// Creates a user. /// Creates a user.
/// </summary> /// </summary>

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@ -13,12 +14,10 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Lyrics;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -39,7 +38,6 @@ public class UserLibraryController : BaseJellyfinApiController
private readonly IDtoService _dtoService; private readonly IDtoService _dtoService;
private readonly IUserViewManager _userViewManager; private readonly IUserViewManager _userViewManager;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly ILyricManager _lyricManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserLibraryController"/> class. /// Initializes a new instance of the <see cref="UserLibraryController"/> class.
@ -50,15 +48,13 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param> /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
public UserLibraryController( public UserLibraryController(
IUserManager userManager, IUserManager userManager,
IUserDataManager userDataRepository, IUserDataManager userDataRepository,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IDtoService dtoService, IDtoService dtoService,
IUserViewManager userViewManager, IUserViewManager userViewManager,
IFileSystem fileSystem, IFileSystem fileSystem)
ILyricManager lyricManager)
{ {
_userManager = userManager; _userManager = userManager;
_userDataRepository = userDataRepository; _userDataRepository = userDataRepository;
@ -66,7 +62,6 @@ public class UserLibraryController : BaseJellyfinApiController
_dtoService = dtoService; _dtoService = dtoService;
_userViewManager = userViewManager; _userViewManager = userViewManager;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_lyricManager = lyricManager;
} }
/// <summary> /// <summary>
@ -76,11 +71,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="itemId">Item id.</param> /// <param name="itemId">Item id.</param>
/// <response code="200">Item returned.</response> /// <response code="200">Item returned.</response>
/// <returns>An <see cref="OkResult"/> containing the item.</returns> /// <returns>An <see cref="OkResult"/> containing the item.</returns>
[HttpGet("Users/{userId}/Items/{itemId}")] [HttpGet("Items/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) public async Task<ActionResult<BaseItemDto>> GetItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -109,17 +107,34 @@ public class UserLibraryController : BaseJellyfinApiController
return _dtoService.GetBaseItemDto(item, dtoOptions, user); return _dtoService.GetBaseItemDto(item, dtoOptions, user);
} }
/// <summary>
/// Gets an item from a user's library.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Item returned.</response>
/// <returns>An <see cref="OkResult"/> containing the item.</returns>
[HttpGet("Users/{userId}/Items/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public Task<ActionResult<BaseItemDto>> GetItemLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> GetItem(userId, itemId);
/// <summary> /// <summary>
/// Gets the root folder from a user's library. /// Gets the root folder from a user's library.
/// </summary> /// </summary>
/// <param name="userId">User id.</param> /// <param name="userId">User id.</param>
/// <response code="200">Root folder returned.</response> /// <response code="200">Root folder returned.</response>
/// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns> /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
[HttpGet("Users/{userId}/Items/Root")] [HttpGet("Items/Root")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetRootFolder([FromRoute, Required] Guid userId) public ActionResult<BaseItemDto> GetRootFolder([FromQuery] Guid? userId)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -130,6 +145,20 @@ public class UserLibraryController : BaseJellyfinApiController
return _dtoService.GetBaseItemDto(item, dtoOptions, user); return _dtoService.GetBaseItemDto(item, dtoOptions, user);
} }
/// <summary>
/// Gets the root folder from a user's library.
/// </summary>
/// <param name="userId">User id.</param>
/// <response code="200">Root folder returned.</response>
/// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
[HttpGet("Users/{userId}/Items/Root")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<BaseItemDto> GetRootFolderLegacy(
[FromRoute, Required] Guid userId)
=> GetRootFolder(userId);
/// <summary> /// <summary>
/// Gets intros to play before the main media item plays. /// Gets intros to play before the main media item plays.
/// </summary> /// </summary>
@ -137,11 +166,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="itemId">Item id.</param> /// <param name="itemId">Item id.</param>
/// <response code="200">Intros returned.</response> /// <response code="200">Intros returned.</response>
/// <returns>An <see cref="OkResult"/> containing the intros to play.</returns> /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/Intros")] [HttpGet("Items/{itemId}/Intros")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -170,6 +202,22 @@ public class UserLibraryController : BaseJellyfinApiController
return new QueryResult<BaseItemDto>(dtos); return new QueryResult<BaseItemDto>(dtos);
} }
/// <summary>
/// Gets intros to play before the main media item plays.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Intros returned.</response>
/// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/Intros")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public Task<ActionResult<QueryResult<BaseItemDto>>> GetIntrosLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> GetIntros(userId, itemId);
/// <summary> /// <summary>
/// Marks an item as a favorite. /// Marks an item as a favorite.
/// </summary> /// </summary>
@ -177,11 +225,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="itemId">Item id.</param> /// <param name="itemId">Item id.</param>
/// <response code="200">Item marked as favorite.</response> /// <response code="200">Item marked as favorite.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("Users/{userId}/FavoriteItems/{itemId}")] [HttpPost("UserFavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) public ActionResult<UserItemDataDto> MarkFavoriteItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -206,6 +257,22 @@ public class UserLibraryController : BaseJellyfinApiController
return MarkFavorite(user, item, true); return MarkFavorite(user, item, true);
} }
/// <summary>
/// Marks an item as a favorite.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Item marked as favorite.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("Users/{userId}/FavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> MarkFavoriteItemLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> MarkFavoriteItem(userId, itemId);
/// <summary> /// <summary>
/// Unmarks item as a favorite. /// Unmarks item as a favorite.
/// </summary> /// </summary>
@ -213,11 +280,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="itemId">Item id.</param> /// <param name="itemId">Item id.</param>
/// <response code="200">Item unmarked as favorite.</response> /// <response code="200">Item unmarked as favorite.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("Users/{userId}/FavoriteItems/{itemId}")] [HttpDelete("UserFavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) public ActionResult<UserItemDataDto> UnmarkFavoriteItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -242,6 +312,22 @@ public class UserLibraryController : BaseJellyfinApiController
return MarkFavorite(user, item, false); return MarkFavorite(user, item, false);
} }
/// <summary>
/// Unmarks item as a favorite.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Item unmarked as favorite.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("Users/{userId}/FavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> UnmarkFavoriteItemLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> UnmarkFavoriteItem(userId, itemId);
/// <summary> /// <summary>
/// Deletes a user's saved personal rating for an item. /// Deletes a user's saved personal rating for an item.
/// </summary> /// </summary>
@ -249,11 +335,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="itemId">Item id.</param> /// <param name="itemId">Item id.</param>
/// <response code="200">Personal rating removed.</response> /// <response code="200">Personal rating removed.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("Users/{userId}/Items/{itemId}/Rating")] [HttpDelete("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) public ActionResult<UserItemDataDto> DeleteUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -278,6 +367,22 @@ public class UserLibraryController : BaseJellyfinApiController
return UpdateUserItemRatingInternal(user, item, null); return UpdateUserItemRatingInternal(user, item, null);
} }
/// <summary>
/// Deletes a user's saved personal rating for an item.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Personal rating removed.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("Users/{userId}/Items/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> DeleteUserItemRatingLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> DeleteUserItemRating(userId, itemId);
/// <summary> /// <summary>
/// Updates a user's rating for an item. /// Updates a user's rating for an item.
/// </summary> /// </summary>
@ -286,11 +391,15 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param> /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
/// <response code="200">Item rating updated.</response> /// <response code="200">Item rating updated.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("Users/{userId}/Items/{itemId}/Rating")] [HttpPost("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes) public ActionResult<UserItemDataDto> UpdateUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[FromQuery] bool? likes)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -315,6 +424,24 @@ public class UserLibraryController : BaseJellyfinApiController
return UpdateUserItemRatingInternal(user, item, likes); return UpdateUserItemRatingInternal(user, item, likes);
} }
/// <summary>
/// Updates a user's rating for an item.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
/// <response code="200">Item rating updated.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("Users/{userId}/Items/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> UpdateUserItemRatingLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery] bool? likes)
=> UpdateUserItemRating(userId, itemId, likes);
/// <summary> /// <summary>
/// Gets local trailers for an item. /// Gets local trailers for an item.
/// </summary> /// </summary>
@ -322,11 +449,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="itemId">Item id.</param> /// <param name="itemId">Item id.</param>
/// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response> /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
/// <returns>The items local trailers.</returns> /// <returns>The items local trailers.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")] [HttpGet("Items/{itemId}/LocalTrailers")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -360,6 +490,22 @@ public class UserLibraryController : BaseJellyfinApiController
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))); .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
} }
/// <summary>
/// Gets local trailers for an item.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
/// <returns>The items local trailers.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailersLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> GetLocalTrailers(userId, itemId);
/// <summary> /// <summary>
/// Gets special features for an item. /// Gets special features for an item.
/// </summary> /// </summary>
@ -367,11 +513,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="itemId">Item id.</param> /// <param name="itemId">Item id.</param>
/// <response code="200">Special features returned.</response> /// <response code="200">Special features returned.</response>
/// <returns>An <see cref="OkResult"/> containing the special features.</returns> /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")] [HttpGet("Items/{itemId}/SpecialFeatures")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -401,6 +550,22 @@ public class UserLibraryController : BaseJellyfinApiController
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))); .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
} }
/// <summary>
/// Gets special features for an item.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Special features returned.</response>
/// <returns>An <see cref="OkResult"/> containing the special features.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeaturesLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> GetSpecialFeatures(userId, itemId);
/// <summary> /// <summary>
/// Gets latest media. /// Gets latest media.
/// </summary> /// </summary>
@ -417,10 +582,10 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="groupItems">Whether or not to group items into a parent container.</param> /// <param name="groupItems">Whether or not to group items into a parent container.</param>
/// <response code="200">Latest media returned.</response> /// <response code="200">Latest media returned.</response>
/// <returns>An <see cref="OkResult"/> containing the latest media.</returns> /// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
[HttpGet("Users/{userId}/Items/Latest")] [HttpGet("Items/Latest")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia( public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
[FromRoute, Required] Guid userId, [FromQuery] Guid? userId,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
@ -432,7 +597,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] int limit = 20, [FromQuery] int limit = 20,
[FromQuery] bool groupItems = true) [FromQuery] bool groupItems = true)
{ {
var user = _userManager.GetUserById(userId); var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -458,7 +624,7 @@ public class UserLibraryController : BaseJellyfinApiController
IsPlayed = isPlayed, IsPlayed = isPlayed,
Limit = limit, Limit = limit,
ParentId = parentId ?? Guid.Empty, ParentId = parentId ?? Guid.Empty,
UserId = userId, UserId = requestUserId,
}, },
dtoOptions); dtoOptions);
@ -483,6 +649,51 @@ public class UserLibraryController : BaseJellyfinApiController
return Ok(dtos); return Ok(dtos);
} }
/// <summary>
/// Gets latest media.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="isPlayed">Filter by items that are played, or not.</param>
/// <param name="enableImages">Optional. include image information in output.</param>
/// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. include user data.</param>
/// <param name="limit">Return item limit.</param>
/// <param name="groupItems">Whether or not to group items into a parent container.</param>
/// <response code="200">Latest media returned.</response>
/// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
[HttpGet("Users/{userId}/Items/Latest")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMediaLegacy(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int limit = 20,
[FromQuery] bool groupItems = true)
=> GetLatestMedia(
userId,
parentId,
fields,
includeItemTypes,
isPlayed,
enableImages,
imageTypeLimit,
enableImageTypes,
enableUserData,
limit,
groupItems);
private async Task RefreshItemOnDemandIfNeeded(BaseItem item) private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
{ {
if (item is Person) if (item is Person)

View File

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos; using Jellyfin.Api.Models.UserViewDtos;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@ -59,19 +60,17 @@ public class UserViewsController : BaseJellyfinApiController
/// <param name="includeHidden">Whether or not to include hidden content.</param> /// <param name="includeHidden">Whether or not to include hidden content.</param>
/// <response code="200">User views returned.</response> /// <response code="200">User views returned.</response>
/// <returns>An <see cref="OkResult"/> containing the user views.</returns> /// <returns>An <see cref="OkResult"/> containing the user views.</returns>
[HttpGet("Users/{userId}/Views")] [HttpGet("UserViews")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public QueryResult<BaseItemDto> GetUserViews( public QueryResult<BaseItemDto> GetUserViews(
[FromRoute, Required] Guid userId, [FromQuery] Guid? userId,
[FromQuery] bool? includeExternalContent, [FromQuery] bool? includeExternalContent,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews,
[FromQuery] bool includeHidden = false) [FromQuery] bool includeHidden = false)
{ {
var query = new UserViewQuery userId = RequestHelpers.GetUserId(User, userId);
{
UserId = userId, var query = new UserViewQuery { UserId = userId.Value, IncludeHidden = includeHidden };
IncludeHidden = includeHidden
};
if (includeExternalContent.HasValue) if (includeExternalContent.HasValue)
{ {
@ -92,7 +91,7 @@ public class UserViewsController : BaseJellyfinApiController
fields.Add(ItemFields.DisplayPreferencesId); fields.Add(ItemFields.DisplayPreferencesId);
dtoOptions.Fields = fields.ToArray(); dtoOptions.Fields = fields.ToArray();
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId.Value);
var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)) var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
.ToArray(); .ToArray();
@ -100,6 +99,26 @@ public class UserViewsController : BaseJellyfinApiController
return new QueryResult<BaseItemDto>(dtos); return new QueryResult<BaseItemDto>(dtos);
} }
/// <summary>
/// Get user views.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="includeExternalContent">Whether or not to include external views such as channels or live tv.</param>
/// <param name="presetViews">Preset views.</param>
/// <param name="includeHidden">Whether or not to include hidden content.</param>
/// <response code="200">User views returned.</response>
/// <returns>An <see cref="OkResult"/> containing the user views.</returns>
[HttpGet("Users/{userId}/Views")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public QueryResult<BaseItemDto> GetUserViewsLegacy(
[FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews,
[FromQuery] bool includeHidden = false)
=> GetUserViews(userId, includeExternalContent, presetViews, includeHidden);
/// <summary> /// <summary>
/// Get user view grouping options. /// Get user view grouping options.
/// </summary> /// </summary>
@ -110,12 +129,13 @@ public class UserViewsController : BaseJellyfinApiController
/// An <see cref="OkResult"/> containing the user view grouping options /// An <see cref="OkResult"/> containing the user view grouping options
/// or a <see cref="NotFoundResult"/> if user not found. /// or a <see cref="NotFoundResult"/> if user not found.
/// </returns> /// </returns>
[HttpGet("Users/{userId}/GroupingOptions")] [HttpGet("UserViews/GroupingOptions")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromRoute, Required] Guid userId) public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromQuery] Guid? userId)
{ {
var user = _userManager.GetUserById(userId); userId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(userId.Value);
if (user is null) if (user is null)
{ {
return NotFound(); return NotFound();
@ -133,4 +153,23 @@ public class UserViewsController : BaseJellyfinApiController
.OrderBy(i => i.Name) .OrderBy(i => i.Name)
.AsEnumerable()); .AsEnumerable());
} }
/// <summary>
/// Get user view grouping options.
/// </summary>
/// <param name="userId">User id.</param>
/// <response code="200">User view grouping options returned.</response>
/// <response code="404">User not found.</response>
/// <returns>
/// An <see cref="OkResult"/> containing the user view grouping options
/// or a <see cref="NotFoundResult"/> if user not found.
/// </returns>
[HttpGet("Users/{userId}/GroupingOptions")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptionsLegacy(
[FromRoute, Required] Guid userId)
=> GetGroupingOptions(userId);
} }