diff options
Diffstat (limited to 'Jellyfin.Api/Controllers')
| -rw-r--r-- | Jellyfin.Api/Controllers/FilterController.cs | 8 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/ImageByNameController.cs | 252 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/LibraryController.cs | 15 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/MoviesController.cs | 6 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/QuickConnectController.cs | 26 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/UserController.cs | 10 |
6 files changed, 33 insertions, 284 deletions
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index b6780ee20..17d136384 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -92,25 +92,25 @@ namespace Jellyfin.Api.Controllers Years = itemList.Select(i => i.ProductionYear ?? -1) .Where(i => i > 0) .Distinct() - .OrderBy(i => i) + .Order() .ToArray(), Genres = itemList.SelectMany(i => i.Genres) .DistinctNames() - .OrderBy(i => i) + .Order() .ToArray(), Tags = itemList .SelectMany(i => i.Tags) .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(i => i) + .Order() .ToArray(), OfficialRatings = itemList .Select(i => i.OfficialRating) .Where(i => !string.IsNullOrWhiteSpace(i)) .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(i => i) + .Order() .ToArray() }; } diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs deleted file mode 100644 index c54851b96..000000000 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.IO; -using System.Linq; -using System.Net.Mime; -using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Jellyfin.Api.Controllers -{ - /// <summary> - /// Images By Name Controller. - /// </summary> - [Route("Images")] - public class ImageByNameController : BaseJellyfinApiController - { - private readonly IServerApplicationPaths _applicationPaths; - private readonly IFileSystem _fileSystem; - - /// <summary> - /// Initializes a new instance of the <see cref="ImageByNameController" /> class. - /// </summary> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager" /> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem" /> interface.</param> - public ImageByNameController( - IServerConfigurationManager serverConfigurationManager, - IFileSystem fileSystem) - { - _applicationPaths = serverConfigurationManager.ApplicationPaths; - _fileSystem = fileSystem; - } - - /// <summary> - /// Get all general images. - /// </summary> - /// <response code="200">Retrieved list of images.</response> - /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> - [HttpGet("General")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages() - { - return GetImageList(_applicationPaths.GeneralPath, false); - } - - /// <summary> - /// Get General Image. - /// </summary> - /// <param name="name">The name of the image.</param> - /// <param name="type">Image Type (primary, backdrop, logo, etc).</param> - /// <response code="200">Image stream retrieved.</response> - /// <response code="404">Image not found.</response> - /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> - [HttpGet("General/{name}/{type}")] - [AllowAnonymous] - [Produces(MediaTypeNames.Application.Octet)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public ActionResult GetGeneralImage([FromRoute, Required] string name, [FromRoute, Required] string type) - { - var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase) - ? "folder" - : type; - - var path = BaseItem.SupportedImageExtensions - .Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i))) - .FirstOrDefault(System.IO.File.Exists); - - if (path is null) - { - return NotFound(); - } - - if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.InvariantCulture)) - { - return BadRequest("Invalid image path."); - } - - var contentType = MimeTypes.GetMimeType(path); - return File(AsyncFile.OpenRead(path), contentType); - } - - /// <summary> - /// Get all general images. - /// </summary> - /// <response code="200">Retrieved list of images.</response> - /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> - [HttpGet("Ratings")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages() - { - return GetImageList(_applicationPaths.RatingsPath, false); - } - - /// <summary> - /// Get rating image. - /// </summary> - /// <param name="theme">The theme to get the image from.</param> - /// <param name="name">The name of the image.</param> - /// <response code="200">Image stream retrieved.</response> - /// <response code="404">Image not found.</response> - /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> - [HttpGet("Ratings/{theme}/{name}")] - [AllowAnonymous] - [Produces(MediaTypeNames.Application.Octet)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public ActionResult GetRatingImage( - [FromRoute, Required] string theme, - [FromRoute, Required] string name) - { - return GetImageFile(_applicationPaths.RatingsPath, theme, name); - } - - /// <summary> - /// Get all media info images. - /// </summary> - /// <response code="200">Image list retrieved.</response> - /// <returns>An <see cref="OkResult"/> containing the list of images.</returns> - [HttpGet("MediaInfo")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages() - { - return GetImageList(_applicationPaths.MediaInfoImagesPath, false); - } - - /// <summary> - /// Get media info image. - /// </summary> - /// <param name="theme">The theme to get the image from.</param> - /// <param name="name">The name of the image.</param> - /// <response code="200">Image stream retrieved.</response> - /// <response code="404">Image not found.</response> - /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> - [HttpGet("MediaInfo/{theme}/{name}")] - [AllowAnonymous] - [Produces(MediaTypeNames.Application.Octet)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public ActionResult GetMediaInfoImage( - [FromRoute, Required] string theme, - [FromRoute, Required] string name) - { - return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name); - } - - /// <summary> - /// Internal FileHelper. - /// </summary> - /// <param name="basePath">Path to begin search.</param> - /// <param name="theme">Theme to search.</param> - /// <param name="name">File name to search for.</param> - /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> - private ActionResult GetImageFile(string basePath, string theme, string? name) - { - var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme)); - - if (Directory.Exists(themeFolder)) - { - var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i)) - .FirstOrDefault(System.IO.File.Exists); - - if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) - { - if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) - { - return BadRequest("Invalid image path."); - } - - var contentType = MimeTypes.GetMimeType(path); - - return PhysicalFile(path, contentType); - } - } - - var allFolder = Path.GetFullPath(Path.Combine(basePath, "all")); - if (Directory.Exists(allFolder)) - { - var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i)) - .FirstOrDefault(System.IO.File.Exists); - - if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) - { - if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) - { - return BadRequest("Invalid image path."); - } - - var contentType = MimeTypes.GetMimeType(path); - return PhysicalFile(path, contentType); - } - } - - return NotFound(); - } - - private List<ImageByNameInfo> GetImageList(string path, bool supportsThemes) - { - try - { - return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true) - .Select(i => new ImageByNameInfo - { - Name = _fileSystem.GetFileNameWithoutExtension(i), - FileLength = i.Length, - - // For themeable images, use the Theme property - // For general images, the same object structure is fine, - // but it's not owned by a theme, so call it Context - Theme = supportsThemes ? GetThemeName(i.FullName, path) : null, - Context = supportsThemes ? null : GetThemeName(i.FullName, path), - Format = i.Extension.ToLowerInvariant().TrimStart('.') - }) - .OrderBy(i => i.Name) - .ToList(); - } - catch (IOException) - { - return new List<ImageByNameInfo>(); - } - } - - private string? GetThemeName(string path, string rootImagePath) - { - var parentName = Path.GetDirectoryName(path); - - if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - parentName = Path.GetFileName(parentName); - - return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ? null : parentName; - } - } -} diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index ab2020830..196d509fb 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -770,8 +770,7 @@ namespace Jellyfin.Api.Controllers Name = i.Name, DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary) }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToArray(); result.MetadataReaders = plugins @@ -781,8 +780,7 @@ namespace Jellyfin.Api.Controllers Name = i.Name, DefaultEnabled = true }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToArray(); result.SubtitleFetchers = plugins @@ -792,8 +790,7 @@ namespace Jellyfin.Api.Controllers Name = i.Name, DefaultEnabled = true }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToArray(); var typeOptions = new List<LibraryTypeOptionsDto>(); @@ -814,8 +811,7 @@ namespace Jellyfin.Api.Controllers Name = i.Name, DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToArray(), ImageFetchers = plugins @@ -826,8 +822,7 @@ namespace Jellyfin.Api.Controllers Name = i.Name, DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) }) - .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .Select(x => x.First()) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToArray(), SupportedImageTypes = plugins diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 03f864b4a..3cf079362 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -200,8 +200,7 @@ namespace Jellyfin.Api.Controllers IsMovie = true, EnableGroupByMetadataKey = true, DtoOptions = dtoOptions - }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) - .Select(x => x.First()) + }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) .Take(itemLimit) .ToList(); @@ -240,8 +239,7 @@ namespace Jellyfin.Api.Controllers IsMovie = true, EnableGroupByMetadataKey = true, DtoOptions = dtoOptions - }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) - .Select(x => x.First()) + }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) .Take(itemLimit) .ToList(); diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index 77d88475f..6dbcdae22 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; @@ -51,7 +52,7 @@ namespace Jellyfin.Api.Controllers /// <response code="200">Quick connect request successfully created.</response> /// <response code="401">Quick connect is not active on this server.</response> /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns> - [HttpGet("Initiate")] + [HttpPost("Initiate")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect() { @@ -67,6 +68,16 @@ namespace Jellyfin.Api.Controllers } /// <summary> + /// Old version of <see cref="InitiateQuickConnect" /> using a GET method. + /// Still available to avoid breaking compatibility. + /// </summary> + /// <returns>The result of <see cref="InitiateQuickConnect" />.</returns> + [Obsolete("Use POST request instead")] + [HttpGet("Initiate")] + [ApiExplorerSettings(IgnoreApi = true)] + public Task<ActionResult<QuickConnectResult>> InitiateQuickConnectLegacy() => InitiateQuickConnect(); + + /// <summary> /// Attempts to retrieve authentication information. /// </summary> /// <param name="secret">Secret previously returned from the Initiate endpoint.</param> @@ -96,6 +107,7 @@ namespace Jellyfin.Api.Controllers /// Authorizes a pending quick connect request. /// </summary> /// <param name="code">Quick connect code to authorize.</param> + /// <param name="userId">The user the authorize. Access to the requested user is required.</param> /// <response code="200">Quick connect result authorized successfully.</response> /// <response code="403">Unknown user id.</response> /// <returns>Boolean indicating if the authorization was successful.</returns> @@ -103,17 +115,19 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code) + public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null) { - var userId = User.GetUserId(); - if (userId.Equals(default)) + var currentUserId = User.GetUserId(); + var actualUserId = userId ?? currentUserId; + + if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator))) { - return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id"); + return Forbid("Unknown user id"); } try { - return await _quickConnect.AuthorizeRequest(userId, code).ConfigureAwait(false); + return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false); } catch (AuthenticationException) { diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 002327d74..568224a42 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -157,7 +157,6 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="userId">The user id.</param> /// <param name="pw">The password as plain text.</param> - /// <param name="password">The password sha1-hash.</param> /// <response code="200">User authenticated.</response> /// <response code="403">Sha1-hashed password only is not allowed.</response> /// <response code="404">User not found.</response> @@ -166,10 +165,10 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Obsolete("Authenticate with username instead")] public async Task<ActionResult<AuthenticationResult>> AuthenticateUser( [FromRoute, Required] Guid userId, - [FromQuery, Required] string pw, - [FromQuery] string? password) + [FromQuery, Required] string pw) { var user = _userManager.GetUserById(userId); @@ -178,11 +177,6 @@ namespace Jellyfin.Api.Controllers return NotFound("User not found"); } - if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw)) - { - return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed."); - } - AuthenticateUserByName request = new AuthenticateUserByName { Username = user.Username, |
