From cd273c4e98246420edf39ef63c906fbc3725d8e4 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 19 Jun 2020 15:08:35 -0600 Subject: Start move ImageService.cs to Jellyfin.Api --- Jellyfin.Api/Controllers/ImageController.cs | 139 ++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 Jellyfin.Api/Controllers/ImageController.cs (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs new file mode 100644 index 000000000..6742bffc6 --- /dev/null +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -0,0 +1,139 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Image controller. + /// + public class ImageController : BaseJellyfinApiController + { + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IProviderManager _providerManager; + private readonly IImageProcessor _imageProcessor; + private readonly IFileSystem _fileSystem; + private readonly IAuthorizationContext _authContext; + private readonly ILogger _logger; + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ImageController( + IUserManager userManager, + ILibraryManager libraryManager, + IProviderManager providerManager, + IImageProcessor imageProcessor, + IFileSystem fileSystem, + IAuthorizationContext authContext, + ILogger logger, + IServerConfigurationManager serverConfigurationManager) + { + _userManager = userManager; + _libraryManager = libraryManager; + _providerManager = providerManager; + _imageProcessor = imageProcessor; + _fileSystem = fileSystem; + _authContext = authContext; + _logger = logger; + _serverConfigurationManager = serverConfigurationManager; + } + + /// + /// Sets the user image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image updated. + /// A . + [HttpPost("/Users/{userId}/Images/{imageType}")] + [HttpPost("/Users/{userId}/Images/{imageType}/{index}")] + public async Task PostUserImage( + [FromRoute] Guid userId, + [FromRoute] ImageType imageType, + [FromRoute] int? index) + { + // TODO AssertCanUpdateUser(_authContext, _userManager, id, true); + + var user = _userManager.GetUserById(userId); + await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType.Split(';').FirstOrDefault(); + var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); + + await _providerManager + .SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path) + .ConfigureAwait(false); + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Delete the user's image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image deleted. + /// A . + [HttpDelete("/Users/{userId}/Images/{itemType}")] + [HttpDelete("/Users/{userId}/Images/{itemType}/{index}")] + public ActionResult DeleteUserImage( + [FromRoute] Guid userId, + [FromRoute] ImageType imageType, + [FromRoute] int? index) + { + // TODO AssertCanUpdateUser(_authContext, _userManager, userId, true); + + var user = _userManager.GetUserById(userId); + try + { + System.IO.File.Delete(user.ProfileImage.Path); + } + catch (IOException e) + { + _logger.LogError(e, "Error deleting user profile image:"); + } + + _userManager.ClearProfileImage(user); + return NoContent(); + } + + private static async Task GetMemoryStream(Stream inputStream) + { + using var reader = new StreamReader(inputStream); + var text = await reader.ReadToEndAsync().ConfigureAwait(false); + + var bytes = Convert.FromBase64String(text); + return new MemoryStream(bytes) + { + Position = 0 + }; + } + } +} -- cgit v1.2.3 From 9a8deadc215aa1ca25e1667c8c373a13e07d301e Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 20 Jun 2020 17:06:33 -0600 Subject: implement all non image get endpoints --- Jellyfin.Api/Controllers/ImageController.cs | 242 +++++++++++++++++++++++++- MediaBrowser.Api/Images/ImageService.cs | 261 ---------------------------- 2 files changed, 236 insertions(+), 267 deletions(-) (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 6742bffc6..d8c67dbea 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1,15 +1,24 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -69,13 +78,18 @@ namespace Jellyfin.Api.Controllers /// Image updated. /// A . [HttpPost("/Users/{userId}/Images/{imageType}")] - [HttpPost("/Users/{userId}/Images/{imageType}/{index}")] + [HttpPost("/Users/{userId}/Images/{imageType}/{index?}")] + [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] Guid userId, [FromRoute] ImageType imageType, - [FromRoute] int? index) + [FromRoute] int? index = null) { - // TODO AssertCanUpdateUser(_authContext, _userManager, id, true); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + { + return Forbid("User is not allowed to update the image."); + } var user = _userManager.GetUserById(userId); await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); @@ -102,13 +116,19 @@ namespace Jellyfin.Api.Controllers /// Image deleted. /// A . [HttpDelete("/Users/{userId}/Images/{itemType}")] - [HttpDelete("/Users/{userId}/Images/{itemType}/{index}")] + [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}")] + [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)] public ActionResult DeleteUserImage( [FromRoute] Guid userId, [FromRoute] ImageType imageType, - [FromRoute] int? index) + [FromRoute] int? index = null) { - // TODO AssertCanUpdateUser(_authContext, _userManager, userId, true); + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + { + return Forbid("User is not allowed to delete the image."); + } var user = _userManager.GetUserById(userId); try @@ -124,6 +144,164 @@ namespace Jellyfin.Api.Controllers return NoContent(); } + /// + /// Delete an item's image. + /// + /// Item id. + /// Image type. + /// The image index. + /// Image deleted. + /// Item not found. + /// A on success, or a if item not found. + [HttpDelete("/Items/{itemId}/Images/{imageType}")] + [HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult DeleteItemImage( + [FromRoute] Guid itemId, + [FromRoute] ImageType imageType, + [FromRoute] int? imageIndex = null) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + item.DeleteImage(imageType, imageIndex ?? 0); + return NoContent(); + } + + /// + /// Set item image. + /// + /// Item id. + /// Image type. + /// (Unused) Image index. + /// Image saved. + /// Item not found. + /// A on success, or a if item not found. + [HttpPost("/Items/{itemId}/Images/{imageType}")] + [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + public async Task SetItemImage( + [FromRoute] Guid itemId, + [FromRoute] ImageType imageType, + [FromRoute] int? imageIndex = null) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType.Split(';').FirstOrDefault(); + await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + + return NoContent(); + } + + /// + /// Updates the index for an item image. + /// + /// Item id. + /// Image type. + /// Old image index. + /// New image index. + /// Image index updated. + /// Item not found. + /// A on success, or a if item not found. + [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex}/Index")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult UpdateItemImageIndex( + [FromRoute] Guid itemId, + [FromRoute] ImageType imageType, + [FromRoute] int imageIndex, + [FromQuery] int newIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + item.SwapImages(imageType, imageIndex, newIndex); + return NoContent(); + } + + /// + /// Get item image infos. + /// + /// Item id. + /// Item images returned. + /// Item not found. + /// The list of image infos on success, or if item not found. + [HttpGet("/Items/{itemId}/Images")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetItemImageInfos([FromRoute] Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + var list = new List(); + var itemImages = item.ImageInfos; + + if (itemImages.Length == 0) + { + // short-circuit + return list; + } + + _libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct + + foreach (var image in itemImages) + { + if (!item.AllowsMultipleImages(image.Type)) + { + var info = GetImageInfo(item, image, null); + + if (info != null) + { + list.Add(info); + } + } + } + + foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages)) + { + var index = 0; + + // Prevent implicitly captured closure + var currentImageType = imageType; + + foreach (var image in itemImages.Where(i => i.Type == currentImageType)) + { + var info = GetImageInfo(item, image, index); + + if (info != null) + { + list.Add(info); + } + + index++; + } + } + + return list; + } + private static async Task GetMemoryStream(Stream inputStream) { using var reader = new StreamReader(inputStream); @@ -135,5 +313,57 @@ namespace Jellyfin.Api.Controllers Position = 0 }; } + + private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) + { + int? width = null; + int? height = null; + string? blurhash = null; + long length = 0; + + try + { + if (info.IsLocalFile) + { + var fileInfo = _fileSystem.GetFileInfo(info.Path); + length = fileInfo.Length; + + blurhash = info.BlurHash; + width = info.Width; + height = info.Height; + + if (width <= 0 || height <= 0) + { + width = null; + height = null; + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting image information for {Item}", item.Name); + } + + try + { + return new ImageInfo + { + Path = info.Path, + ImageIndex = imageIndex, + ImageType = info.Type, + ImageTag = _imageProcessor.GetImageCacheTag(item, info), + Size = length, + BlurHash = blurhash, + Width = width, + Height = height + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting image information for {Path}", info.Path); + + return null; + } + } } } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 48c879bb7..a98266e0d 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; @@ -15,7 +14,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; @@ -26,21 +24,6 @@ using User = Jellyfin.Data.Entities.User; namespace MediaBrowser.Api.Images { - /// - /// Class GetItemImage. - /// - [Route("/Items/{Id}/Images", "GET", Summary = "Gets information about an item's images")] - [Authenticated] - public class GetItemImageInfos : IReturn> - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } - [Route("/Items/{Id}/Images/{Type}", "GET")] [Route("/Items/{Id}/Images/{Type}/{Index}", "GET")] [Route("/Items/{Id}/Images/{Type}", "HEAD")] @@ -57,42 +40,6 @@ namespace MediaBrowser.Api.Images public Guid Id { get; set; } } - /// - /// Class UpdateItemImageIndex - /// - [Route("/Items/{Id}/Images/{Type}/{Index}/Index", "POST", Summary = "Updates the index for an item image")] - [Authenticated(Roles = "admin")] - public class UpdateItemImageIndex : IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// Gets or sets the type of the image. - /// - /// The type of the image. - [ApiMember(Name = "Type", Description = "Image Type", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public ImageType Type { get; set; } - - /// - /// Gets or sets the index. - /// - /// The index. - [ApiMember(Name = "Index", Description = "Image Index", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] - public int Index { get; set; } - - /// - /// Gets or sets the new index. - /// - /// The new index. - [ApiMember(Name = "NewIndex", Description = "The new image index", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public int NewIndex { get; set; } - } - /// /// Class GetPersonImage /// @@ -147,44 +94,6 @@ namespace MediaBrowser.Api.Images public Guid Id { get; set; } } - /// - /// Class DeleteItemImage - /// - [Route("/Items/{Id}/Images/{Type}", "DELETE")] - [Route("/Items/{Id}/Images/{Type}/{Index}", "DELETE")] - [Authenticated(Roles = "admin")] - public class DeleteItemImage : DeleteImageRequest, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } - } - - /// - /// Class PostItemImage - /// - [Route("/Items/{Id}/Images/{Type}", "POST")] - [Route("/Items/{Id}/Images/{Type}/{Index}", "POST")] - [Authenticated(Roles = "admin")] - public class PostItemImage : DeleteImageRequest, IRequiresRequestStream, IReturnVoid - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - - /// - /// The raw Http Request Input Stream - /// - /// The request stream. - public Stream RequestStream { get; set; } - } - /// /// Class ImageService /// @@ -223,126 +132,6 @@ namespace MediaBrowser.Api.Images _authContext = authContext; } - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetItemImageInfos request) - { - var item = _libraryManager.GetItemById(request.Id); - - var result = GetItemImageInfos(item); - - return ToOptimizedResult(result); - } - - /// - /// Gets the item image infos. - /// - /// The item. - /// Task{List{ImageInfo}}. - public List GetItemImageInfos(BaseItem item) - { - var list = new List(); - var itemImages = item.ImageInfos; - - if (itemImages.Length == 0) - { - // short-circuit - return list; - } - - _libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct - - foreach (var image in itemImages) - { - if (!item.AllowsMultipleImages(image.Type)) - { - var info = GetImageInfo(item, image, null); - - if (info != null) - { - list.Add(info); - } - } - } - - foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages)) - { - var index = 0; - - // Prevent implicitly captured closure - var currentImageType = imageType; - - foreach (var image in itemImages.Where(i => i.Type == currentImageType)) - { - var info = GetImageInfo(item, image, index); - - if (info != null) - { - list.Add(info); - } - - index++; - } - } - - return list; - } - - private ImageInfo GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) - { - int? width = null; - int? height = null; - string blurhash = null; - long length = 0; - - try - { - if (info.IsLocalFile) - { - var fileInfo = _fileSystem.GetFileInfo(info.Path); - length = fileInfo.Length; - - blurhash = info.BlurHash; - width = info.Width; - height = info.Height; - - if (width <= 0 || height <= 0) - { - width = null; - height = null; - } - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting image information for {Item}", item.Name); - } - - try - { - return new ImageInfo - { - Path = info.Path, - ImageIndex = imageIndex, - ImageType = info.Type, - ImageTag = _imageProcessor.GetImageCacheTag(item, info), - Size = length, - BlurHash = blurhash, - Width = width, - Height = height - }; - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting image information for {Path}", info.Path); - - return null; - } - } - /// /// Gets the specified request. /// @@ -400,56 +189,6 @@ namespace MediaBrowser.Api.Images return GetImage(request, item.Id, item, true); } - /// - /// Posts the specified request. - /// - /// The request. - public Task Post(PostItemImage request) - { - var id = Guid.Parse(GetPathValue(1)); - - request.Type = Enum.Parse(GetPathValue(3).ToString(), true); - - var item = _libraryManager.GetItemById(id); - - return PostImage(item, request.RequestStream, request.Type, Request.ContentType); - } - - /// - /// Deletes the specified request. - /// - /// The request. - public void Delete(DeleteItemImage request) - { - var item = _libraryManager.GetItemById(request.Id); - - item.DeleteImage(request.Type, request.Index ?? 0); - } - - /// - /// Posts the specified request. - /// - /// The request. - public void Post(UpdateItemImageIndex request) - { - var item = _libraryManager.GetItemById(request.Id); - - UpdateItemIndex(item, request.Type, request.Index, request.NewIndex); - } - - /// - /// Updates the index of the item. - /// - /// The item. - /// The type. - /// Index of the current. - /// The new index. - /// Task. - private void UpdateItemIndex(BaseItem item, ImageType type, int currentIndex, int newIndex) - { - item.SwapImages(type, currentIndex, newIndex); - } - /// /// Gets the image. /// -- cgit v1.2.3 From ccd7b3f52435de880158bc41dec9268dc9acbdd5 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 21 Jun 2020 11:31:44 -0600 Subject: WIP GetImage endpoints --- Jellyfin.Api/Controllers/ImageController.cs | 401 +++++++++++++++++++++++++++- 1 file changed, 397 insertions(+), 4 deletions(-) (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index d8c67dbea..c24c5e24c 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -13,6 +14,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -21,6 +23,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Controllers { @@ -302,16 +305,172 @@ namespace Jellyfin.Api.Controllers return list; } + /// + /// Gets the item's image. + /// + /// Item id. + /// Image type. + /// The maximum image width to return. + /// The maximum image height to return. + /// 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. + /// 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. Add a played indicator. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Enable or disable image enhancers such as cover art. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("/Items/{itemId}/Images/{imageType}")] + [HttpHead("/Items/{itemId}/Images/{imageType}")] + [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + public async Task GetItemImage( + [FromRoute] Guid itemId, + [FromRoute] ImageType imageType, + [FromRoute] int? maxWidth, + [FromRoute] int? maxHeight, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] string tag, + [FromQuery] bool? cropWhitespace, + [FromQuery] string format, + [FromQuery] bool addPlayedIndicator, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? blur, + [FromQuery] string backgroundColor, + [FromQuery] string foregroundLayer, + [FromRoute] int? imageIndex = null, + [FromQuery] bool enableImageEnhancers = true) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + itemId, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + enableImageEnhancers, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Gets the item's image. + /// + /// Item id. + /// Image type. + /// Image index. + /// 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. + /// 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. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Enable or disable image enhancers such as cover art. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] + [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] + public ActionResult GetItemImage( + [FromRoute] Guid itemId, + [FromRoute] ImageType imageType, + [FromRoute] int? imageIndex, + [FromRoute] string tag, + [FromRoute] string format, + [FromRoute] int? maxWidth, + [FromRoute] int? maxHeight, + [FromRoute] double? percentPlayed, + [FromRoute] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string backgroundColor, + [FromQuery] string foregroundLayer, + [FromQuery] bool enableImageEnhancers = true) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + return GetImageInternal( + itemId, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + enableImageEnhancers, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)); + } + private static async Task GetMemoryStream(Stream inputStream) { using var reader = new StreamReader(inputStream); var text = await reader.ReadToEndAsync().ConfigureAwait(false); var bytes = Convert.FromBase64String(text); - return new MemoryStream(bytes) - { - Position = 0 - }; + return new MemoryStream(bytes) {Position = 0}; } private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) @@ -365,5 +524,239 @@ namespace Jellyfin.Api.Controllers return null; } } + + private async Task GetImageInternal( + Guid itemId, + ImageType imageType, + int? imageIndex, + string tag, + string format, + int? maxWidth, + int? maxHeight, + double? percentPlayed, + int? unplayedCount, + int? width, + int? height, + int? quality, + bool? cropWhitespace, + bool addPlayedIndicator, + int? blur, + string backgroundColor, + string foregroundLayer, + bool enableImageEnhancers, + BaseItem item, + bool isHeadRequest) + { + if (percentPlayed.HasValue) + { + if (percentPlayed.Value <= 0) + { + percentPlayed = null; + } + else if (percentPlayed.Value >= 100) + { + percentPlayed = null; + addPlayedIndicator = true; + } + } + + if (percentPlayed.HasValue) + { + unplayedCount = null; + } + + if (unplayedCount.HasValue + && unplayedCount.Value <= 0) + { + unplayedCount = null; + } + + var imageInfo = item.GetImageInfo(imageType, imageIndex ?? 0); + if (imageInfo == null) + { + return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item.Name, imageType)); + } + + if (!cropWhitespace.HasValue) + { + cropWhitespace = imageType == ImageType.Logo || imageType == ImageType.Art; + } + + var outputFormats = GetOutputFormats(format); + + TimeSpan? cacheDuration = null; + + if (!string.IsNullOrEmpty(tag)) + { + cacheDuration = TimeSpan.FromDays(365); + } + + var responseHeaders = new Dictionary {{"transferMode.dlna.org", "Interactive"}, {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}}; + + return await GetImageResult( + item, + itemId, + imageIndex, + height, + maxHeight, + maxWidth, + quality, + width, + addPlayedIndicator, + percentPlayed, + unplayedCount, + blur, + backgroundColor, + foregroundLayer, + imageInfo, + cropWhitespace.Value, + outputFormats, + cacheDuration, + responseHeaders, + isHeadRequest).ConfigureAwait(false); + } + + private ImageFormat[] GetOutputFormats(string format) + { + if (!string.IsNullOrWhiteSpace(format) + && Enum.TryParse(format, true, out ImageFormat parsedFormat)) + { + return new[] {parsedFormat}; + } + + return GetClientSupportedFormats(); + } + + private ImageFormat[] GetClientSupportedFormats() + { + var acceptTypes = Request.Headers[HeaderNames.Accept]; + var supportedFormats = new List(); + if (acceptTypes.Count > 0) + { + foreach (var type in acceptTypes) + { + int index = type.IndexOf(';', StringComparison.Ordinal); + if (index != -1) + { + supportedFormats.Add(type.Substring(0, index)); + } + } + } + + var acceptParam = Request.Query[HeaderNames.Accept]; + + var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false); + + if (!supportsWebP) + { + var userAgent = Request.Headers[HeaderNames.UserAgent].ToString(); + if (userAgent.IndexOf("crosswalk", StringComparison.OrdinalIgnoreCase) != -1 && + userAgent.IndexOf("android", StringComparison.OrdinalIgnoreCase) != -1) + { + supportsWebP = true; + } + } + + var formats = new List(4); + + if (supportsWebP) + { + formats.Add(ImageFormat.Webp); + } + + formats.Add(ImageFormat.Jpg); + formats.Add(ImageFormat.Png); + + if (SupportsFormat(supportedFormats, acceptParam, "gif", true)) + { + formats.Add(ImageFormat.Gif); + } + + return formats.ToArray(); + } + + private bool SupportsFormat(IReadOnlyCollection requestAcceptTypes, string acceptParam, string format, bool acceptAll) + { + var mimeType = "image/" + format; + + if (requestAcceptTypes.Contains(mimeType)) + { + return true; + } + + if (acceptAll && requestAcceptTypes.Contains("*/*")) + { + return true; + } + + return string.Equals(acceptParam, format, StringComparison.OrdinalIgnoreCase); + } + + private async Task GetImageResult( + BaseItem item, + Guid itemId, + int? index, + int? height, + int? maxHeight, + int? maxWidth, + int? quality, + int? width, + bool addPlayedIndicator, + double? percentPlayed, + int? unplayedCount, + int? blur, + string backgroundColor, + string foregroundLayer, + ItemImageInfo imageInfo, + bool cropWhitespace, + IReadOnlyCollection supportedFormats, + TimeSpan? cacheDuration, + IDictionary headers, + bool isHeadRequest) + { + if (!imageInfo.IsLocalFile) + { + imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, index ?? 0).ConfigureAwait(false); + } + + var options = new ImageProcessingOptions + { + CropWhiteSpace = cropWhitespace, + Height = height, + ImageIndex = index ?? 0, + Image = imageInfo, + Item = item, + ItemId = itemId, + MaxHeight = maxHeight, + MaxWidth = maxWidth, + Quality = quality ?? 100, + Width = width, + AddPlayedIndicator = addPlayedIndicator, + PercentPlayed = percentPlayed ?? 0, + UnplayedCount = unplayedCount, + Blur = blur, + BackgroundColor = backgroundColor, + ForegroundLayer = foregroundLayer, + SupportedOutputFormats = supportedFormats + }; + + var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false); + + headers[HeaderNames.Vary] = HeaderNames.Accept; + /* + // TODO + return _resultFactory.GetStaticFileResult(Request, new StaticFileResultOptions + { + CacheDuration = cacheDuration, + ResponseHeaders = headers, + ContentType = imageResult.Item2, + DateLastModified = imageResult.Item3, + IsHeadRequest = isHeadRequest, + Path = imageResult.Item1, + FileShare = FileShare.Read + }); + */ + return NoContent(); + } } } -- cgit v1.2.3 From 230c54721db3c4f9110a8db778265a788644cabc Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Jul 2020 08:05:21 -0600 Subject: update post profile image --- Jellyfin.Api/Controllers/ImageController.cs | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index c24c5e24c..1322d77e9 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -100,6 +100,11 @@ namespace Jellyfin.Api.Controllers // Handle image/png; charset=utf-8 var mimeType = Request.ContentType.Split(';').FirstOrDefault(); var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); + if (user.ProfileImage != null) + { + _userManager.ClearProfileImage(user); + } + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); await _providerManager -- cgit v1.2.3 From 6602b0dfb6e05dadd73dd2841c579d5e9f87be59 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Jul 2020 13:17:08 -0600 Subject: Move ImageService.cs to Jellyfin.Api --- Jellyfin.Api/Controllers/ImageController.cs | 694 ++++++++++++++++++++++++---- MediaBrowser.Api/Images/ImageRequest.cs | 100 ---- MediaBrowser.Api/Images/ImageService.cs | 573 ----------------------- MediaBrowser.Api/MediaBrowser.Api.csproj | 1 + 4 files changed, 612 insertions(+), 756 deletions(-) delete mode 100644 MediaBrowser.Api/Images/ImageRequest.cs delete mode 100644 MediaBrowser.Api/Images/ImageService.cs (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 1322d77e9..f89601d17 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -330,7 +330,6 @@ namespace Jellyfin.Api.Controllers /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. /// Image index. - /// Enable or disable image enhancers such as cover art. /// Image stream returned. /// Item not found. /// @@ -341,6 +340,8 @@ namespace Jellyfin.Api.Controllers [HttpHead("/Items/{itemId}/Images/{imageType}")] [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetItemImage( [FromRoute] Guid itemId, [FromRoute] ImageType imageType, @@ -349,17 +350,16 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] string tag, + [FromQuery] string? tag, [FromQuery] bool? cropWhitespace, - [FromQuery] string format, - [FromQuery] bool addPlayedIndicator, + [FromQuery] string? format, + [FromQuery] bool? addPlayedIndicator, [FromQuery] double? percentPlayed, [FromQuery] int? unplayedCount, [FromQuery] int? blur, - [FromQuery] string backgroundColor, - [FromQuery] string foregroundLayer, - [FromRoute] int? imageIndex = null, - [FromQuery] bool enableImageEnhancers = true) + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromRoute] int? imageIndex = null) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -385,7 +385,6 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - enableImageEnhancers, item, Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) .ConfigureAwait(false); @@ -396,7 +395,84 @@ namespace Jellyfin.Api.Controllers /// /// Item id. /// Image type. + /// The maximum image width to return. + /// The maximum image height to return. + /// 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. + /// 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. Add a played indicator. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// 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("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] + [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetItemImage2( + [FromRoute] Guid itemId, + [FromRoute] ImageType imageType, + [FromRoute] int? maxWidth, + [FromRoute] int? maxHeight, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromRoute] string tag, + [FromQuery] bool? cropWhitespace, + [FromRoute] string format, + [FromQuery] bool? addPlayedIndicator, + [FromRoute] double? percentPlayed, + [FromRoute] int? unplayedCount, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromRoute] int? imageIndex = null) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + itemId, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get artist image by name. + /// + /// Artist name. + /// 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. @@ -411,19 +487,20 @@ namespace Jellyfin.Api.Controllers /// Optional. Blur image. /// Optional. Apply a background color for transparent images. /// Optional. Apply a foreground layer on top of the image. - /// Enable or disable image enhancers such as cover art. + /// Image index. /// Image stream returned. /// Item not found. /// /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] - [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] - public ActionResult GetItemImage( - [FromRoute] Guid itemId, + [HttpGet("/Artists/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetArtistImage( + [FromRoute] string name, [FromRoute] ImageType imageType, - [FromRoute] int? imageIndex, [FromRoute] string tag, [FromRoute] string format, [FromRoute] int? maxWidth, @@ -434,39 +511,447 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? height, [FromQuery] int? quality, [FromQuery] bool? cropWhitespace, - [FromQuery] bool addPlayedIndicator, + [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, - [FromQuery] string backgroundColor, - [FromQuery] string foregroundLayer, - [FromQuery] bool enableImageEnhancers = true) + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromRoute] int? imageIndex = null) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetArtist(name); if (item == null) { return NotFound(); } - return GetImageInternal( - itemId, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - cropWhitespace, - addPlayedIndicator, - blur, - backgroundColor, - foregroundLayer, - enableImageEnhancers, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)); + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get genre image by name. + /// + /// Genre name. + /// 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. + /// 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. Add a played indicator. + /// 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("/Genres/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetGenreImage( + [FromRoute] string name, + [FromRoute] ImageType imageType, + [FromRoute] string tag, + [FromRoute] string format, + [FromRoute] int? maxWidth, + [FromRoute] int? maxHeight, + [FromRoute] double? percentPlayed, + [FromRoute] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromRoute] int? imageIndex = null) + { + var item = _libraryManager.GetGenre(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get music genre image by name. + /// + /// Music genre name. + /// 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. + /// 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. Add a played indicator. + /// 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("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetMusicGenreImage( + [FromRoute] string name, + [FromRoute] ImageType imageType, + [FromRoute] string tag, + [FromRoute] string format, + [FromRoute] int? maxWidth, + [FromRoute] int? maxHeight, + [FromRoute] double? percentPlayed, + [FromRoute] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromRoute] int? imageIndex = null) + { + var item = _libraryManager.GetMusicGenre(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get person image by name. + /// + /// Person name. + /// 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. + /// 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. Add a played indicator. + /// 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("/Persons/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetPersonImage( + [FromRoute] string name, + [FromRoute] ImageType imageType, + [FromRoute] string tag, + [FromRoute] string format, + [FromRoute] int? maxWidth, + [FromRoute] int? maxHeight, + [FromRoute] double? percentPlayed, + [FromRoute] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromRoute] int? imageIndex = null) + { + var item = _libraryManager.GetPerson(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get studio image by name. + /// + /// Studio name. + /// 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. + /// 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. Add a played indicator. + /// 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("/Studios/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetStudioImage( + [FromRoute] string name, + [FromRoute] ImageType imageType, + [FromRoute] string tag, + [FromRoute] string format, + [FromRoute] int? maxWidth, + [FromRoute] int? maxHeight, + [FromRoute] double? percentPlayed, + [FromRoute] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromRoute] int? imageIndex = null) + { + var item = _libraryManager.GetStudio(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .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. + /// 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. Add a played indicator. + /// 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}/{imageIndex?}")] + [HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetUserImage( + [FromRoute] Guid userId, + [FromRoute] ImageType imageType, + [FromQuery] string? tag, + [FromQuery] string? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromRoute] int? imageIndex = null) + { + var user = _userManager.GetUserById(userId); + if (user == 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, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + null, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase), + info) + .ConfigureAwait(false); } private static async Task GetMemoryStream(Stream inputStream) @@ -475,7 +960,7 @@ namespace Jellyfin.Api.Controllers var text = await reader.ReadToEndAsync().ConfigureAwait(false); var bytes = Convert.FromBase64String(text); - return new MemoryStream(bytes) {Position = 0}; + return new MemoryStream(bytes) { Position = 0 }; } private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) @@ -525,7 +1010,6 @@ namespace Jellyfin.Api.Controllers catch (Exception ex) { _logger.LogError(ex, "Error getting image information for {Path}", info.Path); - return null; } } @@ -534,8 +1018,8 @@ namespace Jellyfin.Api.Controllers Guid itemId, ImageType imageType, int? imageIndex, - string tag, - string format, + string? tag, + string? format, int? maxWidth, int? maxHeight, double? percentPlayed, @@ -544,13 +1028,13 @@ namespace Jellyfin.Api.Controllers int? height, int? quality, bool? cropWhitespace, - bool addPlayedIndicator, + bool? addPlayedIndicator, int? blur, - string backgroundColor, - string foregroundLayer, - bool enableImageEnhancers, - BaseItem item, - bool isHeadRequest) + string? backgroundColor, + string? foregroundLayer, + BaseItem? item, + bool isHeadRequest, + ItemImageInfo? imageInfo = null) { if (percentPlayed.HasValue) { @@ -576,16 +1060,16 @@ namespace Jellyfin.Api.Controllers unplayedCount = null; } - var imageInfo = item.GetImageInfo(imageType, imageIndex ?? 0); if (imageInfo == null) { - return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item.Name, imageType)); + imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0); + if (imageInfo == null) + { + return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item?.Name, imageType)); + } } - if (!cropWhitespace.HasValue) - { - cropWhitespace = imageType == ImageType.Logo || imageType == ImageType.Art; - } + cropWhitespace ??= imageType == ImageType.Logo || imageType == ImageType.Art; var outputFormats = GetOutputFormats(format); @@ -596,7 +1080,11 @@ namespace Jellyfin.Api.Controllers cacheDuration = TimeSpan.FromDays(365); } - var responseHeaders = new Dictionary {{"transferMode.dlna.org", "Interactive"}, {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}}; + var responseHeaders = new Dictionary + { + { "transferMode.dlna.org", "Interactive" }, + { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" } + }; return await GetImageResult( item, @@ -621,12 +1109,12 @@ namespace Jellyfin.Api.Controllers isHeadRequest).ConfigureAwait(false); } - private ImageFormat[] GetOutputFormats(string format) + private ImageFormat[] GetOutputFormats(string? format) { if (!string.IsNullOrWhiteSpace(format) && Enum.TryParse(format, true, out ImageFormat parsedFormat)) { - return new[] {parsedFormat}; + return new[] { parsedFormat }; } return GetClientSupportedFormats(); @@ -698,7 +1186,7 @@ namespace Jellyfin.Api.Controllers } private async Task GetImageResult( - BaseItem item, + BaseItem? item, Guid itemId, int? index, int? height, @@ -706,12 +1194,12 @@ namespace Jellyfin.Api.Controllers int? maxWidth, int? quality, int? width, - bool addPlayedIndicator, + bool? addPlayedIndicator, double? percentPlayed, int? unplayedCount, int? blur, - string backgroundColor, - string foregroundLayer, + string? backgroundColor, + string? foregroundLayer, ItemImageInfo imageInfo, bool cropWhitespace, IReadOnlyCollection supportedFormats, @@ -719,7 +1207,7 @@ namespace Jellyfin.Api.Controllers IDictionary headers, bool isHeadRequest) { - if (!imageInfo.IsLocalFile) + if (!imageInfo.IsLocalFile && item != null) { imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, index ?? 0).ConfigureAwait(false); } @@ -736,7 +1224,7 @@ namespace Jellyfin.Api.Controllers MaxWidth = maxWidth, Quality = quality ?? 100, Width = width, - AddPlayedIndicator = addPlayedIndicator, + AddPlayedIndicator = addPlayedIndicator ?? false, PercentPlayed = percentPlayed ?? 0, UnplayedCount = unplayedCount, Blur = blur, @@ -745,23 +1233,63 @@ namespace Jellyfin.Api.Controllers SupportedOutputFormats = supportedFormats }; - var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false); - - headers[HeaderNames.Vary] = HeaderNames.Accept; - /* - // TODO - return _resultFactory.GetStaticFileResult(Request, new StaticFileResultOptions - { - CacheDuration = cacheDuration, - ResponseHeaders = headers, - ContentType = imageResult.Item2, - DateLastModified = imageResult.Item3, - IsHeadRequest = isHeadRequest, - Path = imageResult.Item1, - FileShare = FileShare.Read - }); - */ - return NoContent(); + var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(options).ConfigureAwait(false); + + var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache"); + var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader); + + // if the parsing of the IfModifiedSince header was not successful, disable caching + if (!parsingSuccessful) + { + // disableCaching = true; + } + + foreach (var (key, value) in headers) + { + Response.Headers.Add(key, value); + } + + Response.ContentType = imageContentType; + Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); + Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept); + + if (disableCaching) + { + Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); + Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate"); + } + else + { + if (cacheDuration.HasValue) + { + Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds); + } + else + { + Response.Headers.Add(HeaderNames.CacheControl, "public"); + } + + Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false))); + + // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified + if (!(dateImageModified > ifModifiedSinceHeader)) + { + if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow) + { + Response.StatusCode = StatusCodes.Status304NotModified; + return new ContentResult(); + } + } + } + + // if the request is a head request, return a NoContent result with the same headers as it would with a GET request + if (isHeadRequest) + { + return NoContent(); + } + + var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read); + return File(stream, imageContentType); } } } diff --git a/MediaBrowser.Api/Images/ImageRequest.cs b/MediaBrowser.Api/Images/ImageRequest.cs deleted file mode 100644 index 0f3455548..000000000 --- a/MediaBrowser.Api/Images/ImageRequest.cs +++ /dev/null @@ -1,100 +0,0 @@ -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Api.Images -{ - /// - /// Class ImageRequest. - /// - public class ImageRequest : DeleteImageRequest - { - /// - /// The max width. - /// - [ApiMember(Name = "MaxWidth", Description = "The maximum image width to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? MaxWidth { get; set; } - - /// - /// The max height. - /// - [ApiMember(Name = "MaxHeight", Description = "The maximum image height to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? MaxHeight { get; set; } - - /// - /// The width. - /// - [ApiMember(Name = "Width", Description = "The fixed image width to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? Width { get; set; } - - /// - /// The height. - /// - [ApiMember(Name = "Height", Description = "The fixed image height to return.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? Height { get; set; } - - /// - /// Gets or sets the quality. - /// - /// The quality. - [ApiMember(Name = "Quality", Description = "Optional quality setting, from 0-100. Defaults to 90 and should suffice in most cases.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? Quality { get; set; } - - /// - /// Gets or sets the tag. - /// - /// The tag. - [ApiMember(Name = "Tag", Description = "Optional. Supply the cache tag from the item object to receive strong caching headers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Tag { get; set; } - - [ApiMember(Name = "CropWhitespace", Description = "Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? CropWhitespace { get; set; } - - [ApiMember(Name = "EnableImageEnhancers", Description = "Enable or disable image enhancers such as cover art.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool EnableImageEnhancers { get; set; } - - [ApiMember(Name = "Format", Description = "Determines the output foramt of the image - original,gif,jpg,png", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public string Format { get; set; } - - [ApiMember(Name = "AddPlayedIndicator", Description = "Optional. Add a played indicator", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool AddPlayedIndicator { get; set; } - - [ApiMember(Name = "PercentPlayed", Description = "Optional percent to render for the percent played overlay", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public double? PercentPlayed { get; set; } - - [ApiMember(Name = "UnplayedCount", Description = "Optional unplayed count overlay to render", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? UnplayedCount { get; set; } - - public int? Blur { get; set; } - - [ApiMember(Name = "BackgroundColor", Description = "Optional. Apply a background color for transparent images.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string BackgroundColor { get; set; } - - [ApiMember(Name = "ForegroundLayer", Description = "Optional. Apply a foreground layer on top of the image.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ForegroundLayer { get; set; } - - public ImageRequest() - { - EnableImageEnhancers = true; - } - } - - /// - /// Class DeleteImageRequest. - /// - public class DeleteImageRequest - { - /// - /// Gets or sets the type of the image. - /// - /// The type of the image. - [ApiMember(Name = "Type", Description = "Image Type", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET,POST,DELETE")] - public ImageType Type { get; set; } - - /// - /// Gets or sets the index. - /// - /// The index. - [ApiMember(Name = "Index", Description = "Image Index", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET,POST,DELETE")] - public int? Index { get; set; } - } -} diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs deleted file mode 100644 index 55859d9f1..000000000 --- a/MediaBrowser.Api/Images/ImageService.cs +++ /dev/null @@ -1,573 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; -using User = Jellyfin.Data.Entities.User; - -namespace MediaBrowser.Api.Images -{ - [Route("/Items/{Id}/Images/{Type}", "GET")] - [Route("/Items/{Id}/Images/{Type}/{Index}", "GET")] - [Route("/Items/{Id}/Images/{Type}", "HEAD")] - [Route("/Items/{Id}/Images/{Type}/{Index}", "HEAD")] - [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}/{PercentPlayed}/{UnplayedCount}", "GET")] - [Route("/Items/{Id}/Images/{Type}/{Index}/{Tag}/{Format}/{MaxWidth}/{MaxHeight}/{PercentPlayed}/{UnplayedCount}", "HEAD")] - public class GetItemImage : ImageRequest - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path")] - public Guid Id { get; set; } - } - - /// - /// Class GetPersonImage. - /// - [Route("/Artists/{Name}/Images/{Type}", "GET")] - [Route("/Artists/{Name}/Images/{Type}/{Index}", "GET")] - [Route("/Genres/{Name}/Images/{Type}", "GET")] - [Route("/Genres/{Name}/Images/{Type}/{Index}", "GET")] - [Route("/MusicGenres/{Name}/Images/{Type}", "GET")] - [Route("/MusicGenres/{Name}/Images/{Type}/{Index}", "GET")] - [Route("/Persons/{Name}/Images/{Type}", "GET")] - [Route("/Persons/{Name}/Images/{Type}/{Index}", "GET")] - [Route("/Studios/{Name}/Images/{Type}", "GET")] - [Route("/Studios/{Name}/Images/{Type}/{Index}", "GET")] - ////[Route("/Years/{Year}/Images/{Type}", "GET")] - ////[Route("/Years/{Year}/Images/{Type}/{Index}", "GET")] - [Route("/Artists/{Name}/Images/{Type}", "HEAD")] - [Route("/Artists/{Name}/Images/{Type}/{Index}", "HEAD")] - [Route("/Genres/{Name}/Images/{Type}", "HEAD")] - [Route("/Genres/{Name}/Images/{Type}/{Index}", "HEAD")] - [Route("/MusicGenres/{Name}/Images/{Type}", "HEAD")] - [Route("/MusicGenres/{Name}/Images/{Type}/{Index}", "HEAD")] - [Route("/Persons/{Name}/Images/{Type}", "HEAD")] - [Route("/Persons/{Name}/Images/{Type}/{Index}", "HEAD")] - [Route("/Studios/{Name}/Images/{Type}", "HEAD")] - [Route("/Studios/{Name}/Images/{Type}/{Index}", "HEAD")] - ////[Route("/Years/{Year}/Images/{Type}", "HEAD")] - ////[Route("/Years/{Year}/Images/{Type}/{Index}", "HEAD")] - public class GetItemByNameImage : ImageRequest - { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "Name", Description = "Item name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Name { get; set; } - } - - /// - /// Class GetUserImage. - /// - [Route("/Users/{Id}/Images/{Type}", "GET")] - [Route("/Users/{Id}/Images/{Type}/{Index}", "GET")] - [Route("/Users/{Id}/Images/{Type}", "HEAD")] - [Route("/Users/{Id}/Images/{Type}/{Index}", "HEAD")] - public class GetUserImage : ImageRequest - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } - } - - /// - /// Class ImageService. - /// - public class ImageService : BaseApiService - { - private readonly IUserManager _userManager; - - private readonly ILibraryManager _libraryManager; - - private readonly IProviderManager _providerManager; - - private readonly IImageProcessor _imageProcessor; - private readonly IFileSystem _fileSystem; - private readonly IAuthorizationContext _authContext; - - /// - /// Initializes a new instance of the class. - /// - public ImageService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IUserManager userManager, - ILibraryManager libraryManager, - IProviderManager providerManager, - IImageProcessor imageProcessor, - IFileSystem fileSystem, - IAuthorizationContext authContext) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _userManager = userManager; - _libraryManager = libraryManager; - _providerManager = providerManager; - _imageProcessor = imageProcessor; - _fileSystem = fileSystem; - _authContext = authContext; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetItemImage request) - { - return GetImage(request, request.Id, null, false); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Head(GetItemImage request) - { - return GetImage(request, request.Id, null, true); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetUserImage request) - { - var item = _userManager.GetUserById(request.Id); - - return GetImage(request, item, false); - } - - public object Head(GetUserImage request) - { - var item = _userManager.GetUserById(request.Id); - - return GetImage(request, item, true); - } - - public object Get(GetItemByNameImage request) - { - var type = GetPathValue(0).ToString(); - - var item = GetItemByName(request.Name, type, _libraryManager, new DtoOptions(false)); - - return GetImage(request, item.Id, item, false); - } - - public object Head(GetItemByNameImage request) - { - var type = GetPathValue(0).ToString(); - - var item = GetItemByName(request.Name, type, _libraryManager, new DtoOptions(false)); - - return GetImage(request, item.Id, item, true); - } - - /// - /// Gets the image. - /// - /// The request. - /// The item. - /// if set to true [is head request]. - /// System.Object. - /// - public Task GetImage(ImageRequest request, Guid itemId, BaseItem item, bool isHeadRequest) - { - if (request.PercentPlayed.HasValue) - { - if (request.PercentPlayed.Value <= 0) - { - request.PercentPlayed = null; - } - else if (request.PercentPlayed.Value >= 100) - { - request.PercentPlayed = null; - request.AddPlayedIndicator = true; - } - } - - if (request.PercentPlayed.HasValue) - { - request.UnplayedCount = null; - } - - if (request.UnplayedCount.HasValue - && request.UnplayedCount.Value <= 0) - { - request.UnplayedCount = null; - } - - if (item == null) - { - item = _libraryManager.GetItemById(itemId); - - if (item == null) - { - throw new ResourceNotFoundException(string.Format("Item {0} not found.", itemId.ToString("N", CultureInfo.InvariantCulture))); - } - } - - var imageInfo = GetImageInfo(request, item); - if (imageInfo == null) - { - throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", item.Name, request.Type)); - } - - bool cropWhitespace; - if (request.CropWhitespace.HasValue) - { - cropWhitespace = request.CropWhitespace.Value; - } - else - { - cropWhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art; - } - - var outputFormats = GetOutputFormats(request); - - TimeSpan? cacheDuration = null; - - if (!string.IsNullOrEmpty(request.Tag)) - { - cacheDuration = TimeSpan.FromDays(365); - } - - var responseHeaders = new Dictionary - { - {"transferMode.dlna.org", "Interactive"}, - {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"} - }; - - return GetImageResult( - item, - itemId, - request, - imageInfo, - cropWhitespace, - outputFormats, - cacheDuration, - responseHeaders, - isHeadRequest); - } - - public Task GetImage(ImageRequest request, User user, bool isHeadRequest) - { - var imageInfo = GetImageInfo(request, user); - - TimeSpan? cacheDuration = null; - - if (!string.IsNullOrEmpty(request.Tag)) - { - cacheDuration = TimeSpan.FromDays(365); - } - - var responseHeaders = new Dictionary - { - {"transferMode.dlna.org", "Interactive"}, - {"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"} - }; - - var outputFormats = GetOutputFormats(request); - - return GetImageResult(user.Id, - request, - imageInfo, - outputFormats, - cacheDuration, - responseHeaders, - isHeadRequest); - } - - private async Task GetImageResult( - Guid itemId, - ImageRequest request, - ItemImageInfo info, - IReadOnlyCollection supportedFormats, - TimeSpan? cacheDuration, - IDictionary headers, - bool isHeadRequest) - { - info.Type = ImageType.Profile; - var options = new ImageProcessingOptions - { - CropWhiteSpace = true, - Height = request.Height, - ImageIndex = request.Index ?? 0, - Image = info, - Item = null, // Hack alert - ItemId = itemId, - MaxHeight = request.MaxHeight, - MaxWidth = request.MaxWidth, - Quality = request.Quality ?? 100, - Width = request.Width, - AddPlayedIndicator = request.AddPlayedIndicator, - PercentPlayed = 0, - UnplayedCount = request.UnplayedCount, - Blur = request.Blur, - BackgroundColor = request.BackgroundColor, - ForegroundLayer = request.ForegroundLayer, - SupportedOutputFormats = supportedFormats - }; - - var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false); - - headers[HeaderNames.Vary] = HeaderNames.Accept; - - return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions - { - CacheDuration = cacheDuration, - ResponseHeaders = headers, - ContentType = imageResult.Item2, - DateLastModified = imageResult.Item3, - IsHeadRequest = isHeadRequest, - Path = imageResult.Item1, - - FileShare = FileShare.Read - - }).ConfigureAwait(false); - } - - private async Task GetImageResult( - BaseItem item, - Guid itemId, - ImageRequest request, - ItemImageInfo image, - bool cropwhitespace, - IReadOnlyCollection supportedFormats, - TimeSpan? cacheDuration, - IDictionary headers, - bool isHeadRequest) - { - if (!image.IsLocalFile) - { - item ??= _libraryManager.GetItemById(itemId); - image = await _libraryManager.ConvertImageToLocal(item, image, request.Index ?? 0).ConfigureAwait(false); - } - - var options = new ImageProcessingOptions - { - CropWhiteSpace = cropwhitespace, - Height = request.Height, - ImageIndex = request.Index ?? 0, - Image = image, - Item = item, - ItemId = itemId, - MaxHeight = request.MaxHeight, - MaxWidth = request.MaxWidth, - Quality = request.Quality ?? 100, - Width = request.Width, - AddPlayedIndicator = request.AddPlayedIndicator, - PercentPlayed = request.PercentPlayed ?? 0, - UnplayedCount = request.UnplayedCount, - Blur = request.Blur, - BackgroundColor = request.BackgroundColor, - ForegroundLayer = request.ForegroundLayer, - SupportedOutputFormats = supportedFormats - }; - - var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false); - - headers[HeaderNames.Vary] = HeaderNames.Accept; - - return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions - { - CacheDuration = cacheDuration, - ResponseHeaders = headers, - ContentType = imageResult.Item2, - DateLastModified = imageResult.Item3, - IsHeadRequest = isHeadRequest, - Path = imageResult.Item1, - - FileShare = FileShare.Read - }).ConfigureAwait(false); - } - - private ImageFormat[] GetOutputFormats(ImageRequest request) - { - if (!string.IsNullOrWhiteSpace(request.Format) - && Enum.TryParse(request.Format, true, out ImageFormat format)) - { - return new[] { format }; - } - - return GetClientSupportedFormats(); - } - - private ImageFormat[] GetClientSupportedFormats() - { - var supportedFormats = Request.AcceptTypes ?? Array.Empty(); - if (supportedFormats.Length > 0) - { - for (int i = 0; i < supportedFormats.Length; i++) - { - int index = supportedFormats[i].IndexOf(';'); - if (index != -1) - { - supportedFormats[i] = supportedFormats[i].Substring(0, index); - } - } - } - - var acceptParam = Request.QueryString["accept"]; - - var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false); - - if (!supportsWebP) - { - var userAgent = Request.UserAgent ?? string.Empty; - if (userAgent.IndexOf("crosswalk", StringComparison.OrdinalIgnoreCase) != -1 && - userAgent.IndexOf("android", StringComparison.OrdinalIgnoreCase) != -1) - { - supportsWebP = true; - } - } - - var formats = new List(4); - - if (supportsWebP) - { - formats.Add(ImageFormat.Webp); - } - - formats.Add(ImageFormat.Jpg); - formats.Add(ImageFormat.Png); - - if (SupportsFormat(supportedFormats, acceptParam, "gif", true)) - { - formats.Add(ImageFormat.Gif); - } - - return formats.ToArray(); - } - - private bool SupportsFormat(IEnumerable requestAcceptTypes, string acceptParam, string format, bool acceptAll) - { - var mimeType = "image/" + format; - - if (requestAcceptTypes.Contains(mimeType)) - { - return true; - } - - if (acceptAll && requestAcceptTypes.Contains("*/*")) - { - return true; - } - - return string.Equals(Request.QueryString["accept"], format, StringComparison.OrdinalIgnoreCase); - } - - /// - /// Gets the image path. - /// - /// The request. - /// The item. - /// System.String. - private static ItemImageInfo GetImageInfo(ImageRequest request, BaseItem item) - { - var index = request.Index ?? 0; - - return item.GetImageInfo(request.Type, index); - } - - private static ItemImageInfo GetImageInfo(ImageRequest request, User user) - { - var info = new ItemImageInfo - { - Path = user.ProfileImage.Path, - Type = ImageType.Primary, - DateModified = user.ProfileImage.LastModified, - }; - - if (request.Width.HasValue) - { - info.Width = request.Width.Value; - } - - if (request.Height.HasValue) - { - info.Height = request.Height.Value; - } - - return info; - } - - /// - /// Posts the image. - /// - /// The entity. - /// The input stream. - /// Type of the image. - /// Type of the MIME. - /// Task. - public async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType) - { - var memoryStream = await GetMemoryStream(inputStream); - - // Handle image/png; charset=utf-8 - mimeType = mimeType.Split(';').FirstOrDefault(); - - await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); - - entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); - } - - private static async Task GetMemoryStream(Stream inputStream) - { - using var reader = new StreamReader(inputStream); - var text = await reader.ReadToEndAsync().ConfigureAwait(false); - - var bytes = Convert.FromBase64String(text); - return new MemoryStream(bytes) - { - Position = 0 - }; - } - - private async Task PostImage(User user, Stream inputStream, string mimeType) - { - var memoryStream = await GetMemoryStream(inputStream); - - // Handle image/png; charset=utf-8 - mimeType = mimeType.Split(';').FirstOrDefault(); - var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); - if (user.ProfileImage != null) - { - _userManager.ClearProfileImage(user); - } - - user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); - - await _providerManager - .SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path) - .ConfigureAwait(false); - await _userManager.UpdateUserAsync(user); - } - } -} diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index d703bdb05..3f75a3b29 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -12,6 +12,7 @@ + -- cgit v1.2.3 From b006fd1b8f7a0c92e76a031ead630423ff393cc4 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 22 Jul 2020 08:03:45 -0600 Subject: apply review suggestions --- Jellyfin.Api/Controllers/ImageController.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index f89601d17..18220c5f3 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -79,9 +79,12 @@ namespace Jellyfin.Api.Controllers /// (Unused) Image type. /// (Unused) Image index. /// Image updated. + /// User does not have permission to delete the image. /// A . [HttpPost("/Users/{userId}/Images/{imageType}")] [HttpPost("/Users/{userId}/Images/{imageType}/{index?}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [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( @@ -122,12 +125,14 @@ namespace Jellyfin.Api.Controllers /// (Unused) Image type. /// (Unused) Image index. /// Image deleted. + /// User does not have permission to delete the image. /// A . [HttpDelete("/Users/{userId}/Images/{itemType}")] [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}")] [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 ActionResult DeleteUserImage( [FromRoute] Guid userId, [FromRoute] ImageType imageType, @@ -188,7 +193,7 @@ namespace Jellyfin.Api.Controllers /// Image type. /// (Unused) Image index. /// Image saved. - /// Item not found. + /// Item not found. /// A on success, or a if item not found. [HttpPost("/Items/{itemId}/Images/{imageType}")] [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] -- cgit v1.2.3 From 9e00aa3014c0044c0918a775c3394763666b30af Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 3 Aug 2020 14:38:51 -0600 Subject: fix openapi validation errors --- Jellyfin.Api/Controllers/AudioController.cs | 8 ++-- Jellyfin.Api/Controllers/BrandingController.cs | 2 +- Jellyfin.Api/Controllers/DlnaServerController.cs | 12 +++--- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 +- Jellyfin.Api/Controllers/HlsSegmentController.cs | 4 +- Jellyfin.Api/Controllers/ImageController.cs | 28 +++++++------- Jellyfin.Api/Controllers/ItemsController.cs | 2 +- Jellyfin.Api/Controllers/LibraryController.cs | 14 +++---- Jellyfin.Api/Controllers/LiveTvController.cs | 6 +-- Jellyfin.Api/Controllers/SessionController.cs | 2 +- Jellyfin.Api/Controllers/StartupController.cs | 4 +- Jellyfin.Api/Controllers/SubtitleController.cs | 2 +- Jellyfin.Api/Controllers/SyncPlayController.cs | 18 ++++----- Jellyfin.Api/Controllers/SystemController.cs | 4 +- .../Controllers/UniversalAudioController.cs | 6 +-- Jellyfin.Api/Controllers/VideosController.cs | 6 +-- .../Extensions/ApiServiceCollectionExtensions.cs | 11 +++++- tests/Jellyfin.Api.Tests/GetPathValueTests.cs | 45 ---------------------- 18 files changed, 70 insertions(+), 108 deletions(-) delete mode 100644 tests/Jellyfin.Api.Tests/GetPathValueTests.cs (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index ebae1caa0..4de87616c 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -144,10 +144,10 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Audio stream returned. /// A containing the audio file. - [HttpGet("{itemId}/{stream=stream}.{container?}")] - [HttpGet("{itemId}/stream")] - [HttpHead("{itemId}/{stream=stream}.{container?}")] - [HttpHead("{itemId}/stream")] + [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetAudioStreamByContainer")] + [HttpGet("{itemId}/stream", Name = "GetAudioStream")] + [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadAudioStreamByContainer")] + [HttpHead("{itemId}/stream", Name = "HeadAudioStream")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetAudioStream( [FromRoute] Guid itemId, diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs index 67790c0e4..1d4836f27 100644 --- a/Jellyfin.Api/Controllers/BrandingController.cs +++ b/Jellyfin.Api/Controllers/BrandingController.cs @@ -44,7 +44,7 @@ namespace Jellyfin.Api.Controllers /// or a if the css is not configured. /// [HttpGet("Css")] - [HttpGet("Css.css")] + [HttpGet("Css.css", Name = "GetBrandingCss_2")] [Produces("text/css")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 2f5561adb..ef507f2ed 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -42,8 +42,8 @@ namespace Jellyfin.Api.Controllers /// Server UUID. /// Description xml returned. /// An containing the description xml. - [HttpGet("{serverId}/description.xml")] [HttpGet("{serverId}/description")] + [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] [Produces(XMLContentType)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDescriptionXml([FromRoute] string serverId) @@ -60,8 +60,8 @@ namespace Jellyfin.Api.Controllers /// Server UUID. /// Dlna content directory returned. /// An containing the dlna content directory xml. - [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml")] [HttpGet("{serverId}/ContentDirectory/ContentDirectory")] + [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_2")] [Produces(XMLContentType)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] @@ -75,8 +75,8 @@ namespace Jellyfin.Api.Controllers /// /// Server UUID. /// Dlna media receiver registrar xml. - [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml")] [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar")] + [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")] [Produces(XMLContentType)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] @@ -90,8 +90,8 @@ namespace Jellyfin.Api.Controllers /// /// Server UUID. /// Dlna media receiver registrar xml. - [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml")] [HttpGet("{serverId}/ConnectionManager/ConnectionManager")] + [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_2")] [Produces(XMLContentType)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] @@ -181,7 +181,7 @@ namespace Jellyfin.Api.Controllers /// Server UUID. /// The icon filename. /// Icon stream. - [HttpGet("{serverId}/icons/{filename}")] + [HttpGet("{serverId}/icons/{fileName}")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName) { @@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers /// /// The icon filename. /// Icon stream. - [HttpGet("icons/{filename}")] + [HttpGet("icons/{fileName}")] public ActionResult GetIcon([FromRoute] string fileName) { return GetIconInternal(fileName); diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b7e1837c9..c4f79ce95 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -165,7 +165,7 @@ namespace Jellyfin.Api.Controllers /// Video stream returned. /// A containing the playlist file. [HttpGet("/Videos/{itemId}/master.m3u8")] - [HttpHead("/Videos/{itemId}/master.m3u8")] + [HttpHead("/Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetMasterHlsVideoPlaylist( [FromRoute] Guid itemId, @@ -335,7 +335,7 @@ namespace Jellyfin.Api.Controllers /// Audio stream returned. /// A containing the playlist file. [HttpGet("/Audio/{itemId}/master.m3u8")] - [HttpHead("/Audio/{itemId}/master.m3u8")] + [HttpHead("/Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetMasterHlsAudioPlaylist( [FromRoute] Guid itemId, diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index efdb6a369..7bf9326a7 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -50,8 +50,8 @@ namespace Jellyfin.Api.Controllers /// A containing the audio stream. // Can't require authentication just yet due to seeing some requests come from Chrome without full query string // [Authenticated] - [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3")] - [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac")] + [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")] + [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 18220c5f3..3a445b1b3 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers /// User does not have permission to delete the image. /// A . [HttpPost("/Users/{userId}/Images/{imageType}")] - [HttpPost("/Users/{userId}/Images/{imageType}/{index?}")] + [HttpPost("/Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] @@ -128,7 +128,7 @@ namespace Jellyfin.Api.Controllers /// User does not have permission to delete the image. /// A . [HttpDelete("/Users/{userId}/Images/{itemType}")] - [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}")] + [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")] [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)] @@ -167,7 +167,7 @@ namespace Jellyfin.Api.Controllers /// Item not found. /// A on success, or a if item not found. [HttpDelete("/Items/{itemId}/Images/{imageType}")] - [HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers /// Item not found. /// A on success, or a if item not found. [HttpPost("/Items/{itemId}/Images/{imageType}")] - [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -342,9 +342,9 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Items/{itemId}/Images/{imageType}")] - [HttpHead("/Items/{itemId}/Images/{imageType}")] - [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")] + [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")] + [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetItemImage( @@ -422,7 +422,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] - [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] + [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetItemImage2( @@ -500,7 +500,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Artists/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetArtistImage( @@ -578,7 +578,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Genres/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetGenreImage( @@ -656,7 +656,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetMusicGenreImage( @@ -734,7 +734,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Persons/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetPersonImage( @@ -812,7 +812,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Studios/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetStudioImage( @@ -890,7 +890,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Users/{userId}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetUserImage( diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 49fb9238f..354741ced 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers /// Optional, include image information in output. /// A with the items. [HttpGet("/Items")] - [HttpGet("/Users/{uId}/Items")] + [HttpGet("/Users/{uId}/Items", Name = "GetItems_2")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetItems( [FromRoute] Guid? uId, diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 5ad466c55..0ec7e2b8c 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -521,7 +521,7 @@ namespace Jellyfin.Api.Controllers /// The tvdbId. /// Report success. /// A . - [HttpPost("/Library/Series/Added")] + [HttpPost("/Library/Series/Added", Name = "PostAddedSeries")] [HttpPost("/Library/Series/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -551,7 +551,7 @@ namespace Jellyfin.Api.Controllers /// The imdbId. /// Report success. /// A . - [HttpPost("/Library/Movies/Added")] + [HttpPost("/Library/Movies/Added", Name = "PostAddedMovies")] [HttpPost("/Library/Movies/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -679,12 +679,12 @@ namespace Jellyfin.Api.Controllers /// 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, TrailerUrls. /// Similar items returned. /// A containing the similar items. - [HttpGet("/Artists/{itemId}/Similar")] + [HttpGet("/Artists/{itemId}/Similar", Name = "GetSimilarArtists2")] [HttpGet("/Items/{itemId}/Similar")] - [HttpGet("/Albums/{itemId}/Similar")] - [HttpGet("/Shows/{itemId}/Similar")] - [HttpGet("/Movies/{itemId}/Similar")] - [HttpGet("/Trailers/{itemId}/Similar")] + [HttpGet("/Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")] + [HttpGet("/Shows/{itemId}/Similar", Name = "GetSimilarShows2")] + [HttpGet("/Movies/{itemId}/Similar", Name = "GetSimilarMovies2")] + [HttpGet("/Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarItems( [FromRoute] Guid itemId, diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index bbe5544f9..89112eea7 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Channels")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] - public ActionResult> GetChannels( + public ActionResult> GetLiveTvChannels( [FromQuery] ChannelType? type, [FromQuery] Guid? userId, [FromQuery] int? startIndex, @@ -535,7 +535,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Programs")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] - public async Task>> GetPrograms( + public async Task>> GetLiveTvPrograms( [FromQuery] string? channelIds, [FromQuery] Guid? userId, [FromQuery] DateTime? minStartDate, @@ -933,7 +933,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Obsolete("This endpoint is obsolete.")] - public ActionResult GetRecordingGroup([FromQuery] Guid? groupId) + public ActionResult GetRecordingGroup([FromRoute] Guid? groupId) { return NotFound(); } diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 0c98a8e71..1b300e0d8 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -241,7 +241,7 @@ namespace Jellyfin.Api.Controllers /// The command to send. /// General command sent to session. /// A . - [HttpPost("/Sessions/{sessionId}/Command/{Command}")] + [HttpPost("/Sessions/{sessionId}/Command/{command}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendGeneralCommand( [FromRoute] string? sessionId, diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index f9e4e61b5..c8e3cc4f5 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -106,7 +106,7 @@ namespace Jellyfin.Api.Controllers /// Initial user retrieved. /// The first user. [HttpGet("User")] - [HttpGet("FirstUser")] + [HttpGet("FirstUser", Name = "GetFirstUser_2")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetFirstUser() { @@ -131,7 +131,7 @@ namespace Jellyfin.Api.Controllers /// [HttpPost("User")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) + public async Task UpdateStartupUser([FromForm] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index b62ff80fc..f8c19d15c 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -182,7 +182,7 @@ namespace Jellyfin.Api.Controllers /// File returned. /// A with the subtitle file. [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] - [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}")] + [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetSubtitle( [FromRoute, Required] Guid itemId, diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 55ed42227..2b1b95b1b 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("New")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateNewGroup() + public ActionResult SyncPlayCreateGroup() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); _syncPlayManager.NewGroup(currentSession, CancellationToken.None); @@ -62,7 +62,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Join")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult JoinGroup([FromQuery, Required] Guid groupId) + public ActionResult SyncPlayJoinGroup([FromQuery, Required] Guid groupId) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Leave")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult LeaveGroup() + public ActionResult SyncPlayLeaveGroup() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None); @@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers /// An containing the available SyncPlay groups. [HttpGet("List")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSyncPlayGroups([FromQuery] Guid? filterItemId) + public ActionResult> SyncPlayGetGroups([FromQuery] Guid? filterItemId) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty)); @@ -110,7 +110,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Play")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Play() + public ActionResult SyncPlayPlay() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() @@ -128,7 +128,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Pause")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Pause() + public ActionResult SyncPlayPause() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() @@ -147,7 +147,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Seek")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Seek([FromQuery] long positionTicks) + public ActionResult SyncPlaySeek([FromQuery] long positionTicks) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() @@ -169,7 +169,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Buffering")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Buffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone) + public ActionResult SyncPlayBuffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() @@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Ping")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Ping([FromQuery] double ping) + public ActionResult SyncPlayPing([FromQuery] double ping) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index bc606f7aa..e0bce3a41 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -85,8 +85,8 @@ namespace Jellyfin.Api.Controllers /// /// Information retrieved. /// The server name. - [HttpGet("Ping")] - [HttpPost("Ping")] + [HttpGet("Ping", Name = "GetPingSystem")] + [HttpPost("Ping", Name = "PostPingSystem")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult PingSystem() { diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 50ab0ac05..5a9bec2b0 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -69,9 +69,9 @@ namespace Jellyfin.Api.Controllers /// Redirected to remote audio stream. /// A containing the audio file. [HttpGet("/Audio/{itemId}/universal")] - [HttpGet("/Audio/{itemId}/{universal=universal}.{container?}")] - [HttpHead("/Audio/{itemId}/universal")] - [HttpHead("/Audio/{itemId}/{universal=universal}.{container?}")] + [HttpGet("/Audio/{itemId}/{universal=universal}.{container?}", Name = "GetUniversalAudioStream_2")] + [HttpHead("/Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] + [HttpHead("/Audio/{itemId}/{universal=universal}.{container?}", Name = "HeadUniversalAudioStream_2")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status302Found)] diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index d1ef817eb..ebe88a9c0 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -316,10 +316,10 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Video stream returned. /// A containing the audio file. - [HttpGet("{itemId}/{stream=stream}.{container?}")] + [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStream_2")] [HttpGet("{itemId}/stream")] - [HttpHead("{itemId}/{stream=stream}.{container?}")] - [HttpHead("{itemId}/stream")] + [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")] + [HttpHead("{itemId}/stream", Name = "HeadVideoStream")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetVideoStream( [FromRoute] Guid itemId, diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index cfbabf795..6e91042df 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -198,8 +198,15 @@ namespace Jellyfin.Server.Extensions $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}"); // Use method name as operationId - c.CustomOperationIds(description => - description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null); + c.CustomOperationIds( + description => + { + description.TryGetMethodInfo(out MethodInfo methodInfo); + // Attribute name, method name, none. + return description?.ActionDescriptor?.AttributeRouteInfo?.Name + ?? methodInfo?.Name + ?? null; + }); // TODO - remove when all types are supported in System.Text.Json c.AddSwaggerTypeMappings(); diff --git a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs b/tests/Jellyfin.Api.Tests/GetPathValueTests.cs deleted file mode 100644 index 397eb2edc..000000000 --- a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using MediaBrowser.Api; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; - -namespace Jellyfin.Api.Tests -{ - public class GetPathValueTests - { - [Theory] - [InlineData("https://localhost:8096/ScheduledTasks/1234/Triggers", "", 1, "1234")] - [InlineData("https://localhost:8096/emby/ScheduledTasks/1234/Triggers", "", 1, "1234")] - [InlineData("https://localhost:8096/mediabrowser/ScheduledTasks/1234/Triggers", "", 1, "1234")] - [InlineData("https://localhost:8096/jellyfin/2/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - [InlineData("https://localhost:8096/jellyfin/2/emby/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - [InlineData("https://localhost:8096/jellyfin/2/mediabrowser/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - [InlineData("https://localhost:8096/JELLYFIN/2/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - [InlineData("https://localhost:8096/JELLYFIN/2/Emby/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - [InlineData("https://localhost:8096/JELLYFIN/2/MediaBrowser/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - public void GetPathValueTest(string path, string baseUrl, int index, string value) - { - var reqMock = Mock.Of(x => x.PathInfo == path); - var conf = new ServerConfiguration() - { - BaseUrl = baseUrl - }; - - var confManagerMock = Mock.Of(x => x.Configuration == conf); - - var service = new TestService( - new NullLogger(), - confManagerMock, - Mock.Of()) - { - Request = reqMock - }; - - Assert.Equal(value, service.GetPathValue(index).ToString()); - } - } -} -- cgit v1.2.3 From 858aecd409914f6a9ca6f86445ec20c8bacc0b59 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 4 Aug 2020 12:48:53 -0600 Subject: Fix all route for base url support --- Jellyfin.Api/Controllers/AlbumsController.cs | 5 +- Jellyfin.Api/Controllers/DashboardController.cs | 11 +++-- Jellyfin.Api/Controllers/DynamicHlsController.cs | 17 ++++--- Jellyfin.Api/Controllers/FilterController.cs | 5 +- Jellyfin.Api/Controllers/HlsSegmentController.cs | 11 +++-- Jellyfin.Api/Controllers/ImageController.cs | 57 +++++++++++----------- Jellyfin.Api/Controllers/InstantMixController.cs | 15 +++--- Jellyfin.Api/Controllers/ItemLookupController.cs | 25 +++++----- Jellyfin.Api/Controllers/ItemUpdateController.cs | 7 +-- Jellyfin.Api/Controllers/ItemsController.cs | 7 +-- Jellyfin.Api/Controllers/LibraryController.cs | 51 +++++++++---------- Jellyfin.Api/Controllers/MediaInfoController.cs | 11 +++-- Jellyfin.Api/Controllers/PackageController.cs | 14 +++--- Jellyfin.Api/Controllers/PlaystateController.cs | 19 ++++---- Jellyfin.Api/Controllers/PluginsController.cs | 2 +- Jellyfin.Api/Controllers/SessionController.cs | 33 +++++++------ Jellyfin.Api/Controllers/SubtitleController.cs | 15 +++--- Jellyfin.Api/Controllers/SuggestionsController.cs | 3 +- Jellyfin.Api/Controllers/TrailersController.cs | 2 +- .../Controllers/UniversalAudioController.cs | 9 ++-- Jellyfin.Api/Controllers/UserController.cs | 2 +- Jellyfin.Api/Controllers/UserLibraryController.cs | 21 ++++---- Jellyfin.Api/Controllers/UserViewsController.cs | 5 +- Jellyfin.Api/Controllers/VideoHlsController.cs | 5 +- 24 files changed, 186 insertions(+), 166 deletions(-) (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Controllers/AlbumsController.cs b/Jellyfin.Api/Controllers/AlbumsController.cs index 01ba7fc32..190d4bd07 100644 --- a/Jellyfin.Api/Controllers/AlbumsController.cs +++ b/Jellyfin.Api/Controllers/AlbumsController.cs @@ -17,6 +17,7 @@ namespace Jellyfin.Api.Controllers /// /// The albums controller. /// + [Route("")] public class AlbumsController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -48,7 +49,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The maximum number of records to return. /// Similar albums returned. /// A with similar albums. - [HttpGet("/Albums/{albumId}/Similar")] + [HttpGet("Albums/{albumId}/Similar")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarAlbums( [FromRoute] string albumId, @@ -80,7 +81,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The maximum number of records to return. /// Similar artists returned. /// A with similar artists. - [HttpGet("/Artists/{artistId}/Similar")] + [HttpGet("Artists/{artistId}/Similar")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarArtists( [FromRoute] string artistId, diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index e033c50d5..a7bdb24f6 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers /// /// The dashboard controller. /// + [Route("")] public class DashboardController : BaseJellyfinApiController { private readonly ILogger _logger; @@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers /// ConfigurationPages returned. /// Server still loading. /// An with infos about the plugins. - [HttpGet("/web/ConfigurationPages")] + [HttpGet("web/ConfigurationPages")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetConfigurationPages( @@ -118,7 +119,7 @@ namespace Jellyfin.Api.Controllers /// ConfigurationPage returned. /// Plugin configuration page not found. /// The configuration page. - [HttpGet("/web/ConfigurationPage")] + [HttpGet("web/ConfigurationPage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetDashboardConfigurationPage([FromQuery] string? name) @@ -172,7 +173,7 @@ namespace Jellyfin.Api.Controllers /// /// Robots.txt returned. /// The robots.txt. - [HttpGet("/robots.txt")] + [HttpGet("robots.txt")] [ProducesResponseType(StatusCodes.Status200OK)] [ApiExplorerSettings(IgnoreApi = true)] public ActionResult GetRobotsTxt() @@ -187,7 +188,7 @@ namespace Jellyfin.Api.Controllers /// Web client returned. /// Server does not host a web client. /// The resource. - [HttpGet("/web/{*resourceName}")] + [HttpGet("web/{*resourceName}")] [ApiExplorerSettings(IgnoreApi = true)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -218,7 +219,7 @@ namespace Jellyfin.Api.Controllers /// /// Favicon.ico returned. /// The favicon. - [HttpGet("/favicon.ico")] + [HttpGet("favicon.ico")] [ProducesResponseType(StatusCodes.Status200OK)] [ApiExplorerSettings(IgnoreApi = true)] public ActionResult GetFavIcon() diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index c4f79ce95..ec65cb95a 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -37,6 +37,7 @@ namespace Jellyfin.Api.Controllers /// /// Dynamic hls controller. /// + [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] public class DynamicHlsController : BaseJellyfinApiController { @@ -164,8 +165,8 @@ namespace Jellyfin.Api.Controllers /// Enable adaptive bitrate streaming. /// Video stream returned. /// A containing the playlist file. - [HttpGet("/Videos/{itemId}/master.m3u8")] - [HttpHead("/Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")] + [HttpGet("Videos/{itemId}/master.m3u8")] + [HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetMasterHlsVideoPlaylist( [FromRoute] Guid itemId, @@ -334,8 +335,8 @@ namespace Jellyfin.Api.Controllers /// Enable adaptive bitrate streaming. /// Audio stream returned. /// A containing the playlist file. - [HttpGet("/Audio/{itemId}/master.m3u8")] - [HttpHead("/Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")] + [HttpGet("Audio/{itemId}/master.m3u8")] + [HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetMasterHlsAudioPlaylist( [FromRoute] Guid itemId, @@ -503,7 +504,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Video stream returned. /// A containing the audio file. - [HttpGet("/Videos/{itemId}/main.m3u8")] + [HttpGet("Videos/{itemId}/main.m3u8")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetVariantHlsVideoPlaylist( [FromRoute] Guid itemId, @@ -668,7 +669,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Audio stream returned. /// A containing the audio file. - [HttpGet("/Audio/{itemId}/main.m3u8")] + [HttpGet("Audio/{itemId}/main.m3u8")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetVariantHlsAudioPlaylist( [FromRoute] Guid itemId, @@ -835,7 +836,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Video stream returned. /// A containing the audio file. - [HttpGet("/Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] + [HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")] public async Task GetHlsVideoSegment( @@ -1004,7 +1005,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Video stream returned. /// A containing the audio file. - [HttpGet("/Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] + [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")] public async Task GetHlsAudioSegment( diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 9ba5e1161..2a567c846 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -18,6 +18,7 @@ namespace Jellyfin.Api.Controllers /// /// Filters controller. /// + [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] public class FilterController : BaseJellyfinApiController { @@ -44,7 +45,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Filter by MediaType. Allows multiple, comma delimited. /// Legacy filters retrieved. /// Legacy query filters. - [HttpGet("/Items/Filters")] + [HttpGet("Items/Filters")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetQueryFiltersLegacy( [FromQuery] Guid? userId, @@ -133,7 +134,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Search recursive. /// Filters retrieved. /// Query filters. - [HttpGet("/Items/Filters2")] + [HttpGet("Items/Filters2")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetQueryFilters( [FromQuery] Guid? userId, diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 7bf9326a7..e4a6842bc 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -19,6 +19,7 @@ namespace Jellyfin.Api.Controllers /// /// The hls segment controller. /// + [Route("")] public class HlsSegmentController : BaseJellyfinApiController { private readonly IFileSystem _fileSystem; @@ -50,8 +51,8 @@ namespace Jellyfin.Api.Controllers /// A containing the audio stream. // Can't require authentication just yet due to seeing some requests come from Chrome without full query string // [Authenticated] - [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")] - [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")] + [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")] + [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId) @@ -70,7 +71,7 @@ namespace Jellyfin.Api.Controllers /// The playlist id. /// Hls video playlist returned. /// A containing the playlist. - [HttpGet("/Videos/{itemId}/hls/{playlistId}/stream.m3u8")] + [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] @@ -89,7 +90,7 @@ namespace Jellyfin.Api.Controllers /// The play session id. /// Encoding stopped successfully. /// A indicating success. - [HttpDelete("/Videos/ActiveEncodings")] + [HttpDelete("Videos/ActiveEncodings")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId) @@ -109,7 +110,7 @@ namespace Jellyfin.Api.Controllers /// A containing the video segment. // Can't require authentication just yet due to seeing some requests come from Chrome without full query string // [Authenticated] - [HttpGet("/Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")] + [HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] public ActionResult GetHlsVideoSegmentLegacy( diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 3a445b1b3..360164ad4 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -30,6 +30,7 @@ namespace Jellyfin.Api.Controllers /// /// Image controller. /// + [Route("")] public class ImageController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -81,8 +82,8 @@ namespace Jellyfin.Api.Controllers /// Image updated. /// User does not have permission to delete the image. /// A . - [HttpPost("/Users/{userId}/Images/{imageType}")] - [HttpPost("/Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")] + [HttpPost("Users/{userId}/Images/{imageType}")] + [HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] @@ -127,8 +128,8 @@ namespace Jellyfin.Api.Controllers /// Image deleted. /// User does not have permission to delete the image. /// A . - [HttpDelete("/Users/{userId}/Images/{itemType}")] - [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")] + [HttpDelete("Users/{userId}/Images/{itemType}")] + [HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")] [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)] @@ -166,8 +167,8 @@ namespace Jellyfin.Api.Controllers /// Image deleted. /// Item not found. /// A on success, or a if item not found. - [HttpDelete("/Items/{itemId}/Images/{imageType}")] - [HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")] + [HttpDelete("Items/{itemId}/Images/{imageType}")] + [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -195,8 +196,8 @@ namespace Jellyfin.Api.Controllers /// Image saved. /// Item not found. /// A on success, or a if item not found. - [HttpPost("/Items/{itemId}/Images/{imageType}")] - [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")] + [HttpPost("Items/{itemId}/Images/{imageType}")] + [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -230,7 +231,7 @@ namespace Jellyfin.Api.Controllers /// Image index updated. /// Item not found. /// A on success, or a if item not found. - [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex}/Index")] + [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -257,7 +258,7 @@ namespace Jellyfin.Api.Controllers /// Item images returned. /// Item not found. /// The list of image infos on success, or if item not found. - [HttpGet("/Items/{itemId}/Images")] + [HttpGet("Items/{itemId}/Images")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetItemImageInfos([FromRoute] Guid itemId) @@ -341,10 +342,10 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("/Items/{itemId}/Images/{imageType}")] - [HttpHead("/Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")] - [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")] - [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")] + [HttpGet("Items/{itemId}/Images/{imageType}")] + [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")] + [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")] + [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetItemImage( @@ -421,8 +422,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] - [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")] + [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] + [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetItemImage2( @@ -499,8 +500,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("/Artists/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")] + [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetArtistImage( @@ -577,8 +578,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("/Genres/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")] + [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetGenreImage( @@ -655,8 +656,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")] + [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetMusicGenreImage( @@ -733,8 +734,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("/Persons/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")] + [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetPersonImage( @@ -811,8 +812,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("/Studios/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")] + [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetStudioImage( @@ -889,8 +890,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("/Users/{userId}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")] + [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex?}")] + [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetUserImage( diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index bb980af3e..8ca232cef 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -19,6 +19,7 @@ namespace Jellyfin.Api.Controllers /// /// The instant mix controller. /// + [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] public class InstantMixController : BaseJellyfinApiController { @@ -59,7 +60,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The image types to include in the output. /// Instant playlist returned. /// A with the playlist items. - [HttpGet("/Songs/{id}/InstantMix")] + [HttpGet("Songs/{id}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromSong( [FromRoute] Guid id, @@ -96,7 +97,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The image types to include in the output. /// Instant playlist returned. /// A with the playlist items. - [HttpGet("/Albums/{id}/InstantMix")] + [HttpGet("Albums/{id}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromAlbum( [FromRoute] Guid id, @@ -133,7 +134,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The image types to include in the output. /// Instant playlist returned. /// A with the playlist items. - [HttpGet("/Playlists/{id}/InstantMix")] + [HttpGet("Playlists/{id}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromPlaylist( [FromRoute] Guid id, @@ -170,7 +171,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The image types to include in the output. /// Instant playlist returned. /// A with the playlist items. - [HttpGet("/MusicGenres/{name}/InstantMix")] + [HttpGet("MusicGenres/{name}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromMusicGenre( [FromRoute] string? name, @@ -206,7 +207,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The image types to include in the output. /// Instant playlist returned. /// A with the playlist items. - [HttpGet("/Artists/InstantMix")] + [HttpGet("Artists/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromArtists( [FromRoute] Guid id, @@ -243,7 +244,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The image types to include in the output. /// Instant playlist returned. /// A with the playlist items. - [HttpGet("/MusicGenres/InstantMix")] + [HttpGet("MusicGenres/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromMusicGenres( [FromRoute] Guid id, @@ -280,7 +281,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The image types to include in the output. /// Instant playlist returned. /// A with the playlist items. - [HttpGet("/Items/{id}/InstantMix")] + [HttpGet("Items/{id}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromItem( [FromRoute] Guid id, diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index 44709d0ee..0d9dffbfe 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -30,6 +30,7 @@ namespace Jellyfin.Api.Controllers /// /// Item lookup controller. /// + [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] public class ItemLookupController : BaseJellyfinApiController { @@ -68,7 +69,7 @@ namespace Jellyfin.Api.Controllers /// External id info retrieved. /// Item not found. /// List of external id info. - [HttpGet("/Items/{itemId}/ExternalIdInfos")] + [HttpGet("Items/{itemId}/ExternalIdInfos")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -92,7 +93,7 @@ namespace Jellyfin.Api.Controllers /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// - [HttpPost("/Items/RemoteSearch/Movie")] + [HttpPost("Items/RemoteSearch/Movie")] public async Task>> GetMovieRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) @@ -109,7 +110,7 @@ namespace Jellyfin.Api.Controllers /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// - [HttpPost("/Items/RemoteSearch/Trailer")] + [HttpPost("Items/RemoteSearch/Trailer")] public async Task>> GetTrailerRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) @@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// - [HttpPost("/Items/RemoteSearch/MusicVideo")] + [HttpPost("Items/RemoteSearch/MusicVideo")] public async Task>> GetMusicVideoRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) @@ -143,7 +144,7 @@ namespace Jellyfin.Api.Controllers /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// - [HttpPost("/Items/RemoteSearch/Series")] + [HttpPost("Items/RemoteSearch/Series")] public async Task>> GetSeriesRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) @@ -160,7 +161,7 @@ namespace Jellyfin.Api.Controllers /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// - [HttpPost("/Items/RemoteSearch/BoxSet")] + [HttpPost("Items/RemoteSearch/BoxSet")] public async Task>> GetBoxSetRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) @@ -177,7 +178,7 @@ namespace Jellyfin.Api.Controllers /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// - [HttpPost("/Items/RemoteSearch/MusicArtist")] + [HttpPost("Items/RemoteSearch/MusicArtist")] public async Task>> GetMusicArtistRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) @@ -194,7 +195,7 @@ namespace Jellyfin.Api.Controllers /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// - [HttpPost("/Items/RemoteSearch/MusicAlbum")] + [HttpPost("Items/RemoteSearch/MusicAlbum")] public async Task>> GetMusicAlbumRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) @@ -211,7 +212,7 @@ namespace Jellyfin.Api.Controllers /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// - [HttpPost("/Items/RemoteSearch/Person")] + [HttpPost("Items/RemoteSearch/Person")] [Authorize(Policy = Policies.RequiresElevation)] public async Task>> GetPersonRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) { @@ -229,7 +230,7 @@ namespace Jellyfin.Api.Controllers /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// - [HttpPost("/Items/RemoteSearch/Book")] + [HttpPost("Items/RemoteSearch/Book")] public async Task>> GetBookRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) @@ -247,7 +248,7 @@ namespace Jellyfin.Api.Controllers /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the images file stream. /// - [HttpGet("/Items/RemoteSearch/Image")] + [HttpGet("Items/RemoteSearch/Image")] public async Task GetRemoteSearchImage( [FromQuery, Required] string imageUrl, [FromQuery, Required] string providerName) @@ -291,7 +292,7 @@ namespace Jellyfin.Api.Controllers /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an . /// - [HttpPost("/Items/RemoteSearch/Apply/{id}")] + [HttpPost("Items/RemoteSearch/Apply/{id}")] [Authorize(Policy = Policies.RequiresElevation)] public async Task ApplySearchCriteria( [FromRoute] Guid itemId, diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index c9b2aafcc..a5d9d36a3 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -24,6 +24,7 @@ namespace Jellyfin.Api.Controllers /// /// Item update controller. /// + [Route("")] [Authorize(Policy = Policies.RequiresElevation)] public class ItemUpdateController : BaseJellyfinApiController { @@ -63,7 +64,7 @@ namespace Jellyfin.Api.Controllers /// Item updated. /// Item not found. /// An on success, or a if the item could not be found. - [HttpPost("/Items/{itemId}")] + [HttpPost("Items/{itemId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, BindRequired] BaseItemDto request) @@ -136,7 +137,7 @@ namespace Jellyfin.Api.Controllers /// Item metadata editor returned. /// Item not found. /// An on success containing the metadata editor, or a if the item could not be found. - [HttpGet("/Items/{itemId}/MetadataEditor")] + [HttpGet("Items/{itemId}/MetadataEditor")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetMetadataEditorInfo([FromRoute] Guid itemId) @@ -190,7 +191,7 @@ namespace Jellyfin.Api.Controllers /// Item content type updated. /// Item not found. /// An on success, or a if the item could not be found. - [HttpPost("/Items/{itemId}/ContentType")] + [HttpPost("Items/{itemId}/ContentType")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string? contentType) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 354741ced..1b8b68313 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -23,6 +23,7 @@ namespace Jellyfin.Api.Controllers /// /// The items controller. /// + [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] public class ItemsController : BaseJellyfinApiController { @@ -139,8 +140,8 @@ namespace Jellyfin.Api.Controllers /// Optional. Enable the total record count. /// Optional, include image information in output. /// A with the items. - [HttpGet("/Items")] - [HttpGet("/Users/{uId}/Items", Name = "GetItems_2")] + [HttpGet("Items")] + [HttpGet("Users/{uId}/Items", Name = "GetItems_2")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetItems( [FromRoute] Guid? uId, @@ -523,7 +524,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Include image information in output. /// Items returned. /// A with the items that are resumable. - [HttpGet("/Users/{userId}/Items/Resume")] + [HttpGet("Users/{userId}/Items/Resume")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetResumeItems( [FromRoute] Guid userId, diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 0ec7e2b8c..a4637752d 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -43,6 +43,7 @@ namespace Jellyfin.Api.Controllers /// /// Library Controller. /// + [Route("")] public class LibraryController : BaseJellyfinApiController { private readonly IProviderManager _providerManager; @@ -100,7 +101,7 @@ namespace Jellyfin.Api.Controllers /// File stream returned. /// Item not found. /// A with the original file. - [HttpGet("/Items/{itemId}/File")] + [HttpGet("Items/{itemId}/File")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -121,7 +122,7 @@ namespace Jellyfin.Api.Controllers /// /// Critic reviews returned. /// The list of critic reviews. - [HttpGet("/Items/{itemId}/CriticReviews")] + [HttpGet("Items/{itemId}/CriticReviews")] [Authorize(Policy = Policies.DefaultAuthorization)] [Obsolete("This endpoint is obsolete.")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -139,7 +140,7 @@ namespace Jellyfin.Api.Controllers /// Theme songs returned. /// Item not found. /// The item theme songs. - [HttpGet("/Items/{itemId}/ThemeSongs")] + [HttpGet("Items/{itemId}/ThemeSongs")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -205,7 +206,7 @@ namespace Jellyfin.Api.Controllers /// Theme videos returned. /// Item not found. /// The item theme videos. - [HttpGet("/Items/{itemId}/ThemeVideos")] + [HttpGet("Items/{itemId}/ThemeVideos")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -271,7 +272,7 @@ namespace Jellyfin.Api.Controllers /// Theme songs and videos returned. /// Item not found. /// The item theme videos. - [HttpGet("/Items/{itemId}/ThemeMedia")] + [HttpGet("Items/{itemId}/ThemeMedia")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetThemeMedia( @@ -302,7 +303,7 @@ namespace Jellyfin.Api.Controllers /// /// Library scan started. /// A . - [HttpGet("/Library/Refresh")] + [HttpGet("Library/Refresh")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task RefreshLibrary() @@ -326,7 +327,7 @@ namespace Jellyfin.Api.Controllers /// Item deleted. /// Unauthorized access. /// A . - [HttpDelete("/Items/{itemId}")] + [HttpDelete("Items/{itemId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -356,7 +357,7 @@ namespace Jellyfin.Api.Controllers /// Items deleted. /// Unauthorized access. /// A . - [HttpDelete("/Items")] + [HttpDelete("Items")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -400,7 +401,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Get counts of favorite items. /// Item counts returned. /// Item counts. - [HttpGet("/Items/Counts")] + [HttpGet("Items/Counts")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetItemCounts( @@ -434,7 +435,7 @@ namespace Jellyfin.Api.Controllers /// Item parents returned. /// Item not found. /// Item parents. - [HttpGet("/Items/{itemId}/Ancestors")] + [HttpGet("Items/{itemId}/Ancestors")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -476,7 +477,7 @@ namespace Jellyfin.Api.Controllers /// /// Physical paths returned. /// List of physical paths. - [HttpGet("/Library/PhysicalPaths")] + [HttpGet("Library/PhysicalPaths")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetPhysicalPaths() @@ -491,7 +492,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Filter by folders that are marked hidden, or not. /// Media folders returned. /// List of user media folders. - [HttpGet("/Library/MediaFolders")] + [HttpGet("Library/MediaFolders")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetMediaFolders([FromQuery] bool? isHidden) @@ -521,8 +522,8 @@ namespace Jellyfin.Api.Controllers /// The tvdbId. /// Report success. /// A . - [HttpPost("/Library/Series/Added", Name = "PostAddedSeries")] - [HttpPost("/Library/Series/Updated")] + [HttpPost("Library/Series/Added", Name = "PostAddedSeries")] + [HttpPost("Library/Series/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId) @@ -551,8 +552,8 @@ namespace Jellyfin.Api.Controllers /// The imdbId. /// Report success. /// A . - [HttpPost("/Library/Movies/Added", Name = "PostAddedMovies")] - [HttpPost("/Library/Movies/Updated")] + [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")] + [HttpPost("Library/Movies/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedMovies([FromRoute] string? tmdbId, [FromRoute] string? imdbId) @@ -593,7 +594,7 @@ namespace Jellyfin.Api.Controllers /// A list of updated media paths. /// Report success. /// A . - [HttpPost("/Library/Media/Updated")] + [HttpPost("Library/Media/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostUpdatedMedia([FromBody, BindRequired] MediaUpdateInfoDto[] updates) @@ -614,7 +615,7 @@ namespace Jellyfin.Api.Controllers /// Item not found. /// A containing the media stream. /// User can't download or item can't be downloaded. - [HttpGet("/Items/{itemId}/Download")] + [HttpGet("Items/{itemId}/Download")] [Authorize(Policy = Policies.Download)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -679,12 +680,12 @@ namespace Jellyfin.Api.Controllers /// 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, TrailerUrls. /// Similar items returned. /// A containing the similar items. - [HttpGet("/Artists/{itemId}/Similar", Name = "GetSimilarArtists2")] - [HttpGet("/Items/{itemId}/Similar")] - [HttpGet("/Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")] - [HttpGet("/Shows/{itemId}/Similar", Name = "GetSimilarShows2")] - [HttpGet("/Movies/{itemId}/Similar", Name = "GetSimilarMovies2")] - [HttpGet("/Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")] + [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists2")] + [HttpGet("Items/{itemId}/Similar")] + [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")] + [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")] + [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")] + [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarItems( [FromRoute] Guid itemId, @@ -735,7 +736,7 @@ namespace Jellyfin.Api.Controllers /// Whether this is a new library. /// Library options info returned. /// Library options info. - [HttpGet("/Libraries/AvailableOptions")] + [HttpGet("Libraries/AvailableOptions")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetLibraryOptionsInfo( diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index 5b0f46b02..242cbf191 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -34,6 +34,7 @@ namespace Jellyfin.Api.Controllers /// /// The media info controller. /// + [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] public class MediaInfoController : BaseJellyfinApiController { @@ -88,7 +89,7 @@ namespace Jellyfin.Api.Controllers /// The user id. /// Playback info returned. /// A containing a with the playback information. - [HttpGet("/Items/{itemId}/PlaybackInfo")] + [HttpGet("Items/{itemId}/PlaybackInfo")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid? userId) { @@ -116,7 +117,7 @@ namespace Jellyfin.Api.Controllers /// Whether to allow to copy the audio stream. Default: true. /// Playback info returned. /// A containing a with the playback info. - [HttpPost("/Items/{itemId}/PlaybackInfo")] + [HttpPost("Items/{itemId}/PlaybackInfo")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetPostedPlaybackInfo( [FromRoute] Guid itemId, @@ -237,7 +238,7 @@ namespace Jellyfin.Api.Controllers /// Whether to enable direct stream. Default: true. /// Media source opened. /// A containing a . - [HttpPost("/LiveStreams/Open")] + [HttpPost("LiveStreams/Open")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> OpenLiveStream( [FromQuery] string? openToken, @@ -278,7 +279,7 @@ namespace Jellyfin.Api.Controllers /// The livestream id. /// Livestream closed. /// A indicating success. - [HttpPost("/LiveStreams/Close")] + [HttpPost("LiveStreams/Close")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult CloseLiveStream([FromQuery] string? liveStreamId) { @@ -293,7 +294,7 @@ namespace Jellyfin.Api.Controllers /// Test buffer returned. /// Size has to be a numer between 0 and 10,000,000. /// A with specified bitrate. - [HttpGet("/Playback/BitrateTest")] + [HttpGet("Playback/BitrateTest")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [Produces(MediaTypeNames.Application.Octet)] diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index a6c552790..06c4213fb 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers /// /// Package Controller. /// - [Route("Packages")] + [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] public class PackageController : BaseJellyfinApiController { @@ -41,7 +41,7 @@ namespace Jellyfin.Api.Controllers /// The GUID of the associated assembly. /// Package retrieved. /// A containing package information. - [HttpGet("/{name}")] + [HttpGet("Packages/{name}")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetPackageInfo( [FromRoute] [Required] string? name, @@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers /// /// Available packages returned. /// An containing available packages information. - [HttpGet] + [HttpGet("Packages")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetPackages() { @@ -79,7 +79,7 @@ namespace Jellyfin.Api.Controllers /// Package found. /// Package not found. /// A on success, or a if the package could not be found. - [HttpPost("/Installed/{name}")] + [HttpPost("Packages/Installed/{name}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = Policies.RequiresElevation)] @@ -111,7 +111,7 @@ namespace Jellyfin.Api.Controllers /// Installation Id. /// Installation cancelled. /// A on successfully cancelling a package installation. - [HttpDelete("/Installing/{packageId}")] + [HttpDelete("Packages/Installing/{packageId}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult CancelPackageInstallation( @@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers /// /// Package repositories returned. /// An containing the list of package repositories. - [HttpGet("/Repositories")] + [HttpGet("Repositories")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetRepositories() @@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers /// The list of package repositories. /// Package repositories saved. /// A . - [HttpOptions("/Repositories")] + [HttpOptions("Repositories")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SetRepositories([FromBody] List repositoryInfos) diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 3ebd003f1..0422bfe72 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -19,6 +19,7 @@ namespace Jellyfin.Api.Controllers /// /// Playstate controller. /// + [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] public class PlaystateController : BaseJellyfinApiController { @@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The date the item was played. /// Item marked as played. /// An containing the . - [HttpPost("/Users/{userId}/PlayedItems/{itemId}")] + [HttpPost("Users/{userId}/PlayedItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult MarkPlayedItem( [FromRoute] Guid userId, @@ -93,7 +94,7 @@ namespace Jellyfin.Api.Controllers /// Item id. /// Item marked as unplayed. /// A containing the . - [HttpDelete("/Users/{userId}/PlayedItem/{itemId}")] + [HttpDelete("Users/{userId}/PlayedItem/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId) { @@ -115,7 +116,7 @@ namespace Jellyfin.Api.Controllers /// The playback start info. /// Playback start recorded. /// A . - [HttpPost("/Sessions/Playing")] + [HttpPost("Sessions/Playing")] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo) { @@ -131,7 +132,7 @@ namespace Jellyfin.Api.Controllers /// The playback progress info. /// Playback progress recorded. /// A . - [HttpPost("/Sessions/Playing/Progress")] + [HttpPost("Sessions/Playing/Progress")] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo) { @@ -147,7 +148,7 @@ namespace Jellyfin.Api.Controllers /// Playback session id. /// Playback session pinged. /// A . - [HttpPost("/Sessions/Playing/Ping")] + [HttpPost("Sessions/Playing/Ping")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PingPlaybackSession([FromQuery] string playSessionId) { @@ -161,7 +162,7 @@ namespace Jellyfin.Api.Controllers /// The playback stop info. /// Playback stop recorded. /// A . - [HttpPost("/Sessions/Playing/Stopped")] + [HttpPost("Sessions/Playing/Stopped")] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task ReportPlaybackStopped([FromBody] PlaybackStopInfo playbackStopInfo) { @@ -190,7 +191,7 @@ namespace Jellyfin.Api.Controllers /// Indicates if the client can seek. /// Play start recorded. /// A . - [HttpPost("/Users/{userId}/PlayingItems/{itemId}")] + [HttpPost("Users/{userId}/PlayingItems/{itemId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] public async Task OnPlaybackStart( @@ -240,7 +241,7 @@ namespace Jellyfin.Api.Controllers /// Indicates if the player is muted. /// Play progress recorded. /// A . - [HttpPost("/Users/{userId}/PlayingItems/{itemId}/Progress")] + [HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] public async Task OnPlaybackProgress( @@ -292,7 +293,7 @@ namespace Jellyfin.Api.Controllers /// The play session id. /// Playback stop recorded. /// A . - [HttpDelete("/Users/{userId}/PlayingItems/{itemId}")] + [HttpDelete("Users/{userId}/PlayingItems/{itemId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] public async Task OnPlaybackStopped( diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 770d74838..fe10f0f1b 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers /// Not Implemented. /// This endpoint is not implemented. [Obsolete("Paid plugins are not supported")] - [HttpGet("/Registrations/{name}")] + [HttpGet("Registrations/{name}")] [ProducesResponseType(StatusCodes.Status501NotImplemented)] public ActionResult GetRegistration([FromRoute] string? name) { diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 1b300e0d8..3e6f577f1 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -23,6 +23,7 @@ namespace Jellyfin.Api.Controllers /// /// The session controller. /// + [Route("")] public class SessionController : BaseJellyfinApiController { private readonly ISessionManager _sessionManager; @@ -57,7 +58,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Filter by sessions that were active in the last n seconds. /// List of sessions returned. /// An with the available sessions. - [HttpGet("/Sessions")] + [HttpGet("Sessions")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSessions( @@ -120,7 +121,7 @@ namespace Jellyfin.Api.Controllers /// The name of the item. /// Instruction sent to session. /// A . - [HttpPost("/Sessions/{sessionId}/Viewing")] + [HttpPost("Sessions/{sessionId}/Viewing")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DisplayContent( [FromRoute] string? sessionId, @@ -154,7 +155,7 @@ namespace Jellyfin.Api.Controllers /// The . /// Instruction sent to session. /// A . - [HttpPost("/Sessions/{sessionId}/Playing")] + [HttpPost("Sessions/{sessionId}/Playing")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult Play( [FromRoute] string? sessionId, @@ -188,7 +189,7 @@ namespace Jellyfin.Api.Controllers /// The . /// Playstate command sent to session. /// A . - [HttpPost("/Sessions/{sessionId}/Playing/{command}")] + [HttpPost("Sessions/{sessionId}/Playing/{command}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendPlaystateCommand( [FromRoute] string? sessionId, @@ -210,7 +211,7 @@ namespace Jellyfin.Api.Controllers /// The command to send. /// System command sent to session. /// A . - [HttpPost("/Sessions/{sessionId}/System/{command}")] + [HttpPost("Sessions/{sessionId}/System/{command}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendSystemCommand( [FromRoute] string? sessionId, @@ -241,7 +242,7 @@ namespace Jellyfin.Api.Controllers /// The command to send. /// General command sent to session. /// A . - [HttpPost("/Sessions/{sessionId}/Command/{command}")] + [HttpPost("Sessions/{sessionId}/Command/{command}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendGeneralCommand( [FromRoute] string? sessionId, @@ -267,7 +268,7 @@ namespace Jellyfin.Api.Controllers /// The . /// Full general command sent to session. /// A . - [HttpPost("/Sessions/{sessionId}/Command")] + [HttpPost("Sessions/{sessionId}/Command")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendFullGeneralCommand( [FromRoute] string? sessionId, @@ -300,7 +301,7 @@ namespace Jellyfin.Api.Controllers /// The message timeout. If omitted the user will have to confirm viewing the message. /// Message sent. /// A . - [HttpPost("/Sessions/{sessionId}/Message")] + [HttpPost("Sessions/{sessionId}/Message")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendMessageCommand( [FromRoute] string? sessionId, @@ -327,7 +328,7 @@ namespace Jellyfin.Api.Controllers /// The user id. /// User added to session. /// A . - [HttpPost("/Sessions/{sessionId}/User/{userId}")] + [HttpPost("Sessions/{sessionId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddUserToSession( [FromRoute] string? sessionId, @@ -344,7 +345,7 @@ namespace Jellyfin.Api.Controllers /// The user id. /// User removed from session. /// A . - [HttpDelete("/Sessions/{sessionId}/User/{userId}")] + [HttpDelete("Sessions/{sessionId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RemoveUserFromSession( [FromRoute] string? sessionId, @@ -365,7 +366,7 @@ namespace Jellyfin.Api.Controllers /// Determines whether the device supports a unique identifier. /// Capabilities posted. /// A . - [HttpPost("/Sessions/Capabilities")] + [HttpPost("Sessions/Capabilities")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostCapabilities( [FromQuery] string? id, @@ -398,7 +399,7 @@ namespace Jellyfin.Api.Controllers /// The . /// Capabilities updated. /// A . - [HttpPost("/Sessions/Capabilities/Full")] + [HttpPost("Sessions/Capabilities/Full")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostFullCapabilities( [FromQuery] string? id, @@ -421,7 +422,7 @@ namespace Jellyfin.Api.Controllers /// The item id. /// Session reported to server. /// A . - [HttpPost("/Sessions/Viewing")] + [HttpPost("Sessions/Viewing")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult ReportViewing( [FromQuery] string? sessionId, @@ -438,7 +439,7 @@ namespace Jellyfin.Api.Controllers /// /// Session end reported to server. /// A . - [HttpPost("/Sessions/Logout")] + [HttpPost("Sessions/Logout")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult ReportSessionEnded() { @@ -453,7 +454,7 @@ namespace Jellyfin.Api.Controllers /// /// Auth providers retrieved. /// An with the auth providers. - [HttpGet("/Auth/Providers")] + [HttpGet("Auth/Providers")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetAuthProviders() { @@ -465,7 +466,7 @@ namespace Jellyfin.Api.Controllers /// /// Password reset providers retrieved. /// An with the password reset providers. - [HttpGet("/Auto/PasswordResetProviders")] + [HttpGet("Auto/PasswordResetProviders")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetPasswordResetProviders() { diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index f8c19d15c..22144ab1a 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -30,6 +30,7 @@ namespace Jellyfin.Api.Controllers /// /// Subtitle controller. /// + [Route("")] public class SubtitleController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -80,7 +81,7 @@ namespace Jellyfin.Api.Controllers /// Subtitle deleted. /// Item not found. /// A . - [HttpDelete("/Videos/{itemId}/Subtitles/{index}")] + [HttpDelete("Videos/{itemId}/Subtitles/{index}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -107,7 +108,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Only show subtitles which are a perfect match. /// Subtitles retrieved. /// An array of . - [HttpGet("/Items/{itemId}/RemoteSearch/Subtitles/{language}")] + [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> SearchRemoteSubtitles( @@ -127,7 +128,7 @@ namespace Jellyfin.Api.Controllers /// The subtitle id. /// Subtitle downloaded. /// A . - [HttpPost("/Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] + [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task DownloadRemoteSubtitles( @@ -157,7 +158,7 @@ namespace Jellyfin.Api.Controllers /// The item id. /// File returned. /// A with the subtitle file. - [HttpGet("/Providers/Subtitles/Subtitles/{id}")] + [HttpGet("Providers/Subtitles/Subtitles/{id}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] @@ -181,8 +182,8 @@ namespace Jellyfin.Api.Controllers /// Optional. The start position of the subtitle in ticks. /// File returned. /// A with the subtitle file. - [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] - [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")] + [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] + [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetSubtitle( [FromRoute, Required] Guid itemId, @@ -247,7 +248,7 @@ namespace Jellyfin.Api.Controllers /// The subtitle segment length. /// Subtitle playlist retrieved. /// A with the HLS subtitle playlist. - [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] + [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 55759f316..42db6b6a1 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -16,6 +16,7 @@ namespace Jellyfin.Api.Controllers /// /// The suggestions controller. /// + [Route("")] public class SuggestionsController : BaseJellyfinApiController { private readonly IDtoService _dtoService; @@ -49,7 +50,7 @@ namespace Jellyfin.Api.Controllers /// Whether to enable the total record count. /// Suggestions returned. /// A with the suggestions. - [HttpGet("/Users/{userId}/Suggestions")] + [HttpGet("Users/{userId}/Suggestions")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSuggestions( [FromRoute] Guid userId, diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index fbab7948f..5157b08ae 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -108,7 +108,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Enable the total record count. /// Optional, include image information in output. /// A with the trailers. - [HttpGet("/Trailers")] + [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetTrailers( [FromQuery] Guid? userId, diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 5a9bec2b0..75df16aa7 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -19,6 +19,7 @@ namespace Jellyfin.Api.Controllers /// /// The universal audio controller. /// + [Route("")] public class UniversalAudioController : BaseJellyfinApiController { private readonly IAuthorizationContext _authorizationContext; @@ -68,10 +69,10 @@ namespace Jellyfin.Api.Controllers /// Audio stream returned. /// Redirected to remote audio stream. /// A containing the audio file. - [HttpGet("/Audio/{itemId}/universal")] - [HttpGet("/Audio/{itemId}/{universal=universal}.{container?}", Name = "GetUniversalAudioStream_2")] - [HttpHead("/Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] - [HttpHead("/Audio/{itemId}/{universal=universal}.{container?}", Name = "HeadUniversalAudioStream_2")] + [HttpGet("Audio/{itemId}/universal")] + [HttpGet("Audio/{itemId}/{universal=universal}.{container?}", Name = "GetUniversalAudioStream_2")] + [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] + [HttpHead("Audio/{itemId}/{universal=universal}.{container?}", Name = "HeadUniversalAudioStream_2")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status302Found)] diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 482baf641..2ce5c7e56 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -450,7 +450,7 @@ namespace Jellyfin.Api.Controllers /// The create user by name request body. /// User created. /// An of the new user. - [HttpPost("/Users/New")] + [HttpPost("New")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CreateUserByName([FromBody] CreateUserByName request) diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index cedda3b9d..f55ff6f3d 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -25,6 +25,7 @@ namespace Jellyfin.Api.Controllers /// /// User library controller. /// + [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] public class UserLibraryController : BaseJellyfinApiController { @@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers /// Item id. /// Item returned. /// An containing the d item. - [HttpGet("/Users/{userId}/Items/{itemId}")] + [HttpGet("Users/{userId}/Items/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetItem([FromRoute] Guid userId, [FromRoute] Guid itemId) { @@ -90,7 +91,7 @@ namespace Jellyfin.Api.Controllers /// User id. /// Root folder returned. /// An containing the user's root folder. - [HttpGet("/Users/{userId}/Items/Root")] + [HttpGet("Users/{userId}/Items/Root")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetRootFolder([FromRoute] Guid userId) { @@ -107,7 +108,7 @@ namespace Jellyfin.Api.Controllers /// Item id. /// Intros returned. /// An containing the intros to play. - [HttpGet("/Users/{userId}/Items/{itemId}/Intros")] + [HttpGet("Users/{userId}/Items/{itemId}/Intros")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetIntros([FromRoute] Guid userId, [FromRoute] Guid itemId) { @@ -135,7 +136,7 @@ namespace Jellyfin.Api.Controllers /// Item id. /// Item marked as favorite. /// An containing the . - [HttpPost("/Users/{userId}/FavoriteItems/{itemId}")] + [HttpPost("Users/{userId}/FavoriteItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult MarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId) { @@ -149,7 +150,7 @@ namespace Jellyfin.Api.Controllers /// Item id. /// Item unmarked as favorite. /// An containing the . - [HttpDelete("/Users/{userId}/FavoriteItems/{itemId}")] + [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UnmarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId) { @@ -163,7 +164,7 @@ namespace Jellyfin.Api.Controllers /// Item id. /// Personal rating removed. /// An containing the . - [HttpDelete("/Users/{userId}/Items/{itemId}/Rating")] + [HttpDelete("Users/{userId}/Items/{itemId}/Rating")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult DeleteUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId) { @@ -178,7 +179,7 @@ namespace Jellyfin.Api.Controllers /// Whether this is likes. /// Item rating updated. /// An containing the . - [HttpPost("/Users/{userId}/Items/{itemId}/Rating")] + [HttpPost("Users/{userId}/Items/{itemId}/Rating")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UpdateUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId, [FromQuery] bool? likes) { @@ -192,7 +193,7 @@ namespace Jellyfin.Api.Controllers /// Item id. /// An containing the item's local trailers. /// The items local trailers. - [HttpGet("/Users/{userId}/Items/{itemId}/LocalTrailers")] + [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetLocalTrailers([FromRoute] Guid userId, [FromRoute] Guid itemId) { @@ -227,7 +228,7 @@ namespace Jellyfin.Api.Controllers /// Item id. /// Special features returned. /// An containing the special features. - [HttpGet("/Users/{userId}/Items/{itemId}/SpecialFeatures")] + [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSpecialFeatures([FromRoute] Guid userId, [FromRoute] Guid itemId) { @@ -260,7 +261,7 @@ namespace Jellyfin.Api.Controllers /// Whether or not to group items into a parent container. /// Latest media returned. /// An containing the latest media. - [HttpGet("/Users/{userId}/Items/Latest")] + [HttpGet("Users/{userId}/Items/Latest")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetLatestMedia( [FromRoute] Guid userId, diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index f4bd451ef..6df7cc779 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -21,6 +21,7 @@ namespace Jellyfin.Api.Controllers /// /// User views controller. /// + [Route("")] public class UserViewsController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -60,7 +61,7 @@ namespace Jellyfin.Api.Controllers /// Whether or not to include hidden content. /// User views returned. /// An containing the user views. - [HttpGet("/Users/{userId}/Views")] + [HttpGet("Users/{userId}/Views")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetUserViews( [FromRoute] Guid userId, @@ -122,7 +123,7 @@ namespace Jellyfin.Api.Controllers /// An containing the user view grouping options /// or a if user not found. /// - [HttpGet("/Users/{userId}/GroupingOptions")] + [HttpGet("Users/{userId}/GroupingOptions")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetGroupingOptions([FromRoute] Guid userId) diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 8520dd163..76188f46d 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -30,6 +30,7 @@ namespace Jellyfin.Api.Controllers /// /// The video hls controller. /// + [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] public class VideoHlsController : BaseJellyfinApiController { @@ -158,7 +159,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Whether to enable subtitles in the manifest. /// Hls live stream retrieved. /// A containing the hls file. - [HttpGet("/Videos/{itemId}/live.m3u8")] + [HttpGet("Videos/{itemId}/live.m3u8")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetLiveHlsStream( [FromRoute] Guid itemId, @@ -271,7 +272,7 @@ namespace Jellyfin.Api.Controllers }; var cancellationTokenSource = new CancellationTokenSource(); - var state = await StreamingHelpers.GetStreamingState( + using var state = await StreamingHelpers.GetStreamingState( streamingRequest, Request, _authContext, -- cgit v1.2.3 From fffa94fc33b923863e7cfe0d57d85ae86206975e Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 6 Aug 2020 08:17:45 -0600 Subject: Apply fixes from review --- .../FirstTimeSetupOrDefaultHandler.cs | 56 ++++++++++++++++++++++ .../FirstTimeSetupOrDefaultRequirement.cs | 11 +++++ ...IgnoreParentalControlOrFirstTimeSetupHandler.cs | 51 ++++++++++++++++++++ ...reParentalControlOrFirstTimeSetupRequirement.cs | 11 +++++ .../IgnoreParentalControlHandler.cs | 42 ++++++++++++++++ .../IgnoreParentalControlRequirement.cs | 11 +++++ .../IgnoreSchedulePolicy/IgnoreScheduleHandler.cs | 42 ---------------- .../IgnoreScheduleRequirement.cs | 11 ----- .../LocalAccessOrRequiresElevationHandler.cs | 46 ++++++++++++++++++ .../LocalAccessOrRequiresElevationRequirement.cs | 11 +++++ Jellyfin.Api/Constants/Policies.cs | 19 +++++++- Jellyfin.Api/Controllers/ApiKeyController.cs | 2 +- Jellyfin.Api/Controllers/CollectionController.cs | 5 +- .../Controllers/ConfigurationController.cs | 6 +-- Jellyfin.Api/Controllers/DevicesController.cs | 14 +++--- .../Controllers/DisplayPreferencesController.cs | 7 ++- Jellyfin.Api/Controllers/EnvironmentController.cs | 8 ++-- Jellyfin.Api/Controllers/ImageByNameController.cs | 11 +++-- Jellyfin.Api/Controllers/ImageController.cs | 2 + Jellyfin.Api/Controllers/InstantMixController.cs | 3 +- Jellyfin.Api/Controllers/ItemLookupController.cs | 21 ++++---- Jellyfin.Api/Controllers/ItemUpdateController.cs | 6 +-- Jellyfin.Api/Controllers/LibraryController.cs | 9 ++-- .../Controllers/LibraryStructureController.cs | 4 +- Jellyfin.Api/Controllers/LocalizationController.cs | 2 +- Jellyfin.Api/Controllers/MediaInfoController.cs | 5 +- .../Controllers/NotificationsController.cs | 14 ++++-- Jellyfin.Api/Controllers/PackageController.cs | 1 - Jellyfin.Api/Controllers/PersonsController.cs | 3 ++ Jellyfin.Api/Controllers/PlaylistsController.cs | 4 +- Jellyfin.Api/Controllers/PluginsController.cs | 4 +- Jellyfin.Api/Controllers/RemoteImageController.cs | 6 +-- .../Controllers/ScheduledTasksController.cs | 10 ++-- Jellyfin.Api/Controllers/SessionController.cs | 49 ++++++++++++------- Jellyfin.Api/Controllers/SubtitleController.cs | 6 +-- Jellyfin.Api/Controllers/SyncPlayController.cs | 2 +- Jellyfin.Api/Controllers/SystemController.cs | 7 +-- Jellyfin.Api/Controllers/TimeSyncController.cs | 4 +- Jellyfin.Api/Controllers/TvShowsController.cs | 13 ++--- Jellyfin.Api/Controllers/UserController.cs | 11 ++--- .../Controllers/VideoAttachmentsController.cs | 10 ++-- Jellyfin.Api/Controllers/VideosController.cs | 4 +- Jellyfin.Api/Controllers/YearsController.cs | 3 ++ .../Models/StartupDtos/StartupConfigurationDto.cs | 2 +- .../Extensions/ApiServiceCollectionExtensions.cs | 35 ++++++++++++-- .../IgnoreScheduleHandlerTests.cs | 8 ++-- 46 files changed, 432 insertions(+), 180 deletions(-) create mode 100644 Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs create mode 100644 Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs create mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs create mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs create mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs create mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs delete mode 100644 Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs delete mode 100644 Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs create mode 100644 Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs create mode 100644 Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs new file mode 100644 index 000000000..67fb2b79a --- /dev/null +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy +{ + /// + /// Authorization handler for requiring first time setup or elevated privileges. + /// + public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler + { + private readonly IConfigurationManager _configurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public FirstTimeSetupOrDefaultHandler( + IConfigurationManager configurationManager, + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + _configurationManager = configurationManager; + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrElevatedRequirement) + { + if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) + { + context.Succeed(firstTimeSetupOrElevatedRequirement); + return Task.CompletedTask; + } + + var validated = ValidateClaims(context.User); + if (validated) + { + context.Succeed(firstTimeSetupOrElevatedRequirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs new file mode 100644 index 000000000..23d7ee01f --- /dev/null +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy +{ + /// + /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler. + /// + public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs new file mode 100644 index 000000000..6c9258b3d --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy +{ + /// + /// Escape schedule controls handler. + /// + public class IgnoreParentalControlOrFirstTimeSetupHandler : BaseAuthorizationHandler + { + private readonly IConfigurationManager _configurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public IgnoreParentalControlOrFirstTimeSetupHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor, + IConfigurationManager configurationManager) + : base(userManager, networkManager, httpContextAccessor) + { + _configurationManager = configurationManager; + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement) + { + var validated = ValidateClaims(context.User, ignoreSchedule: true); + if (validated || !_configurationManager.CommonConfiguration.IsStartupWizardCompleted) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs new file mode 100644 index 000000000..36ded0625 --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy +{ + /// + /// Escape schedule controls requirement. + /// + public class IgnoreParentalControlOrFirstTimeSetupRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs new file mode 100644 index 000000000..5213bc4cb --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy +{ + /// + /// Escape schedule controls handler. + /// + public class IgnoreParentalControlHandler : BaseAuthorizationHandler + { + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public IgnoreParentalControlHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement) + { + var validated = ValidateClaims(context.User, ignoreSchedule: true); + if (!validated) + { + context.Fail(); + return Task.CompletedTask; + } + + context.Succeed(requirement); + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs new file mode 100644 index 000000000..cdad74270 --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy +{ + /// + /// Escape schedule controls requirement. + /// + public class IgnoreParentalControlRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs deleted file mode 100644 index 9afa0b28f..000000000 --- a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy -{ - /// - /// Escape schedule controls handler. - /// - public class IgnoreScheduleHandler : BaseAuthorizationHandler - { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public IgnoreScheduleHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement) - { - var validated = ValidateClaims(context.User, ignoreSchedule: true); - if (!validated) - { - context.Fail(); - return Task.CompletedTask; - } - - context.Succeed(requirement); - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs deleted file mode 100644 index d5bb61ce6..000000000 --- a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy -{ - /// - /// Escape schedule controls requirement. - /// - public class IgnoreScheduleRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs new file mode 100644 index 000000000..d9ab8aa68 --- /dev/null +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy +{ + /// + /// Local access handler. + /// + public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler + { + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public LocalAccessOrRequiresElevationHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement) + { + var validated = ValidateClaims(context.User, localAccessOnly: true); + + if (validated || context.User.IsInRole(UserRoles.Administrator)) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs new file mode 100644 index 000000000..ad96caa81 --- /dev/null +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy +{ + /// + /// The local access authorization requirement. + /// + public class LocalAccessOrRequiresElevationRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index 851b56d73..8de637c4e 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -13,7 +13,7 @@ namespace Jellyfin.Api.Constants /// /// Policy name for requiring first time setup or elevated privileges. /// - public const string FirstTimeSetupOrElevated = "FirstTimeOrElevated"; + public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated"; /// /// Policy name for requiring elevated privileges. @@ -28,11 +28,26 @@ namespace Jellyfin.Api.Constants /// /// Policy name for escaping schedule controls. /// - public const string IgnoreSchedule = "IgnoreSchedule"; + public const string IgnoreParentalControl = "IgnoreParentalControl"; /// /// Policy name for requiring download permission. /// public const string Download = "Download"; + + /// + /// Policy name for requiring first time setup or default permissions. + /// + public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault"; + + /// + /// Policy name for requiring local access or elevated privileges. + /// + public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation"; + + /// + /// Policy name for escaping schedule controls or requiring first time setup. + /// + public const string IgnoreParentalControlOrFirstTimeSetup = "IgnoreParentalControlOrFirstTimeSetup"; } } diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index ccb7f47f0..0e28d4c47 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Keys/{key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RevokeKey([FromRoute] string? key) + public ActionResult RevokeKey([FromRoute, Required] string? key) { _sessionManager.RevokeToken(key); return NoContent(); diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index b63fc5ab1..53821a188 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; @@ -86,7 +87,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds) + public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds) { _collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true)); return NoContent(); @@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpDelete("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds) + public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds) { _collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true)); return NoContent(); diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 7d262ed59..019703dae 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Constants; @@ -9,7 +10,6 @@ using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Configuration")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) + public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration) { _configurationManager.ReplaceConfiguration(configuration); return NoContent(); @@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("MediaEncoder/Path")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) + public ActionResult UpdateMediaEncoderPath([FromForm, Required] MediaEncoderPathDto mediaEncoderPath) { _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); return NoContent(); diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 3cf7b3378..23d10e215 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Constants; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Security; @@ -8,7 +9,6 @@ using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery, Required] Guid? userId) { var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; return _deviceManager.GetDevices(deviceQuery); @@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceInfo([FromQuery, BindRequired] string? id) + public ActionResult GetDeviceInfo([FromQuery, Required] string? id) { var deviceInfo = _deviceManager.GetDevice(id); if (deviceInfo == null) @@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceOptions([FromQuery, BindRequired] string? id) + public ActionResult GetDeviceOptions([FromQuery, Required] string? id) { var deviceInfo = _deviceManager.GetDeviceOptions(id); if (deviceInfo == null) @@ -111,8 +111,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateDeviceOptions( - [FromQuery, BindRequired] string? id, - [FromBody, BindRequired] DeviceOptions deviceOptions) + [FromQuery, Required] string? id, + [FromBody, Required] DeviceOptions deviceOptions) { var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); if (existingDeviceOptions == null) @@ -134,7 +134,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteDevice([FromQuery, BindRequired] string? id) + public ActionResult DeleteDevice([FromQuery, Required] string? id) { var existingDevice = _deviceManager.GetDevice(id); if (existingDevice == null) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 62f6097f3..c547d0cde 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -11,7 +11,6 @@ using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -99,9 +98,9 @@ namespace Jellyfin.Api.Controllers [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")] public ActionResult UpdateDisplayPreferences( [FromRoute] string? displayPreferencesId, - [FromQuery, BindRequired] Guid userId, - [FromQuery, BindRequired] string? client, - [FromBody, BindRequired] DisplayPreferencesDto displayPreferences) + [FromQuery, Required] Guid userId, + [FromQuery, Required] string? client, + [FromBody, Required] DisplayPreferencesDto displayPreferences) { HomeSectionType[] defaults = { diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 719bb7d86..64670f7d8 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using Jellyfin.Api.Constants; @@ -8,7 +9,6 @@ using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers @@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("DirectoryContents")] [ProducesResponseType(StatusCodes.Status200OK)] public IEnumerable GetDirectoryContents( - [FromQuery, BindRequired] string path, + [FromQuery, Required] string path, [FromQuery] bool includeFiles = false, [FromQuery] bool includeDirectories = false) { @@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("ValidatePath")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult ValidatePath([FromBody, BindRequired] ValidatePathDto validatePathDto) + public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto) { if (validatePathDto.IsFile.HasValue) { @@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers /// Parent path. [HttpGet("ParentPath")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetParentPath([FromQuery, BindRequired] string path) + public ActionResult GetParentPath([FromQuery, Required] string path) { string? parent = Path.GetDirectoryName(path); if (string.IsNullOrEmpty(parent)) diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 5244c35b8..528590536 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net.Mime; @@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetGeneralImage([FromRoute] string? name, [FromRoute] string? type) + public ActionResult GetGeneralImage([FromRoute, Required] string? name, [FromRoute, Required] string? type) { var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase) ? "folder" @@ -110,8 +111,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetRatingImage( - [FromRoute] string? theme, - [FromRoute] string? name) + [FromRoute, Required] string? theme, + [FromRoute, Required] string? name) { return GetImageFile(_applicationPaths.RatingsPath, theme, name); } @@ -143,8 +144,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetMediaInfoImage( - [FromRoute] string? theme, - [FromRoute] string? name) + [FromRoute, Required] string? theme, + [FromRoute, Required] string? name) { return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name); } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 360164ad4..410456a25 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -84,6 +84,7 @@ namespace Jellyfin.Api.Controllers /// A . [HttpPost("Users/{userId}/Images/{imageType}")] [HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] @@ -259,6 +260,7 @@ namespace Jellyfin.Api.Controllers /// Item not found. /// The list of image infos on success, or if item not found. [HttpGet("Items/{itemId}/Images")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetItemImageInfos([FromRoute] Guid itemId) diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 8ca232cef..73bd30c4d 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; @@ -174,7 +175,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("MusicGenres/{name}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromMusicGenre( - [FromRoute] string? name, + [FromRoute, Required] string? name, [FromQuery] Guid? userId, [FromQuery] int? limit, [FromQuery] string? fields, diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index 0d9dffbfe..c9ad15bab 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -22,7 +22,6 @@ using MediaBrowser.Model.Providers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers @@ -94,7 +93,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Movie")] - public async Task>> GetMovieRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -111,7 +110,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Trailer")] - public async Task>> GetTrailerRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -128,7 +127,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/MusicVideo")] - public async Task>> GetMusicVideoRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -145,7 +144,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Series")] - public async Task>> GetSeriesRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -162,7 +161,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/BoxSet")] - public async Task>> GetBoxSetRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -179,7 +178,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/MusicArtist")] - public async Task>> GetMusicArtistRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -196,7 +195,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/MusicAlbum")] - public async Task>> GetMusicAlbumRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -214,7 +213,7 @@ namespace Jellyfin.Api.Controllers /// [HttpPost("Items/RemoteSearch/Person")] [Authorize(Policy = Policies.RequiresElevation)] - public async Task>> GetPersonRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -231,7 +230,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Book")] - public async Task>> GetBookRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -296,7 +295,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] public async Task ApplySearchCriteria( [FromRoute] Guid itemId, - [FromBody, BindRequired] RemoteSearchResult searchResult, + [FromBody, Required] RemoteSearchResult searchResult, [FromQuery] bool replaceAllImages = true) { var item = _libraryManager.GetItemById(itemId); diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index a5d9d36a3..4b40c6ada 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using Jellyfin.Api.Constants; @@ -17,7 +18,6 @@ using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -67,7 +67,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Items/{itemId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, BindRequired] BaseItemDto request) + public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -194,7 +194,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Items/{itemId}/ContentType")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string? contentType) + public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, Required] string? contentType) { var item = _libraryManager.GetItemById(itemId); if (item == null) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 4731a5c8b..4548e202a 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Linq; @@ -32,7 +33,6 @@ using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; using Book = MediaBrowser.Controller.Entities.Book; using Movie = Jellyfin.Data.Entities.Movie; @@ -597,7 +597,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Library/Media/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostUpdatedMedia([FromBody, BindRequired] MediaUpdateInfoDto[] updates) + public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates) { foreach (var item in updates) { @@ -685,6 +685,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")] [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")] [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarItems( [FromRoute] Guid itemId, @@ -736,11 +737,11 @@ namespace Jellyfin.Api.Controllers /// Library options info returned. /// Library options info. [HttpGet("Libraries/AvailableOptions")] - [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetLibraryOptionsInfo( [FromQuery] string? libraryContentType, - [FromQuery] bool isNewLibrary = false) + [FromQuery] bool isNewLibrary) { var result = new LibraryOptionsResultDto(); diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index ca150f3f2..cdab4f356 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Linq; @@ -17,7 +18,6 @@ using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -204,7 +204,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Paths")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddMediaPath( - [FromBody, BindRequired] MediaPathDto mediaPathDto, + [FromBody, Required] MediaPathDto mediaPathDto, [FromQuery] bool refreshLibrary = false) { _libraryMonitor.Stop(); diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs index 1466dd3ec..ef2e7e8b1 100644 --- a/Jellyfin.Api/Controllers/LocalizationController.cs +++ b/Jellyfin.Api/Controllers/LocalizationController.cs @@ -11,7 +11,7 @@ namespace Jellyfin.Api.Controllers /// /// Localization controller. /// - [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] public class LocalizationController : BaseJellyfinApiController { private readonly ILocalizationManager _localization; diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index 242cbf191..517113074 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Net.Mime; @@ -91,7 +92,7 @@ namespace Jellyfin.Api.Controllers /// A containing a with the playback information. [HttpGet("Items/{itemId}/PlaybackInfo")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid? userId) + public async Task> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId) { return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false); } @@ -281,7 +282,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("LiveStreams/Close")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CloseLiveStream([FromQuery] string? liveStreamId) + public ActionResult CloseLiveStream([FromQuery, Required] string? liveStreamId) { _mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult(); return NoContent(); diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 1bb39b5f7..47ce48b2d 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; +using Jellyfin.Api.Constants; using Jellyfin.Api.Models.NotificationDtos; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Notifications; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Notifications; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -16,6 +19,7 @@ namespace Jellyfin.Api.Controllers /// /// The notification controller. /// + [Authorize(Policy = Policies.DefaultAuthorization)] public class NotificationsController : BaseJellyfinApiController { private readonly INotificationManager _notificationManager; @@ -83,19 +87,19 @@ namespace Jellyfin.Api.Controllers /// /// Sends a notification to all admins. /// - /// The name of the notification. - /// The description of the notification. /// The URL of the notification. /// The level of the notification. + /// The name of the notification. + /// The description of the notification. /// Notification sent. /// A . [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult CreateAdminNotification( - [FromQuery] string? name, - [FromQuery] string? description, [FromQuery] string? url, - [FromQuery] NotificationLevel? level) + [FromQuery] NotificationLevel? level, + [FromQuery, Required] string name = "", + [FromQuery, Required] string description = "") { var notification = new NotificationRequest { diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 06c4213fb..3d6a87909 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -127,7 +127,6 @@ namespace Jellyfin.Api.Controllers /// Package repositories returned. /// An containing the list of package repositories. [HttpGet("Repositories")] - [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetRepositories() { diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 23cc23ce7..b6ccec666 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Linq; +using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Data.Entities; @@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers /// /// Persons controller. /// + [Authorize(Policy = Policies.DefaultAuthorization)] public class PersonsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index cf4660494..12c87d7c3 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; @@ -14,7 +15,6 @@ using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CreatePlaylist( - [FromBody, BindRequired] CreatePlaylistDto createPlaylistRequest) + [FromBody, Required] CreatePlaylistDto createPlaylistRequest) { Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids); var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index fe10f0f1b..b2f34680b 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -13,7 +14,6 @@ using MediaBrowser.Model.Plugins; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("SecurityInfo")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdatePluginSecurityInfo([FromBody, BindRequired] PluginSecurityInfo pluginSecurityInfo) + public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo) { return NoContent(); } diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 50a161ef6..baa3d80ac 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net.Mime; @@ -18,7 +19,6 @@ using MediaBrowser.Model.Providers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetRemoteImage([FromQuery, BindRequired] string imageUrl) + public async Task GetRemoteImage([FromQuery, Required] string imageUrl) { var urlHash = imageUrl.GetMD5(); var pointerCachePath = GetFullCachePath(urlHash.ToString()); @@ -209,7 +209,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DownloadRemoteImage( [FromRoute] Guid itemId, - [FromQuery, BindRequired] ImageType type, + [FromQuery, Required] ImageType type, [FromQuery] string? imageUrl) { var item = _libraryManager.GetItemById(itemId); diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index 3df325e3b..e672070c0 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using MediaBrowser.Model.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("{taskId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetTask([FromRoute] string? taskId) + public ActionResult GetTask([FromRoute, Required] string? taskId) { var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase)); @@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Running/{taskId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult StopTask([FromRoute] string? taskId) + public ActionResult StopTask([FromRoute, Required] string? taskId) { var task = _taskManager.ScheduledTasks.FirstOrDefault(o => o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); @@ -144,8 +144,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateTask( - [FromRoute] string? taskId, - [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos) + [FromRoute, Required] string? taskId, + [FromBody, Required] TaskTriggerInfo[] triggerInfos) { var task = _taskManager.ScheduledTasks.FirstOrDefault(o => o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 3e6f577f1..48b57bdb7 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -122,12 +122,13 @@ namespace Jellyfin.Api.Controllers /// Instruction sent to session. /// A . [HttpPost("Sessions/{sessionId}/Viewing")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DisplayContent( - [FromRoute] string? sessionId, - [FromQuery] string? itemType, - [FromQuery] string? itemId, - [FromQuery] string? itemName) + [FromRoute, Required] string? sessionId, + [FromQuery, Required] string? itemType, + [FromQuery, Required] string? itemId, + [FromQuery, Required] string? itemName) { var command = new BrowseRequest { @@ -156,9 +157,10 @@ namespace Jellyfin.Api.Controllers /// Instruction sent to session. /// A . [HttpPost("Sessions/{sessionId}/Playing")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult Play( - [FromRoute] string? sessionId, + [FromRoute, Required] string? sessionId, [FromQuery] Guid[] itemIds, [FromQuery] long? startPositionTicks, [FromQuery] PlayCommand playCommand, @@ -190,9 +192,10 @@ namespace Jellyfin.Api.Controllers /// Playstate command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Playing/{command}")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendPlaystateCommand( - [FromRoute] string? sessionId, + [FromRoute, Required] string? sessionId, [FromBody] PlaystateRequest playstateRequest) { _sessionManager.SendPlaystateCommand( @@ -212,10 +215,11 @@ namespace Jellyfin.Api.Controllers /// System command sent to session. /// A . [HttpPost("Sessions/{sessionId}/System/{command}")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendSystemCommand( - [FromRoute] string? sessionId, - [FromRoute] string? command) + [FromRoute, Required] string? sessionId, + [FromRoute, Required] string? command) { var name = command; if (Enum.TryParse(name, true, out GeneralCommandType commandType)) @@ -243,10 +247,11 @@ namespace Jellyfin.Api.Controllers /// General command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Command/{command}")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendGeneralCommand( - [FromRoute] string? sessionId, - [FromRoute] string? command) + [FromRoute, Required] string? sessionId, + [FromRoute, Required] string? command) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); @@ -269,9 +274,10 @@ namespace Jellyfin.Api.Controllers /// Full general command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Command")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendFullGeneralCommand( - [FromRoute] string? sessionId, + [FromRoute, Required] string? sessionId, [FromBody, Required] GeneralCommand command) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); @@ -302,11 +308,12 @@ namespace Jellyfin.Api.Controllers /// Message sent. /// A . [HttpPost("Sessions/{sessionId}/Message")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendMessageCommand( - [FromRoute] string? sessionId, - [FromQuery] string? text, - [FromQuery] string? header, + [FromRoute, Required] string? sessionId, + [FromQuery, Required] string? text, + [FromQuery, Required] string? header, [FromQuery] long? timeoutMs) { var command = new MessageCommand @@ -329,9 +336,10 @@ namespace Jellyfin.Api.Controllers /// User added to session. /// A . [HttpPost("Sessions/{sessionId}/User/{userId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddUserToSession( - [FromRoute] string? sessionId, + [FromRoute, Required] string? sessionId, [FromRoute] Guid userId) { _sessionManager.AddAdditionalUser(sessionId, userId); @@ -346,6 +354,7 @@ namespace Jellyfin.Api.Controllers /// User removed from session. /// A . [HttpDelete("Sessions/{sessionId}/User/{userId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RemoveUserFromSession( [FromRoute] string? sessionId, @@ -367,9 +376,10 @@ namespace Jellyfin.Api.Controllers /// Capabilities posted. /// A . [HttpPost("Sessions/Capabilities")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostCapabilities( - [FromQuery] string? id, + [FromQuery, Required] string? id, [FromQuery] string? playableMediaTypes, [FromQuery] string? supportedCommands, [FromQuery] bool supportsMediaControl = false, @@ -400,9 +410,10 @@ namespace Jellyfin.Api.Controllers /// Capabilities updated. /// A . [HttpPost("Sessions/Capabilities/Full")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostFullCapabilities( - [FromQuery] string? id, + [FromQuery, Required] string? id, [FromBody, Required] ClientCapabilities capabilities) { if (string.IsNullOrWhiteSpace(id)) @@ -423,6 +434,7 @@ namespace Jellyfin.Api.Controllers /// Session reported to server. /// A . [HttpPost("Sessions/Viewing")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult ReportViewing( [FromQuery] string? sessionId, @@ -440,6 +452,7 @@ namespace Jellyfin.Api.Controllers /// Session end reported to server. /// A . [HttpPost("Sessions/Logout")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult ReportSessionEnded() { @@ -455,6 +468,7 @@ namespace Jellyfin.Api.Controllers /// Auth providers retrieved. /// An with the auth providers. [HttpGet("Auth/Providers")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetAuthProviders() { @@ -468,6 +482,7 @@ namespace Jellyfin.Api.Controllers /// An with the password reset providers. [HttpGet("Auto/PasswordResetProviders")] [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.RequiresElevation)] public ActionResult> GetPasswordResetProviders() { return _userManager.GetPasswordResetProviders(); diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index d5633fba5..988acccc3 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> SearchRemoteSubtitles( [FromRoute] Guid itemId, - [FromRoute] string? language, + [FromRoute, Required] string? language, [FromQuery] bool? isPerfectMatch) { var video = (Video)_libraryManager.GetItemById(itemId); @@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task DownloadRemoteSubtitles( [FromRoute] Guid itemId, - [FromRoute] string? subtitleId) + [FromRoute, Required] string? subtitleId) { var video = (Video)_libraryManager.GetItemById(itemId); @@ -162,7 +162,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] - public async Task GetRemoteSubtitles([FromRoute] string? id) + public async Task GetRemoteSubtitles([FromRoute, Required] string? id) { var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 2b1b95b1b..e16a10ba4 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers /// /// Optional. Filter by item id. /// Groups returned. - /// An containing the available SyncPlay groups. + /// An containing the available SyncPlay groups. [HttpGet("List")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> SyncPlayGetGroups([FromQuery] Guid? filterItemId) diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 6f9a75e2f..08f1b421d 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -23,7 +23,6 @@ namespace Jellyfin.Api.Controllers /// /// The system controller. /// - [Route("System")] public class SystemController : BaseJellyfinApiController { private readonly IServerApplicationHost _appHost; @@ -60,8 +59,7 @@ namespace Jellyfin.Api.Controllers /// Information retrieved. /// A with info about the system. [HttpGet("Info")] - [Authorize(Policy = Policies.IgnoreSchedule)] - [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + [Authorize(Policy = Policies.IgnoreParentalControlOrFirstTimeSetup)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetSystemInfo() { @@ -99,8 +97,7 @@ namespace Jellyfin.Api.Controllers /// Server restarted. /// No content. Server restarted. [HttpPost("Restart")] - [Authorize(Policy = Policies.LocalAccessOnly)] - [Authorize(Policy = Policies.RequiresElevation)] + [Authorize(Policy = Policies.LocalAccessOrRequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RestartApplication() { diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs index bbabcd6e6..2dc744e7c 100644 --- a/Jellyfin.Api/Controllers/TimeSyncController.cs +++ b/Jellyfin.Api/Controllers/TimeSyncController.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Api.Controllers /// /// The time sync controller. /// - [Route("GetUtcTime")] + [Route("")] public class TimeSyncController : BaseJellyfinApiController { /// @@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers /// /// Time returned. /// An to sync the client and server time. - [HttpGet] + [HttpGet("GetUtcTime")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public ActionResult GetUtcTime() { diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index d4560dfa2..f463ab889 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using Jellyfin.Api.Constants; @@ -68,7 +69,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("NextUp")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetNextUp( - [FromQuery] Guid? userId, + [FromQuery, Required] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? fields, @@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Upcoming")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetUpcomingEpisodes( - [FromQuery] Guid? userId, + [FromQuery, Required] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? fields, @@ -193,8 +194,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetEpisodes( - [FromRoute] string? seriesId, - [FromQuery] Guid? userId, + [FromRoute, Required] string? seriesId, + [FromQuery, Required] Guid? userId, [FromQuery] string? fields, [FromQuery] int? season, [FromQuery] string? seasonId, @@ -316,8 +317,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetSeasons( - [FromRoute] string? seriesId, - [FromQuery] Guid? userId, + [FromRoute, Required] string? seriesId, + [FromQuery, Required] Guid? userId, [FromQuery] string? fields, [FromQuery] bool? isSpecialSeason, [FromQuery] bool? isMissing, diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 2ce5c7e56..d897f07b7 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -20,7 +20,6 @@ using MediaBrowser.Model.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -106,7 +105,7 @@ namespace Jellyfin.Api.Controllers /// User not found. /// An with information about the user or a if the user was not found. [HttpGet("{userId}")] - [Authorize(Policy = Policies.IgnoreSchedule)] + [Authorize(Policy = Policies.IgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetUserById([FromRoute] Guid userId) @@ -157,8 +156,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> AuthenticateUser( [FromRoute, Required] Guid userId, - [FromQuery, BindRequired] string? pw, - [FromQuery, BindRequired] string? password) + [FromQuery, Required] string? pw, + [FromQuery] string? password) { var user = _userManager.GetUserById(userId); @@ -190,7 +189,7 @@ namespace Jellyfin.Api.Controllers /// A containing an with information about the new session. [HttpPost("AuthenticateByName")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> AuthenticateUserByName([FromBody, BindRequired] AuthenticateUserByName request) + public async Task> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request) { var auth = _authContext.GetAuthorizationInfo(Request); @@ -371,7 +370,7 @@ namespace Jellyfin.Api.Controllers /// User policy update forbidden. /// A indicating success or a or a on failure.. [HttpPost("{userId}/Policy")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index eef0a93cd..09a1c93e6 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -1,12 +1,11 @@ using System; +using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -16,7 +15,6 @@ namespace Jellyfin.Api.Controllers /// Attachments controller. /// [Route("Videos")] - [Authorize(Policy = Policies.DefaultAuthorization)] public class VideoAttachmentsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -49,9 +47,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetAttachment( - [FromRoute] Guid videoId, - [FromRoute] string? mediaSourceId, - [FromRoute] int index) + [FromRoute, Required] Guid videoId, + [FromRoute, Required] string mediaSourceId, + [FromRoute, Required] int index) { try { diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index ebe88a9c0..fe065c76f 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Net.Http; @@ -35,7 +36,6 @@ namespace Jellyfin.Api.Controllers /// /// The videos controller. /// - [Route("Videos")] public class VideosController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public ActionResult MergeVersions([FromQuery] string? itemIds) + public ActionResult MergeVersions([FromQuery, Required] string? itemIds) { var items = RequestHelpers.Split(itemIds, ',', true) .Select(i => _libraryManager.GetItemById(i)) diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index d09b016a9..eb91ac23e 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Data.Entities; @@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers /// /// Years controller. /// + [Authorize(Policy = Policies.DefaultAuthorization)] public class YearsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs index a5f012245..66e797699 100644 --- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs +++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs @@ -8,7 +8,7 @@ namespace Jellyfin.Api.Models.StartupDtos /// /// Gets or sets UI language culture. /// - public string? UICulture { get; set; } + public string UICulture { get; set; } = null!; /// /// Gets or sets the metadata country code. diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 6e91042df..586746430 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -7,8 +7,11 @@ using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.DownloadPolicy; +using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; -using Jellyfin.Api.Auth.IgnoreSchedulePolicy; +using Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy; +using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; +using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy; using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; @@ -41,9 +44,12 @@ namespace Jellyfin.Server.Extensions { serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); return serviceCollection.AddAuthorizationCore(options => { @@ -61,6 +67,13 @@ namespace Jellyfin.Server.Extensions policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddRequirements(new DownloadRequirement()); }); + options.AddPolicy( + Policies.FirstTimeSetupOrDefault, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new FirstTimeSetupOrDefaultRequirement()); + }); options.AddPolicy( Policies.FirstTimeSetupOrElevated, policy => @@ -69,11 +82,18 @@ namespace Jellyfin.Server.Extensions policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement()); }); options.AddPolicy( - Policies.IgnoreSchedule, + Policies.IgnoreParentalControl, policy => { policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new IgnoreScheduleRequirement()); + policy.AddRequirements(new IgnoreParentalControlRequirement()); + }); + options.AddPolicy( + Policies.IgnoreParentalControlOrFirstTimeSetup, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new IgnoreParentalControlOrFirstTimeSetupRequirement()); }); options.AddPolicy( Policies.LocalAccessOnly, @@ -82,6 +102,13 @@ namespace Jellyfin.Server.Extensions policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddRequirements(new LocalAccessRequirement()); }); + options.AddPolicy( + Policies.LocalAccessOrRequiresElevation, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new LocalAccessOrRequiresElevationRequirement()); + }); options.AddPolicy( Policies.RequiresElevation, policy => diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs index b65d45aa0..7150c90bb 100644 --- a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; -using Jellyfin.Api.Auth.IgnoreSchedulePolicy; +using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -20,7 +20,7 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy { private readonly Mock _configurationManagerMock; private readonly List _requirements; - private readonly IgnoreScheduleHandler _sut; + private readonly IgnoreParentalControlHandler _sut; private readonly Mock _userManagerMock; private readonly Mock _httpContextAccessor; @@ -33,11 +33,11 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy { var fixture = new Fixture().Customize(new AutoMoqCustomization()); _configurationManagerMock = fixture.Freeze>(); - _requirements = new List { new IgnoreScheduleRequirement() }; + _requirements = new List { new IgnoreParentalControlRequirement() }; _userManagerMock = fixture.Freeze>(); _httpContextAccessor = fixture.Freeze>(); - _sut = fixture.Create(); + _sut = fixture.Create(); } [Theory] -- cgit v1.2.3 From 05f9473544dd40c6ffdc8512d675256468c09cb3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 6 Aug 2020 17:59:48 -0600 Subject: Apply fixes from review --- ...FirstTimeOrIgnoreParentalControlSetupHandler.cs | 57 ++++++++++++++++++++++ ...tTimeOrIgnoreParentalControlSetupRequirement.cs | 11 +++++ .../FirstTimeSetupOrDefaultHandler.cs | 8 +-- .../FirstTimeSetupOrDefaultRequirement.cs | 2 +- ...IgnoreParentalControlOrFirstTimeSetupHandler.cs | 51 ------------------- ...reParentalControlOrFirstTimeSetupRequirement.cs | 11 ----- .../LocalAccessOrRequiresElevationHandler.cs | 3 +- .../LocalAccessOrRequiresElevationRequirement.cs | 2 +- Jellyfin.Api/Constants/Policies.cs | 2 +- Jellyfin.Api/Controllers/ImageController.cs | 1 + Jellyfin.Api/Controllers/SystemController.cs | 2 +- .../Models/StartupDtos/StartupConfigurationDto.cs | 2 +- .../Extensions/ApiServiceCollectionExtensions.cs | 8 +-- 13 files changed, 83 insertions(+), 77 deletions(-) create mode 100644 Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs create mode 100644 Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs delete mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs delete mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs new file mode 100644 index 000000000..2a02f8bc7 --- /dev/null +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs @@ -0,0 +1,57 @@ +using System.Threading.Tasks; +using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy +{ + /// + /// Ignore parental control schedule and allow before startup wizard has been completed. + /// + public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler + { + private readonly IConfigurationManager _configurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public FirstTimeOrIgnoreParentalControlSetupHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor, + IConfigurationManager configurationManager) + : base(userManager, networkManager, httpContextAccessor) + { + _configurationManager = configurationManager; + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement) + { + if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) + { + context.Succeed(requirement); + return Task.CompletedTask; + } + + var validated = ValidateClaims(context.User, ignoreSchedule: true); + if (validated) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs new file mode 100644 index 000000000..00aaec334 --- /dev/null +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy +{ + /// + /// First time setup or ignore parental controls requirement. + /// + public class FirstTimeOrIgnoreParentalControlSetupRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs index 67fb2b79a..9815e252e 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy { /// - /// Authorization handler for requiring first time setup or elevated privileges. + /// Authorization handler for requiring first time setup or default privileges. /// public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler { @@ -32,18 +32,18 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy } /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrElevatedRequirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrDefaultRequirement) { if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) { - context.Succeed(firstTimeSetupOrElevatedRequirement); + context.Succeed(firstTimeSetupOrDefaultRequirement); return Task.CompletedTask; } var validated = ValidateClaims(context.User); if (validated) { - context.Succeed(firstTimeSetupOrElevatedRequirement); + context.Succeed(firstTimeSetupOrDefaultRequirement); } else { diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs index 23d7ee01f..f7366bd7a 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Authorization; namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy { /// - /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler. + /// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler. /// public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement { diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs deleted file mode 100644 index 6c9258b3d..000000000 --- a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Threading.Tasks; -using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy -{ - /// - /// Escape schedule controls handler. - /// - public class IgnoreParentalControlOrFirstTimeSetupHandler : BaseAuthorizationHandler - { - private readonly IConfigurationManager _configurationManager; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public IgnoreParentalControlOrFirstTimeSetupHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor, - IConfigurationManager configurationManager) - : base(userManager, networkManager, httpContextAccessor) - { - _configurationManager = configurationManager; - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement) - { - var validated = ValidateClaims(context.User, ignoreSchedule: true); - if (validated || !_configurationManager.CommonConfiguration.IsStartupWizardCompleted) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs deleted file mode 100644 index 36ded0625..000000000 --- a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy -{ - /// - /// Escape schedule controls requirement. - /// - public class IgnoreParentalControlOrFirstTimeSetupRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index d9ab8aa68..14722aa57 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy { /// - /// Local access handler. + /// Local access or require elevated privileges handler. /// public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler { @@ -30,7 +30,6 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement) { var validated = ValidateClaims(context.User, localAccessOnly: true); - if (validated || context.User.IsInRole(UserRoles.Administrator)) { context.Succeed(requirement); diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs index ad96caa81..d9c64d01c 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs @@ -3,7 +3,7 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy { /// - /// The local access authorization requirement. + /// The local access or elevated privileges authorization requirement. /// public class LocalAccessOrRequiresElevationRequirement : IAuthorizationRequirement { diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index 8de637c4e..7d7767470 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -48,6 +48,6 @@ namespace Jellyfin.Api.Constants /// /// Policy name for escaping schedule controls or requiring first time setup. /// - public const string IgnoreParentalControlOrFirstTimeSetup = "IgnoreParentalControlOrFirstTimeSetup"; + public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl"; } } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 410456a25..45447ae0c 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -131,6 +131,7 @@ namespace Jellyfin.Api.Controllers /// A . [HttpDelete("Users/{userId}/Images/{itemType}")] [HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")] + [Authorize(Policy = Policies.DefaultAuthorization)] [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)] diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 08f1b421d..bbfd163de 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers /// Information retrieved. /// A with info about the system. [HttpGet("Info")] - [Authorize(Policy = Policies.IgnoreParentalControlOrFirstTimeSetup)] + [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetSystemInfo() { diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs index 66e797699..a5f012245 100644 --- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs +++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs @@ -8,7 +8,7 @@ namespace Jellyfin.Api.Models.StartupDtos /// /// Gets or sets UI language culture. /// - public string UICulture { get; set; } = null!; + public string? UICulture { get; set; } /// /// Gets or sets the metadata country code. diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 586746430..83d8fac5b 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -7,9 +7,9 @@ using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.DownloadPolicy; +using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy; using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; -using Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy; using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy; using Jellyfin.Api.Auth.LocalAccessPolicy; @@ -47,7 +47,7 @@ namespace Jellyfin.Server.Extensions serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -89,11 +89,11 @@ namespace Jellyfin.Server.Extensions policy.AddRequirements(new IgnoreParentalControlRequirement()); }); options.AddPolicy( - Policies.IgnoreParentalControlOrFirstTimeSetup, + Policies.FirstTimeSetupOrIgnoreParentalControl, policy => { policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new IgnoreParentalControlOrFirstTimeSetupRequirement()); + policy.AddRequirements(new FirstTimeOrIgnoreParentalControlSetupRequirement()); }); options.AddPolicy( Policies.LocalAccessOnly, -- cgit v1.2.3