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>
pull/9374/merge
Cody Robibero 2 months ago committed by GitHub
parent 8d40d431e8
commit 6e5ec99ea1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -11,7 +11,9 @@ using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
@ -86,31 +88,26 @@ public class ImageController : BaseJellyfinApiController
/// Sets the user image.
/// </summary>
/// <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="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}")]
[HttpPost("UserImage")]
[Authorize]
[AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[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(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null)
[FromQuery] Guid? userId)
{
var user = _userManager.GetUserById(userId);
var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null)
{
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.");
}
@ -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>
/// Sets the user image.
/// </summary>
@ -153,81 +172,41 @@ public class ImageController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}/{index}")]
[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")]
[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] ImageType imageType,
[FromRoute] int index)
{
var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound();
}
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
{
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();
}
}
=> PostUserImage(userId);
/// <summary>
/// Delete the user's image.
/// </summary>
/// <param name="userId">User Id.</param>
/// <param name="imageType">(Unused) Image type.</param>
/// <param name="index">(Unused) Image index.</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("Users/{userId}/Images/{imageType}")]
[HttpDelete("UserImage")]
[Authorize]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> DeleteUserImage(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null)
[FromQuery] Guid? userId)
{
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
var requestUserId = RequestHelpers.GetUserId(User, userId);
if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, requestUserId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
var user = _userManager.GetUserById(userId);
var user = _userManager.GetUserById(requestUserId);
if (user?.ProfileImage is null)
{
return NoContent();
@ -246,6 +225,29 @@ public class ImageController : BaseJellyfinApiController
return NoContent();
}
/// <summary>
/// Delete the user's image.
/// </summary>
/// <param name="userId">User Id.</param>
/// <param name="imageType">(Unused) Image type.</param>
/// <param name="index">(Unused) Image index.</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("Users/{userId}/Images/{imageType}")]
[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 = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public Task<ActionResult> DeleteUserImageLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null)
=> DeleteUserImage(userId);
/// <summary>
/// Delete the user's image.
/// </summary>
@ -257,38 +259,17 @@ public class ImageController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
[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 = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> DeleteUserImageByIndex(
public Task<ActionResult> DeleteUserImageByIndexLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromRoute] int index)
{
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();
}
=> DeleteUserImage(userId);
/// <summary>
/// Delete an item's image.
@ -541,7 +522,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="fillWidth">Width 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="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="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
@ -571,7 +551,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] ImageFormat? format,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
@ -622,7 +601,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="fillWidth">Width 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="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="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
@ -652,7 +630,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] ImageFormat? format,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
@ -701,7 +678,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="fillWidth">Width 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="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="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
@ -731,7 +707,6 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromRoute, Required] string tag,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromRoute, Required] ImageFormat format,
[FromRoute, Required] double percentPlayed,
[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="fillWidth">Width 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="backgroundColor">Optional. Apply a background color for transparent images.</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? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[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="fillWidth">Width 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="backgroundColor">Optional. Apply a background color for transparent images.</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? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[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="fillWidth">Width 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="backgroundColor">Optional. Apply a background color for transparent images.</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? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[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="fillWidth">Width 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="backgroundColor">Optional. Apply a background color for transparent images.</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? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[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="fillWidth">Width 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="backgroundColor">Optional. Apply a background color for transparent images.</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? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[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="fillWidth">Width 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="backgroundColor">Optional. Apply a background color for transparent images.</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? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[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="fillWidth">Width 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="backgroundColor">Optional. Apply a background color for transparent images.</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? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[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="fillWidth">Width 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="backgroundColor">Optional. Apply a background color for transparent images.</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? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[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="fillWidth">Width 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="backgroundColor">Optional. Apply a background color for transparent images.</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? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
@ -1492,7 +1449,6 @@ public class ImageController : BaseJellyfinApiController
/// 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>
@ -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="fillWidth">Width 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="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="400">User id not provided.</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 = "HeadUserImage")]
[HttpGet("UserImage")]
[HttpHead("UserImage", Name = "HeadUserImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetUserImage(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromQuery] Guid? userId,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
@ -1534,13 +1490,18 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
[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)
{
return NotFound();
@ -1565,7 +1526,7 @@ public class ImageController : BaseJellyfinApiController
return await GetImageInternal(
user.Id,
imageType,
ImageType.Profile,
imageIndex,
tag,
format,
@ -1586,6 +1547,75 @@ public class ImageController : BaseJellyfinApiController
.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>
/// Get user profile image.
/// </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="fillWidth">Width 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="backgroundColor">Optional. Apply a background color for transparent images.</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.
/// </returns>
[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.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetUserImageByIndex(
public Task<ActionResult> GetUserImageByIndexLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
@ -1633,56 +1664,26 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] int? quality,
[FromQuery] int? fillWidth,
[FromQuery] int? fillHeight,
[FromQuery, ParameterObsolete] bool? cropWhitespace,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
{
var user = _userManager.GetUserById(userId);
if (user?.ProfileImage is null)
{
return NotFound();
}
var info = new ItemImageInfo
{
Path = user.ProfileImage.Path,
Type = ImageType.Profile,
DateModified = user.ProfileImage.LastModified
};
if (width.HasValue)
{
info.Width = width.Value;
}
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);
}
=> GetUserImage(
userId,
tag,
format,
maxWidth,
maxHeight,
percentPlayed,
unplayedCount,
width,
height,
quality,
fillWidth,
fillHeight,
blur,
backgroundColor,
foregroundLayer,
imageIndex);
/// <summary>
/// Generates or gets the splashscreen.

@ -612,8 +612,10 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
[HttpGet("Users/{userId}/Items")]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId(
public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserIdLegacy(
[FromRoute] Guid userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
@ -699,8 +701,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
return GetItems(
=> GetItems(
userId,
maxOfficialRating,
hasThemeSong,
@ -786,7 +787,6 @@ public class ItemsController : BaseJellyfinApiController
genreIds,
enableTotalRecordCount,
enableImages);
}
/// <summary>
/// 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>
/// <response code="200">Items returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
[HttpGet("Users/{userId}/Items/Resume")]
[HttpGet("UserItems/Resume")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
@ -827,7 +827,8 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? enableImages = true,
[FromQuery] bool excludeActiveSessions = false)
{
var user = _userManager.GetUserById(userId);
var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null)
{
return NotFound();
@ -854,7 +855,7 @@ public class ItemsController : BaseJellyfinApiController
if (excludeActiveSessions)
{
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)
.ToArray();
}
@ -887,6 +888,63 @@ public class ItemsController : BaseJellyfinApiController
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>
@ -895,24 +953,43 @@ public class ItemsController : BaseJellyfinApiController
/// <response code="200">return item user data.</response>
/// <response code="404">Item is not found.</response>
/// <returns>Return <see cref="UserItemDataDto"/>.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/UserData")]
[HttpGet("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<UserItemDataDto> GetItemUserData(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
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.");
}
var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
var item = _libraryManager.GetItemById(itemId);
return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user);
}
/// <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("Users/{userId}/Items/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> GetItemUserDataLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> GetItemUserData(userId, itemId);
/// <summary>
/// Update Item User Data.
/// </summary>
@ -922,20 +999,21 @@ public class ItemsController : BaseJellyfinApiController
/// <response code="200">return updated user item data.</response>
/// <response code="404">Item is not found.</response>
/// <returns>Return <see cref="UserItemDataDto"/>.</returns>
[HttpPost("Users/{userId}/Items/{itemId}/UserData")]
[HttpPost("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<UserItemDataDto> UpdateItemUserData(
[FromRoute, Required] Guid userId,
[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 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);
if (item == null)
{
@ -946,4 +1024,24 @@ public class ItemsController : BaseJellyfinApiController
return _userDataRepository.GetUserDataDto(item, user);
}
/// <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("Users/{userId}/Items/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult<UserItemDataDto> UpdateItemUserDataLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromBody, Required] UpdateUserItemDataDto userDataDto)
=> UpdateItemUserData(userId, itemId, userDataDto);
}

@ -68,15 +68,16 @@ public class PlaystateController : BaseJellyfinApiController
/// <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}")]
[HttpPost("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[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)
{
return NotFound();
@ -105,6 +106,26 @@ public class PlaystateController : BaseJellyfinApiController
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>
/// Marks an item as unplayed for user.
/// </summary>
@ -113,12 +134,15 @@ public class PlaystateController : BaseJellyfinApiController
/// <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}")]
[HttpDelete("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[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)
{
return NotFound();
@ -147,6 +171,24 @@ public class PlaystateController : BaseJellyfinApiController
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>
/// Reports playback has started within a session.
/// </summary>
@ -215,9 +257,8 @@ public class PlaystateController : BaseJellyfinApiController
}
/// <summary>
/// Reports that a user has begun playing an item.
/// Reports that a session has begun 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="audioStreamIndex">The audio stream index.</param>
@ -228,11 +269,9 @@ public class PlaystateController : BaseJellyfinApiController
/// <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}")]
[HttpPost("PlayingItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackStart(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
@ -261,11 +300,41 @@ public class PlaystateController : BaseJellyfinApiController
}
/// <summary>
/// Reports a user's playback progress.
/// Reports that a user has begun 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="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="audioStreamIndex">The audio 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>
/// <response code="204">Play progress recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")]
[HttpPost("PlayingItems/{itemId}/Progress")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackProgress(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] long? positionTicks,
@ -319,22 +386,58 @@ public class PlaystateController : BaseJellyfinApiController
}
/// <summary>
/// Reports that a user has stopped playing an item.
/// Reports a user's playback progress.
/// </summary>
/// <param name="userId">User id.</param>
/// <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="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="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}")]
[HttpDelete("PlayingItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackStopped(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] string? nextMediaType,
@ -363,6 +466,33 @@ public class PlaystateController : BaseJellyfinApiController
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>
/// Updates the played status.
/// </summary>

@ -1,7 +1,9 @@
using System;
using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
@ -53,19 +55,26 @@ public class SuggestionsController : BaseJellyfinApiController
/// <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")]
[HttpGet("Items/Suggestions")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? userId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false)
{
var user = userId.IsEmpty()
? null
: _userManager.GetUserById(userId);
User? user;
if (userId.IsNullOrEmpty())
{
user = null;
}
else
{
var requestUserId = RequestHelpers.GetUserId(User, userId);
user = _userManager.GetUserById(requestUserId);
}
var dtoOptions = new DtoOptions().AddClientFields(User);
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
@ -88,4 +97,28 @@ public class SuggestionsController : BaseJellyfinApiController
result.TotalRecordCount,
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);
}

@ -178,6 +178,7 @@ public class UserController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ApiExplorerSettings(IgnoreApi = true)]
[Obsolete("Authenticate with username instead")]
public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
[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="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")]
[HttpPost("Password")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateUserPassword(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? userId,
[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.");
}
var user = _userManager.GetUserById(userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null)
{
@ -290,7 +292,7 @@ public class UserController : BaseJellyfinApiController
}
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(
user.Username,
@ -315,6 +317,27 @@ public class UserController : BaseJellyfinApiController
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>
/// Updates a user's easy password.
/// </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>
[HttpPost("{userId}/EasyPassword")]
[Obsolete("Use Quick Connect instead")]
[ApiExplorerSettings(IgnoreApi = true)]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
@ -346,22 +370,23 @@ public class UserController : BaseJellyfinApiController
/// <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}")]
[HttpPost]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUser(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? userId,
[FromBody, Required] UserDto updateUser)
{
var user = _userManager.GetUserById(userId);
var requestUserId = userId ?? User.GetUserId();
var user = _userManager.GetUserById(requestUserId);
if (user is null)
{
return NotFound();
}
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, requestUserId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
}
@ -376,6 +401,27 @@ public class UserController : BaseJellyfinApiController
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>
/// Updates a user policy.
/// </summary>
@ -440,24 +486,44 @@ public class UserController : BaseJellyfinApiController
/// <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")]
[HttpPost("Configuration")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUserConfiguration(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? userId,
[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");
}
await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
await _userManager.UpdateConfigurationAsync(requestUserId, userConfig).ConfigureAwait(false);
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>
/// Creates a user.
/// </summary>

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
@ -13,12 +14,10 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Lyrics;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -39,7 +38,6 @@ public class UserLibraryController : BaseJellyfinApiController
private readonly IDtoService _dtoService;
private readonly IUserViewManager _userViewManager;
private readonly IFileSystem _fileSystem;
private readonly ILyricManager _lyricManager;
/// <summary>
/// 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="userViewManager">Instance of the <see cref="IUserViewManager"/> 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(
IUserManager userManager,
IUserDataManager userDataRepository,
ILibraryManager libraryManager,
IDtoService dtoService,
IUserViewManager userViewManager,
IFileSystem fileSystem,
ILyricManager lyricManager)
IFileSystem fileSystem)
{
_userManager = userManager;
_userDataRepository = userDataRepository;
@ -66,7 +62,6 @@ public class UserLibraryController : BaseJellyfinApiController
_dtoService = dtoService;
_userViewManager = userViewManager;
_fileSystem = fileSystem;
_lyricManager = lyricManager;
}
/// <summary>
@ -76,11 +71,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <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}")]
[HttpGet("Items/{itemId}")]
[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)
{
return NotFound();
@ -109,17 +107,34 @@ public class UserLibraryController : BaseJellyfinApiController
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>
/// 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")]
[HttpGet("Items/Root")]
[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)
{
return NotFound();
@ -130,6 +145,20 @@ public class UserLibraryController : BaseJellyfinApiController
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>
/// Gets intros to play before the main media item plays.
/// </summary>
@ -137,11 +166,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <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")]
[HttpGet("Items/{itemId}/Intros")]
[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)
{
return NotFound();
@ -170,6 +202,22 @@ public class UserLibraryController : BaseJellyfinApiController
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>
/// Marks an item as a favorite.
/// </summary>
@ -177,11 +225,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <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}")]
[HttpPost("UserFavoriteItems/{itemId}")]
[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)
{
return NotFound();
@ -206,6 +257,22 @@ public class UserLibraryController : BaseJellyfinApiController
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>
/// Unmarks item as a favorite.
/// </summary>
@ -213,11 +280,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <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}")]
[HttpDelete("UserFavoriteItems/{itemId}")]
[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)
{
return NotFound();
@ -242,6 +312,22 @@ public class UserLibraryController : BaseJellyfinApiController
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>
/// Deletes a user's saved personal rating for an item.
/// </summary>
@ -249,11 +335,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <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")]
[HttpDelete("UserItems/{itemId}/Rating")]
[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)
{
return NotFound();
@ -278,6 +367,22 @@ public class UserLibraryController : BaseJellyfinApiController
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>
/// Updates a user's rating for an item.
/// </summary>
@ -286,11 +391,15 @@ public class UserLibraryController : BaseJellyfinApiController
/// <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")]
[HttpPost("UserItems/{itemId}/Rating")]
[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)
{
return NotFound();
@ -315,6 +424,24 @@ public class UserLibraryController : BaseJellyfinApiController
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>
/// Gets local trailers for an item.
/// </summary>
@ -322,11 +449,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <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")]
[HttpGet("Items/{itemId}/LocalTrailers")]
[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)
{
return NotFound();
@ -360,6 +490,22 @@ public class UserLibraryController : BaseJellyfinApiController
.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>
/// Gets special features for an item.
/// </summary>
@ -367,11 +513,14 @@ public class UserLibraryController : BaseJellyfinApiController
/// <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")]
[HttpGet("Items/{itemId}/SpecialFeatures")]
[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)
{
return NotFound();
@ -401,6 +550,22 @@ public class UserLibraryController : BaseJellyfinApiController
.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>
/// Gets latest media.
/// </summary>
@ -417,10 +582,10 @@ public class UserLibraryController : BaseJellyfinApiController
/// <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")]
[HttpGet("Items/Latest")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
@ -432,7 +597,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] int limit = 20,
[FromQuery] bool groupItems = true)
{
var user = _userManager.GetUserById(userId);
var requestUserId = RequestHelpers.GetUserId(User, userId);
var user = _userManager.GetUserById(requestUserId);
if (user is null)
{
return NotFound();
@ -458,7 +624,7 @@ public class UserLibraryController : BaseJellyfinApiController
IsPlayed = isPlayed,
Limit = limit,
ParentId = parentId ?? Guid.Empty,
UserId = userId,
UserId = requestUserId,
},
dtoOptions);
@ -483,6 +649,51 @@ public class UserLibraryController : BaseJellyfinApiController
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)
{
if (item is Person)

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
using Jellyfin.Data.Enums;
@ -59,19 +60,17 @@ public class UserViewsController : BaseJellyfinApiController
/// <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")]
[HttpGet("UserViews")]
[ProducesResponseType(StatusCodes.Status200OK)]
public QueryResult<BaseItemDto> GetUserViews(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? userId,
[FromQuery] bool? includeExternalContent,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews,
[FromQuery] bool includeHidden = false)
{
var query = new UserViewQuery
{
UserId = userId,
IncludeHidden = includeHidden
};
userId = RequestHelpers.GetUserId(User, userId);
var query = new UserViewQuery { UserId = userId.Value, IncludeHidden = includeHidden };
if (includeExternalContent.HasValue)
{
@ -92,7 +91,7 @@ public class UserViewsController : BaseJellyfinApiController
fields.Add(ItemFields.DisplayPreferencesId);
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))
.ToArray();
@ -100,6 +99,26 @@ public class UserViewsController : BaseJellyfinApiController
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>
/// Get user view grouping options.
/// </summary>
@ -110,12 +129,13 @@ public class UserViewsController : BaseJellyfinApiController
/// An <see cref="OkResult"/> containing the user view grouping options
/// or a <see cref="NotFoundResult"/> if user not found.
/// </returns>
[HttpGet("Users/{userId}/GroupingOptions")]
[HttpGet("UserViews/GroupingOptions")]
[ProducesResponseType(StatusCodes.Status200OK)]
[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)
{
return NotFound();
@ -133,4 +153,23 @@ public class UserViewsController : BaseJellyfinApiController
.OrderBy(i => i.Name)
.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);
}

Loading…
Cancel
Save