diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 8368b846dd..6b38fa7d34 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -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. /// /// User Id. - /// (Unused) Image type. - /// (Unused) Image index. /// Image updated. /// User does not have permission to delete the image. /// A . - [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 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 } } + /// + /// Sets the user image. + /// + /// User Id. + /// (Unused) Image type. + /// Image updated. + /// User does not have permission to delete the image. + /// A . + [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 PostUserImageLegacy( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType) + => PostUserImage(userId); + /// /// Sets the user image. /// @@ -153,53 +172,57 @@ public class ImageController : BaseJellyfinApiController /// A . [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 PostUserImageByIndex( + public Task PostUserImageByIndexLegacy( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, [FromRoute] int index) + => PostUserImage(userId); + + /// + /// Delete the user's image. + /// + /// User Id. + /// Image deleted. + /// User does not have permission to delete the image. + /// A . + [HttpDelete("UserImage")] + [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task DeleteUserImage( + [FromQuery] Guid? userId) { - var user = _userManager.GetUserById(userId); - if (user is null) + var requestUserId = RequestHelpers.GetUserId(User, userId); + 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(); } + + 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(); } /// @@ -213,38 +236,17 @@ public class ImageController : BaseJellyfinApiController /// A . [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 async Task DeleteUserImage( + public Task DeleteUserImageLegacy( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, [FromQuery] int? index = null) - { - 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); /// /// Delete the user's image. @@ -257,38 +259,17 @@ public class ImageController : BaseJellyfinApiController /// A . [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 DeleteUserImageByIndex( + public Task 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); /// /// Delete an item's image. @@ -541,7 +522,6 @@ public class ImageController : BaseJellyfinApiController /// Width of box to fill. /// Height of box to fill. /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. The of the returned image. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. @@ -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 /// Width of box to fill. /// Height of box to fill. /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. The of the returned image. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. @@ -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 /// Width of box to fill. /// Height of box to fill. /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Determines the output format of the image - original,gif,jpg,png. /// Optional. Percent to render for the percent played overlay. /// Optional. Unplayed count overlay to render. @@ -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 /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. @@ -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 /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. @@ -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 /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. @@ -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 /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. @@ -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 /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. @@ -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 /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. @@ -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 /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. @@ -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 /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. @@ -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 /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. @@ -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. /// /// User id. - /// Image type. /// Optional. Supply the cache tag from the item object to receive strong caching headers. /// Determines the output format of the image - original,gif,jpg,png. /// The maximum image width to return. @@ -1504,25 +1460,25 @@ public class ImageController : BaseJellyfinApiController /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image index. /// Image stream returned. + /// User id not provided. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// - [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 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); } + /// + /// Get user profile image. + /// + /// User id. + /// Image type. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [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 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); + /// /// Get user profile image. /// @@ -1603,7 +1633,6 @@ public class ImageController : BaseJellyfinApiController /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. /// Width of box to fill. /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. @@ -1614,11 +1643,13 @@ public class ImageController : BaseJellyfinApiController /// or a if item not found. /// [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 GetUserImageByIndex( + public Task 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); /// /// Generates or gets the splashscreen. diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index d10fba920c..26ae1a820f 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -612,8 +612,10 @@ public class ItemsController : BaseJellyfinApiController /// Optional, include image information in output. /// A with the items. [HttpGet("Users/{userId}/Items")] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetItemsByUserId( + public ActionResult> 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); - } /// /// Gets items based on a query. @@ -808,10 +808,10 @@ public class ItemsController : BaseJellyfinApiController /// Optional. Whether to exclude the currently active sessions. /// Items returned. /// A with the items that are resumable. - [HttpGet("Users/{userId}/Items/Resume")] + [HttpGet("UserItems/Resume")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> 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,90 @@ public class ItemsController : BaseJellyfinApiController returnItems); } + /// + /// Gets items based on a query. + /// + /// The user id. + /// The start index. + /// The item limit. + /// The search term. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// 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. + /// Optional. Filter by MediaType. Allows multiple, comma delimited. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited. + /// Optional. Enable the total record count. + /// Optional. Include image information in output. + /// Optional. Whether to exclude the currently active sessions. + /// Items returned. + /// A with the items that are resumable. + [HttpGet("Users/{userId}/Items/Resume")] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> 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); + + /// + /// Get Item User Data. + /// + /// The user id. + /// The item id. + /// return item user data. + /// Item is not found. + /// Return . + [HttpGet("UserItems/{itemId}/UserData")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult 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); + } + /// /// Get Item User Data. /// @@ -898,19 +983,46 @@ public class ItemsController : BaseJellyfinApiController [HttpGet("Users/{userId}/Items/{itemId}/UserData")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetItemUserData( + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult GetItemUserDataLegacy( [FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + => GetItemUserData(userId, itemId); + + /// + /// Update Item User Data. + /// + /// The user id. + /// The item id. + /// New user data object. + /// return updated user item data. + /// Item is not found. + /// Return . + [HttpPost("UserItems/{itemId}/UserData")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult 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); + 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); } /// @@ -925,25 +1037,11 @@ public class ItemsController : BaseJellyfinApiController [HttpPost("Users/{userId}/Items/{itemId}/UserData")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItemUserData( + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult UpdateItemUserDataLegacy( [FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromBody, Required] UpdateUserItemDataDto 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); - } + => UpdateItemUserData(userId, itemId, userDataDto); } diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index bde2f4d1ac..949d101dcd 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -68,15 +68,16 @@ public class PlaystateController : BaseJellyfinApiController /// Item marked as played. /// Item not found. /// An containing the , or a if item was not found. - [HttpPost("Users/{userId}/PlayedItems/{itemId}")] + [HttpPost("UserPlayedItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> 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; } + /// + /// Marks an item as played for user. + /// + /// User id. + /// Item id. + /// Optional. The date the item was played. + /// Item marked as played. + /// Item not found. + /// An containing the , or a if item was not found. + [HttpPost("Users/{userId}/PlayedItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public Task> MarkPlayedItemLegacy( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId, + [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed) + => MarkPlayedItem(userId, itemId, datePlayed); + /// /// Marks an item as unplayed for user. /// @@ -113,12 +134,15 @@ public class PlaystateController : BaseJellyfinApiController /// Item marked as unplayed. /// Item not found. /// A containing the , or a if item was not found. - [HttpDelete("Users/{userId}/PlayedItems/{itemId}")] + [HttpDelete("UserPlayedItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + public async Task> 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; } + /// + /// Marks an item as unplayed for user. + /// + /// User id. + /// Item id. + /// Item marked as unplayed. + /// Item not found. + /// A containing the , or a if item was not found. + [HttpDelete("Users/{userId}/PlayedItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public Task> MarkUnplayedItemLegacy( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId) + => MarkUnplayedItem(userId, itemId); + /// /// Reports playback has started within a session. /// @@ -215,9 +257,8 @@ public class PlaystateController : BaseJellyfinApiController } /// - /// Reports that a user has begun playing an item. + /// Reports that a session has begun playing an item. /// - /// User id. /// Item id. /// The id of the MediaSource. /// The audio stream index. @@ -228,11 +269,9 @@ public class PlaystateController : BaseJellyfinApiController /// Indicates if the client can seek. /// Play start recorded. /// A . - [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 OnPlaybackStart( - [FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] string? mediaSourceId, [FromQuery] int? audioStreamIndex, @@ -261,11 +300,41 @@ public class PlaystateController : BaseJellyfinApiController } /// - /// Reports a user's playback progress. + /// Reports that a user has begun playing an item. /// /// User id. /// Item id. /// The id of the MediaSource. + /// The audio stream index. + /// The subtitle stream index. + /// The play method. + /// The live stream id. + /// The play session id. + /// Indicates if the client can seek. + /// Play start recorded. + /// A . + [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 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); + + /// + /// Reports a session's playback progress. + /// + /// Item id. + /// The id of the MediaSource. /// Optional. The current position, in ticks. 1 tick = 10000 ms. /// The audio stream index. /// The subtitle stream index. @@ -278,11 +347,9 @@ public class PlaystateController : BaseJellyfinApiController /// Indicates if the player is muted. /// Play progress recorded. /// A . - [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 OnPlaybackProgress( - [FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] string? mediaSourceId, [FromQuery] long? positionTicks, @@ -319,22 +386,58 @@ public class PlaystateController : BaseJellyfinApiController } /// - /// Reports that a user has stopped playing an item. + /// Reports a user's playback progress. /// /// User id. /// Item id. /// The id of the MediaSource. + /// Optional. The current position, in ticks. 1 tick = 10000 ms. + /// The audio stream index. + /// The subtitle stream index. + /// Scale of 0-100. + /// The play method. + /// The live stream id. + /// The play session id. + /// The repeat mode. + /// Indicates if the player is paused. + /// Indicates if the player is muted. + /// Play progress recorded. + /// A . + [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 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); + + /// + /// Reports that a session has stopped playing an item. + /// + /// Item id. + /// The id of the MediaSource. /// The next media type that will play. /// Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms. /// The live stream id. /// The play session id. /// Playback stop recorded. /// A . - [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 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(); } + /// + /// Reports that a user has stopped playing an item. + /// + /// User id. + /// Item id. + /// The id of the MediaSource. + /// The next media type that will play. + /// Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms. + /// The live stream id. + /// The play session id. + /// Playback stop recorded. + /// A . + [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 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); + /// /// Updates the played status. /// diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 2aa6d25a79..ad625cc6e0 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -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 /// Whether to enable the total record count. /// Suggestions returned. /// A with the suggestions. - [HttpGet("Users/{userId}/Suggestions")] + [HttpGet("Items/Suggestions")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> 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); } + + /// + /// Gets suggestions. + /// + /// The user id. + /// The media types. + /// The type. + /// Optional. The start index. + /// Optional. The limit. + /// Whether to enable the total record count. + /// Suggestions returned. + /// A with the suggestions. + [HttpGet("Users/{userId}/Suggestions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult> 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); } diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index ea10ee24f9..c3923a2ada 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -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> AuthenticateUser( [FromRoute, Required] Guid userId, @@ -263,21 +264,22 @@ public class UserController : BaseJellyfinApiController /// User is not allowed to update the password. /// User not found. /// A indicating success or a or a on failure. - [HttpPost("{userId}/Password")] + [HttpPost("Password")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task 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(); } + /// + /// Updates a user's password. + /// + /// The user id. + /// The request. + /// Password successfully reset. + /// User is not allowed to update the password. + /// User not found. + /// A indicating success or a or a on failure. + [HttpPost("{userId}/Password")] + [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public Task UpdateUserPasswordLegacy( + [FromRoute, Required] Guid userId, + [FromBody, Required] UpdateUserPassword request) + => UpdateUserPassword(userId, request); + /// /// Updates a user's easy password. /// @@ -326,6 +349,7 @@ public class UserController : BaseJellyfinApiController /// A indicating success or a or a on failure. [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 /// User information was not supplied. /// User update forbidden. /// A indicating success or a or a on failure. - [HttpPost("{userId}")] + [HttpPost] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task 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(); } + /// + /// Updates a user. + /// + /// The user id. + /// The updated user model. + /// User updated. + /// User information was not supplied. + /// User update forbidden. + /// A indicating success or a or a on failure. + [HttpPost("{userId}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public Task UpdateUserLegacy( + [FromRoute, Required] Guid userId, + [FromBody, Required] UserDto updateUser) + => UpdateUser(userId, updateUser); + /// /// Updates a user policy. /// @@ -440,24 +486,44 @@ public class UserController : BaseJellyfinApiController /// User configuration updated. /// User configuration update forbidden. /// A indicating success. - [HttpPost("{userId}/Configuration")] + [HttpPost("Configuration")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task 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(); } + /// + /// Updates a user configuration. + /// + /// The user id. + /// The new user configuration. + /// User configuration updated. + /// User configuration update forbidden. + /// A indicating success. + [HttpPost("{userId}/Configuration")] + [Authorize] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public Task UpdateUserConfigurationLegacy( + [FromRoute, Required] Guid userId, + [FromBody, Required] UserConfiguration userConfig) + => UpdateUserConfiguration(userId, userConfig); + /// /// Creates a user. /// diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index e3bfd4ea9c..c19ad33c84 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -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; /// /// Initializes a new instance of the class. @@ -50,15 +48,13 @@ public class UserLibraryController : BaseJellyfinApiController /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. 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; } /// @@ -76,11 +71,14 @@ public class UserLibraryController : BaseJellyfinApiController /// Item id. /// Item returned. /// An containing the item. - [HttpGet("Users/{userId}/Items/{itemId}")] + [HttpGet("Items/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + public async Task> 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); } + /// + /// Gets an item from a user's library. + /// + /// User id. + /// Item id. + /// Item returned. + /// An containing the item. + [HttpGet("Users/{userId}/Items/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public Task> GetItemLegacy( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId) + => GetItem(userId, itemId); + /// /// Gets the root folder from a user's library. /// /// User id. /// Root folder returned. /// An containing the user's root folder. - [HttpGet("Users/{userId}/Items/Root")] + [HttpGet("Items/Root")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetRootFolder([FromRoute, Required] Guid userId) + public ActionResult 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); } + /// + /// Gets the root folder from a user's library. + /// + /// User id. + /// Root folder returned. + /// An containing the user's root folder. + [HttpGet("Users/{userId}/Items/Root")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult GetRootFolderLegacy( + [FromRoute, Required] Guid userId) + => GetRootFolder(userId); + /// /// Gets intros to play before the main media item plays. /// @@ -137,11 +166,14 @@ public class UserLibraryController : BaseJellyfinApiController /// Item id. /// Intros returned. /// An containing the intros to play. - [HttpGet("Users/{userId}/Items/{itemId}/Intros")] + [HttpGet("Items/{itemId}/Intros")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + public async Task>> 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(dtos); } + /// + /// Gets intros to play before the main media item plays. + /// + /// User id. + /// Item id. + /// Intros returned. + /// An containing the intros to play. + [HttpGet("Users/{userId}/Items/{itemId}/Intros")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public Task>> GetIntrosLegacy( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId) + => GetIntros(userId, itemId); + /// /// Marks an item as a favorite. /// @@ -177,11 +225,14 @@ public class UserLibraryController : BaseJellyfinApiController /// Item id. /// Item marked as favorite. /// An containing the . - [HttpPost("Users/{userId}/FavoriteItems/{itemId}")] + [HttpPost("UserFavoriteItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + public ActionResult 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); } + /// + /// Marks an item as a favorite. + /// + /// User id. + /// Item id. + /// Item marked as favorite. + /// An containing the . + [HttpPost("Users/{userId}/FavoriteItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult MarkFavoriteItemLegacy( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId) + => MarkFavoriteItem(userId, itemId); + /// /// Unmarks item as a favorite. /// @@ -213,11 +280,14 @@ public class UserLibraryController : BaseJellyfinApiController /// Item id. /// Item unmarked as favorite. /// An containing the . - [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")] + [HttpDelete("UserFavoriteItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + public ActionResult 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); } + /// + /// Unmarks item as a favorite. + /// + /// User id. + /// Item id. + /// Item unmarked as favorite. + /// An containing the . + [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult UnmarkFavoriteItemLegacy( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId) + => UnmarkFavoriteItem(userId, itemId); + /// /// Deletes a user's saved personal rating for an item. /// @@ -249,11 +335,14 @@ public class UserLibraryController : BaseJellyfinApiController /// Item id. /// Personal rating removed. /// An containing the . - [HttpDelete("Users/{userId}/Items/{itemId}/Rating")] + [HttpDelete("UserItems/{itemId}/Rating")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + public ActionResult 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); } + /// + /// Deletes a user's saved personal rating for an item. + /// + /// User id. + /// Item id. + /// Personal rating removed. + /// An containing the . + [HttpDelete("Users/{userId}/Items/{itemId}/Rating")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult DeleteUserItemRatingLegacy( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId) + => DeleteUserItemRating(userId, itemId); + /// /// Updates a user's rating for an item. /// @@ -286,11 +391,15 @@ public class UserLibraryController : BaseJellyfinApiController /// Whether this is likes. /// Item rating updated. /// An containing the . - [HttpPost("Users/{userId}/Items/{itemId}/Rating")] + [HttpPost("UserItems/{itemId}/Rating")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes) + public ActionResult 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); } + /// + /// Updates a user's rating for an item. + /// + /// User id. + /// Item id. + /// Whether this is likes. + /// Item rating updated. + /// An containing the . + [HttpPost("Users/{userId}/Items/{itemId}/Rating")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult UpdateUserItemRatingLegacy( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId, + [FromQuery] bool? likes) + => UpdateUserItemRating(userId, itemId, likes); + /// /// Gets local trailers for an item. /// @@ -322,11 +449,14 @@ public class UserLibraryController : BaseJellyfinApiController /// Item id. /// An containing the item's local trailers. /// The items local trailers. - [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")] + [HttpGet("Items/{itemId}/LocalTrailers")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + public ActionResult> 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))); } + /// + /// Gets local trailers for an item. + /// + /// User id. + /// Item id. + /// An containing the item's local trailers. + /// The items local trailers. + [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult> GetLocalTrailersLegacy( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId) + => GetLocalTrailers(userId, itemId); + /// /// Gets special features for an item. /// @@ -367,11 +513,14 @@ public class UserLibraryController : BaseJellyfinApiController /// Item id. /// Special features returned. /// An containing the special features. - [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")] + [HttpGet("Items/{itemId}/SpecialFeatures")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + public ActionResult> 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))); } + /// + /// Gets special features for an item. + /// + /// User id. + /// Item id. + /// Special features returned. + /// An containing the special features. + [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult> GetSpecialFeaturesLegacy( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId) + => GetSpecialFeatures(userId, itemId); + /// /// Gets latest media. /// @@ -417,10 +582,10 @@ public class UserLibraryController : BaseJellyfinApiController /// Whether or not to group items into a parent container. /// Latest media returned. /// An containing the latest media. - [HttpGet("Users/{userId}/Items/Latest")] + [HttpGet("Items/Latest")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> 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); } + /// + /// Gets latest media. + /// + /// User id. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Filter by items that are played, or not. + /// Optional. include image information in output. + /// Optional. the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. include user data. + /// Return item limit. + /// Whether or not to group items into a parent container. + /// Latest media returned. + /// An containing the latest media. + [HttpGet("Users/{userId}/Items/Latest")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult> 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) diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 035d044741..bf3ce1d396 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -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 /// Whether or not to include hidden content. /// User views returned. /// An containing the user views. - [HttpGet("Users/{userId}/Views")] + [HttpGet("UserViews")] [ProducesResponseType(StatusCodes.Status200OK)] public QueryResult 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(dtos); } + /// + /// Get user views. + /// + /// User id. + /// Whether or not to include external views such as channels or live tv. + /// Preset views. + /// Whether or not to include hidden content. + /// User views returned. + /// An containing the user views. + [HttpGet("Users/{userId}/Views")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public QueryResult GetUserViewsLegacy( + [FromRoute, Required] Guid userId, + [FromQuery] bool? includeExternalContent, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews, + [FromQuery] bool includeHidden = false) + => GetUserViews(userId, includeExternalContent, presetViews, includeHidden); + /// /// Get user view grouping options. /// @@ -110,12 +129,13 @@ public class UserViewsController : BaseJellyfinApiController /// An containing the user view grouping options /// or a if user not found. /// - [HttpGet("Users/{userId}/GroupingOptions")] + [HttpGet("UserViews/GroupingOptions")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult> GetGroupingOptions([FromRoute, Required] Guid userId) + public ActionResult> 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()); } + + /// + /// Get user view grouping options. + /// + /// User id. + /// User view grouping options returned. + /// User not found. + /// + /// An containing the user view grouping options + /// or a if user not found. + /// + [HttpGet("Users/{userId}/GroupingOptions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Obsolete("Kept for backwards compatibility")] + [ApiExplorerSettings(IgnoreApi = true)] + public ActionResult> GetGroupingOptionsLegacy( + [FromRoute, Required] Guid userId) + => GetGroupingOptions(userId); }