Преглед изворни кода

Remove api client generator errors

crobibero пре 4 година
родитељ
комит
547ee88561

+ 166 - 3
Jellyfin.Api/Controllers/AudioController.cs

@@ -85,15 +85,178 @@ namespace Jellyfin.Api.Controllers
         /// <param name="streamOptions">Optional. The streaming options.</param>
         /// <response code="200">Audio stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
-        [HttpGet("{itemId}/stream.{container:required}", Name = "GetAudioStreamByContainer")]
         [HttpGet("{itemId}/stream", Name = "GetAudioStream")]
-        [HttpHead("{itemId}/stream.{container:required}", Name = "HeadAudioStreamByContainer")]
         [HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesAudioFile]
         public async Task<ActionResult> GetAudioStream(
             [FromRoute, Required] Guid itemId,
-            [FromRoute] string? container,
+            [FromQuery] string? container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext? context,
+            [FromQuery] Dictionary<string, string>? streamOptions)
+        {
+            StreamingRequestDto streamingRequest = new StreamingRequestDto
+            {
+                Id = itemId,
+                Container = container,
+                Static = @static ?? true,
+                Params = @params,
+                Tag = tag,
+                DeviceProfileId = deviceProfileId,
+                PlaySessionId = playSessionId,
+                SegmentContainer = segmentContainer,
+                SegmentLength = segmentLength,
+                MinSegments = minSegments,
+                MediaSourceId = mediaSourceId,
+                DeviceId = deviceId,
+                AudioCodec = audioCodec,
+                EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+                AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+                AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+                AudioSampleRate = audioSampleRate,
+                MaxAudioChannels = maxAudioChannels,
+                AudioBitRate = audioBitRate,
+                MaxAudioBitDepth = maxAudioBitDepth,
+                AudioChannels = audioChannels,
+                Profile = profile,
+                Level = level,
+                Framerate = framerate,
+                MaxFramerate = maxFramerate,
+                CopyTimestamps = copyTimestamps ?? true,
+                StartTimeTicks = startTimeTicks,
+                Width = width,
+                Height = height,
+                VideoBitRate = videoBitRate,
+                SubtitleStreamIndex = subtitleStreamIndex,
+                SubtitleMethod = subtitleMethod,
+                MaxRefFrames = maxRefFrames,
+                MaxVideoBitDepth = maxVideoBitDepth,
+                RequireAvc = requireAvc ?? true,
+                DeInterlace = deInterlace ?? true,
+                RequireNonAnamorphic = requireNonAnamorphic ?? true,
+                TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+                CpuCoreLimit = cpuCoreLimit,
+                LiveStreamId = liveStreamId,
+                EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+                VideoCodec = videoCodec,
+                SubtitleCodec = subtitleCodec,
+                TranscodeReasons = transcodingReasons,
+                AudioStreamIndex = audioStreamIndex,
+                VideoStreamIndex = videoStreamIndex,
+                Context = context ?? EncodingContext.Static,
+                StreamOptions = streamOptions
+            };
+
+            return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Gets an audio stream.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="container">The audio container.</param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <response code="200">Audio stream returned.</response>
+        /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+        [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
+        [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesAudioFile]
+        public async Task<ActionResult> GetAudioStreamByContainer(
+            [FromRoute, Required] Guid itemId,
+            [FromRoute, Required] string container,
             [FromQuery] bool? @static,
             [FromQuery] string? @params,
             [FromQuery] string? tag,

+ 2 - 2
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -833,7 +833,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid itemId,
             [FromRoute, Required] string playlistId,
             [FromRoute, Required] int segmentId,
-            [FromRoute] string container,
+            [FromRoute, Required] string container,
             [FromQuery] bool? @static,
             [FromQuery] string? @params,
             [FromQuery] string? tag,
@@ -1004,7 +1004,7 @@ namespace Jellyfin.Api.Controllers
             [FromRoute, Required] Guid itemId,
             [FromRoute, Required] string playlistId,
             [FromRoute, Required] int segmentId,
-            [FromRoute] string container,
+            [FromRoute, Required] string container,
             [FromQuery] bool? @static,
             [FromQuery] string? @params,
             [FromQuery] string? tag,

+ 683 - 53
Jellyfin.Api/Controllers/ImageController.cs

@@ -85,7 +85,6 @@ namespace Jellyfin.Api.Controllers
         /// <response code="403">User does not have permission to delete the image.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Users/{userId}/Images/{imageType}")]
-        [HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
@@ -94,7 +93,53 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> PostUserImage(
             [FromRoute, Required] Guid userId,
             [FromRoute, Required] ImageType imageType,
-            [FromRoute] int? index = null)
+            [FromQuery] int? index = null)
+        {
+            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);
+
+            // 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)
+            {
+                await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+            }
+
+            user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
+
+            await _providerManager
+                .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+                .ConfigureAwait(false);
+            await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Sets the user image.
+        /// </summary>
+        /// <param name="userId">User Id.</param>
+        /// <param name="imageType">(Unused) Image type.</param>
+        /// <param name="index">(Unused) Image index.</param>
+        /// <response code="204">Image updated.</response>
+        /// <response code="403">User does not have permission to delete the image.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
+        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [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<ActionResult> PostUserImageByIndex(
+            [FromRoute, Required] Guid userId,
+            [FromRoute, Required] ImageType imageType,
+            [FromRoute] int index)
         {
             if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
             {
@@ -131,8 +176,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Image deleted.</response>
         /// <response code="403">User does not have permission to delete the image.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpDelete("Users/{userId}/Images/{itemType}")]
-        [HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
+        [HttpDelete("Users/{userId}/Images/{imageType}")]
         [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")]
@@ -141,7 +185,46 @@ namespace Jellyfin.Api.Controllers
         public async Task<ActionResult> DeleteUserImage(
             [FromRoute, Required] Guid userId,
             [FromRoute, Required] ImageType imageType,
-            [FromRoute] int? index = null)
+            [FromQuery] int? index = null)
+        {
+            if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+            {
+                return Forbid("User is not allowed to delete the image.");
+            }
+
+            var user = _userManager.GetUserById(userId);
+            try
+            {
+                System.IO.File.Delete(user.ProfileImage.Path);
+            }
+            catch (IOException e)
+            {
+                _logger.LogError(e, "Error deleting user profile image:");
+            }
+
+            await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Delete the user's image.
+        /// </summary>
+        /// <param name="userId">User Id.</param>
+        /// <param name="imageType">(Unused) Image type.</param>
+        /// <param name="index">(Unused) Image index.</param>
+        /// <response code="204">Image deleted.</response>
+        /// <response code="403">User does not have permission to delete the image.</response>
+        /// <returns>A <see cref="NoContentResult"/>.</returns>
+        [HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
+        [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)]
+        [ProducesResponseType(StatusCodes.Status403Forbidden)]
+        public async Task<ActionResult> DeleteUserImageByIndex(
+            [FromRoute, Required] Guid userId,
+            [FromRoute, Required] ImageType imageType,
+            [FromRoute] int index)
         {
             if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
             {
@@ -172,14 +255,13 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
         [HttpDelete("Items/{itemId}/Images/{imageType}")]
-        [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> DeleteItemImage(
             [FromRoute, Required] Guid itemId,
             [FromRoute, Required] ImageType imageType,
-            [FromRoute] int? imageIndex = null)
+            [FromQuery] int? imageIndex)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
@@ -191,25 +273,83 @@ namespace Jellyfin.Api.Controllers
             return NoContent();
         }
 
+        /// <summary>
+        /// Delete an item's image.
+        /// </summary>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">The image index.</param>
+        /// <response code="204">Image deleted.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+        [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+        [Authorize(Policy = Policies.RequiresElevation)]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        public async Task<ActionResult> DeleteItemImageByIndex(
+            [FromRoute, Required] Guid itemId,
+            [FromRoute, Required] ImageType imageType,
+            [FromRoute] int imageIndex)
+        {
+            var item = _libraryManager.GetItemById(itemId);
+            if (item == null)
+            {
+                return NotFound();
+            }
+
+            await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
+            return NoContent();
+        }
+
         /// <summary>
         /// Set item image.
         /// </summary>
         /// <param name="itemId">Item id.</param>
         /// <param name="imageType">Image type.</param>
-        /// <param name="imageIndex">(Unused) Image index.</param>
         /// <response code="204">Image saved.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
         [HttpPost("Items/{itemId}/Images/{imageType}")]
-        [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
         public async Task<ActionResult> SetItemImage(
+            [FromRoute, Required] Guid itemId,
+            [FromRoute, Required] ImageType imageType)
+        {
+            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);
+            await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+
+            return NoContent();
+        }
+
+        /// <summary>
+        /// Set item image.
+        /// </summary>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">(Unused) Image index.</param>
+        /// <response code="204">Image saved.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+        [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<ActionResult> SetItemImageByIndex(
             [FromRoute, Required] Guid itemId,
             [FromRoute, Required] ImageType imageType,
-            [FromRoute] int? imageIndex = null)
+            [FromRoute] int imageIndex)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
@@ -349,8 +489,6 @@ namespace Jellyfin.Api.Controllers
         /// </returns>
         [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)]
         [ProducesImageFile]
@@ -371,7 +509,86 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? blur,
             [FromQuery] string? backgroundColor,
             [FromQuery] string? foregroundLayer,
-            [FromRoute] int? imageIndex = null)
+            [FromQuery] int? imageIndex)
+        {
+            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);
+        }
+
+        /// <summary>
+        /// Gets the item's image.
+        /// </summary>
+        /// <param name="itemId">Item id.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+        [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesImageFile]
+        public async Task<ActionResult> GetItemImageByIndex(
+            [FromRoute, Required] Guid itemId,
+            [FromRoute, Required] ImageType imageType,
+            [FromRoute] int imageIndex,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] string? tag,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] ImageFormat? format,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromQuery] double? percentPlayed,
+            [FromQuery] int? unplayedCount,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer)
         {
             var item = _libraryManager.GetItemById(itemId);
             if (item == null)
@@ -507,8 +724,8 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [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)]
         [ProducesImageFile]
@@ -586,8 +803,8 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
+        [HttpGet("Genres/{name}/Images/{imageType}")]
+        [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesImageFile]
@@ -608,7 +825,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? blur,
             [FromQuery] string? backgroundColor,
             [FromQuery] string? foregroundLayer,
-            [FromRoute] int? imageIndex = null)
+            [FromQuery] int? imageIndex)
         {
             var item = _libraryManager.GetGenre(name);
             if (item == null)
@@ -640,10 +857,11 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Get music genre image by name.
+        /// Get genre image by name.
         /// </summary>
-        /// <param name="name">Music genre name.</param>
+        /// <param name="name">Genre name.</param>
         /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">Image index.</param>
         /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
         /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
         /// <param name="maxWidth">The maximum image width to return.</param>
@@ -658,21 +876,21 @@ namespace Jellyfin.Api.Controllers
         /// <param name="blur">Optional. Blur image.</param>
         /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
         /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
-        /// <param name="imageIndex">Image index.</param>
         /// <response code="200">Image stream returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
+        [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
+        [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesImageFile]
-        public async Task<ActionResult> GetMusicGenreImage(
+        public async Task<ActionResult> GetGenreImageByIndex(
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
+            [FromRoute, Required] int imageIndex,
             [FromQuery] string tag,
             [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
@@ -686,10 +904,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? addPlayedIndicator,
             [FromQuery] int? blur,
             [FromQuery] string? backgroundColor,
-            [FromQuery] string? foregroundLayer,
-            [FromRoute] int? imageIndex = null)
+            [FromQuery] string? foregroundLayer)
         {
-            var item = _libraryManager.GetMusicGenre(name);
+            var item = _libraryManager.GetGenre(name);
             if (item == null)
             {
                 return NotFound();
@@ -719,9 +936,9 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Get person image by name.
+        /// Get music genre image by name.
         /// </summary>
-        /// <param name="name">Person name.</param>
+        /// <param name="name">Music genre name.</param>
         /// <param name="imageType">Image type.</param>
         /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
         /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
@@ -744,12 +961,12 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
+        [HttpGet("MusicGenres/{name}/Images/{imageType}")]
+        [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesImageFile]
-        public async Task<ActionResult> GetPersonImage(
+        public async Task<ActionResult> GetMusicGenreImage(
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
             [FromQuery] string tag,
@@ -766,9 +983,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? blur,
             [FromQuery] string? backgroundColor,
             [FromQuery] string? foregroundLayer,
-            [FromRoute] int? imageIndex = null)
+            [FromQuery] int? imageIndex)
         {
-            var item = _libraryManager.GetPerson(name);
+            var item = _libraryManager.GetMusicGenre(name);
             if (item == null)
             {
                 return NotFound();
@@ -798,10 +1015,11 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Get studio image by name.
+        /// Get music genre image by name.
         /// </summary>
-        /// <param name="name">Studio name.</param>
+        /// <param name="name">Music genre name.</param>
         /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">Image index.</param>
         /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
         /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
         /// <param name="maxWidth">The maximum image width to return.</param>
@@ -816,23 +1034,23 @@ namespace Jellyfin.Api.Controllers
         /// <param name="blur">Optional. Blur image.</param>
         /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
         /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
-        /// <param name="imageIndex">Image index.</param>
         /// <response code="200">Image stream returned.</response>
         /// <response code="404">Item not found.</response>
         /// <returns>
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
+        [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
+        [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesImageFile]
-        public async Task<ActionResult> GetStudioImage(
+        public async Task<ActionResult> GetMusicGenreImageByIndex(
             [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
-            [FromRoute, Required] string tag,
-            [FromRoute, Required] ImageFormat format,
+            [FromRoute, Required] int imageIndex,
+            [FromQuery] string tag,
+            [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
             [FromQuery] double? percentPlayed,
@@ -844,10 +1062,9 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? addPlayedIndicator,
             [FromQuery] int? blur,
             [FromQuery] string? backgroundColor,
-            [FromQuery] string? foregroundLayer,
-            [FromRoute] int? imageIndex = null)
+            [FromQuery] string? foregroundLayer)
         {
-            var item = _libraryManager.GetStudio(name);
+            var item = _libraryManager.GetMusicGenre(name);
             if (item == null)
             {
                 return NotFound();
@@ -877,9 +1094,9 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Get user profile image.
+        /// Get person image by name.
         /// </summary>
-        /// <param name="userId">User id.</param>
+        /// <param name="name">Person name.</param>
         /// <param name="imageType">Image type.</param>
         /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
         /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
@@ -902,15 +1119,15 @@ namespace Jellyfin.Api.Controllers
         /// A <see cref="FileStreamResult"/> containing the file stream on success,
         /// or a <see cref="NotFoundResult"/> if item not found.
         /// </returns>
-        [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex?}")]
-        [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
+        [HttpGet("Persons/{name}/Images/{imageType}")]
+        [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         [ProducesImageFile]
-        public async Task<ActionResult> GetUserImage(
-            [FromRoute, Required] Guid userId,
+        public async Task<ActionResult> GetPersonImage(
+            [FromRoute, Required] string name,
             [FromRoute, Required] ImageType imageType,
-            [FromQuery] string? tag,
+            [FromQuery] string tag,
             [FromQuery] ImageFormat? format,
             [FromQuery] int? maxWidth,
             [FromQuery] int? maxHeight,
@@ -924,7 +1141,420 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? blur,
             [FromQuery] string? backgroundColor,
             [FromQuery] string? foregroundLayer,
-            [FromRoute] int? imageIndex = null)
+            [FromQuery] int? imageIndex)
+        {
+            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);
+        }
+
+        /// <summary>
+        /// Get person image by name.
+        /// </summary>
+        /// <param name="name">Person name.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
+        [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesImageFile]
+        public async Task<ActionResult> GetPersonImageByIndex(
+            [FromRoute, Required] string name,
+            [FromRoute, Required] ImageType imageType,
+            [FromRoute, Required] int imageIndex,
+            [FromQuery] string tag,
+            [FromQuery] ImageFormat? format,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] double? percentPlayed,
+            [FromQuery] int? unplayedCount,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer)
+        {
+            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);
+        }
+
+        /// <summary>
+        /// Get studio image by name.
+        /// </summary>
+        /// <param name="name">Studio name.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("Studios/{name}/Images/{imageType}")]
+        [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesImageFile]
+        public async Task<ActionResult> GetStudioImage(
+            [FromRoute, Required] string name,
+            [FromRoute, Required] ImageType imageType,
+            [FromQuery] string? tag,
+            [FromQuery] ImageFormat? format,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] double? percentPlayed,
+            [FromQuery] int? unplayedCount,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer,
+            [FromQuery] int? imageIndex)
+        {
+            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);
+        }
+
+        /// <summary>
+        /// Get studio image by name.
+        /// </summary>
+        /// <param name="name">Studio name.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
+        [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesImageFile]
+        public async Task<ActionResult> GetStudioImageByIndex(
+            [FromRoute, Required] string name,
+            [FromRoute, Required] ImageType imageType,
+            [FromRoute, Required] int imageIndex,
+            [FromQuery] string? tag,
+            [FromQuery] ImageFormat? format,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] double? percentPlayed,
+            [FromQuery] int? unplayedCount,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer)
+        {
+            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);
+        }
+
+        /// <summary>
+        /// Get user profile image.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("Users/{userId}/Images/{imageType}")]
+        [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesImageFile]
+        public async Task<ActionResult> GetUserImage(
+            [FromRoute, Required] Guid userId,
+            [FromRoute, Required] ImageType imageType,
+            [FromQuery] string? tag,
+            [FromQuery] ImageFormat? format,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] double? percentPlayed,
+            [FromQuery] int? unplayedCount,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer,
+            [FromQuery] int? imageIndex)
+        {
+            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);
+        }
+
+        /// <summary>
+        /// Get user profile image.
+        /// </summary>
+        /// <param name="userId">User id.</param>
+        /// <param name="imageType">Image type.</param>
+        /// <param name="imageIndex">Image index.</param>
+        /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+        /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+        /// <param name="maxWidth">The maximum image width to return.</param>
+        /// <param name="maxHeight">The maximum image height to return.</param>
+        /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+        /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+        /// <param name="width">The fixed image width to return.</param>
+        /// <param name="height">The fixed image height to return.</param>
+        /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+        /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+        /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+        /// <param name="blur">Optional. Blur image.</param>
+        /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+        /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+        /// <response code="200">Image stream returned.</response>
+        /// <response code="404">Item not found.</response>
+        /// <returns>
+        /// A <see cref="FileStreamResult"/> containing the file stream on success,
+        /// or a <see cref="NotFoundResult"/> if item not found.
+        /// </returns>
+        [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
+        [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesResponseType(StatusCodes.Status404NotFound)]
+        [ProducesImageFile]
+        public async Task<ActionResult> GetUserImageByIndex(
+            [FromRoute, Required] Guid userId,
+            [FromRoute, Required] ImageType imageType,
+            [FromRoute, Required] int imageIndex,
+            [FromQuery] string? tag,
+            [FromQuery] ImageFormat? format,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] double? percentPlayed,
+            [FromQuery] int? unplayedCount,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? quality,
+            [FromQuery] bool? cropWhitespace,
+            [FromQuery] bool? addPlayedIndicator,
+            [FromQuery] int? blur,
+            [FromQuery] string? backgroundColor,
+            [FromQuery] string? foregroundLayer)
         {
             var user = _userManager.GetUserById(userId);
             if (user == null)

+ 2 - 2
Jellyfin.Api/Controllers/InstantMixController.cs

@@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
-        [HttpGet("Artists/InstantMix")]
+        [HttpGet("Artists/{id}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
             [FromRoute, Required] Guid id,
@@ -242,7 +242,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <response code="200">Instant playlist returned.</response>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
-        [HttpGet("MusicGenres/InstantMix")]
+        [HttpGet("MusicGenres/{id}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
             [FromRoute, Required] Guid id,

+ 251 - 6
Jellyfin.Api/Controllers/ItemsController.cs

@@ -60,7 +60,6 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets items based on a query.
         /// </summary>
-        /// <param name="uId">The user id supplied in the /Users/{uid}/Items.</param>
         /// <param name="userId">The user id supplied as query parameter.</param>
         /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
         /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
@@ -143,10 +142,8 @@ namespace Jellyfin.Api.Controllers
         /// <param name="enableImages">Optional, include image information in output.</param>
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
         [HttpGet("Items")]
-        [HttpGet("Users/{uId}/Items", Name = "GetItems_2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetItems(
-            [FromRoute] Guid? uId,
             [FromQuery] Guid? userId,
             [FromQuery] string? maxOfficialRating,
             [FromQuery] bool? hasThemeSong,
@@ -228,9 +225,6 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool enableTotalRecordCount = true,
             [FromQuery] bool? enableImages = true)
         {
-            // use user id route parameter over query parameter
-            userId = uId ?? userId;
-
             var user = userId.HasValue && !userId.Equals(Guid.Empty)
                 ? _userManager.GetUserById(userId.Value)
                 : null;
@@ -505,6 +499,257 @@ namespace Jellyfin.Api.Controllers
             return new QueryResult<BaseItemDto> { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) };
         }
 
+        /// <summary>
+        /// Gets items based on a query.
+        /// </summary>
+        /// <param name="userId">The user id supplied as query parameter.</param>
+        /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
+        /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
+        /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
+        /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
+        /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
+        /// <param name="hasTrailer">Optional filter by items with trailers.</param>
+        /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+        /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
+        /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
+        /// <param name="isHd">Optional filter by items that are HD or not.</param>
+        /// <param name="is4K">Optional filter by items that are 4K or not.</param>
+        /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
+        /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
+        /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
+        /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
+        /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+        /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
+        /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
+        /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
+        /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
+        /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
+        /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
+        /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
+        /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
+        /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
+        /// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
+        /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
+        /// <param name="searchTerm">Optional. Filter based on a search term.</param>
+        /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+        /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+        /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
+        /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
+        /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+        /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+        /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+        /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
+        /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+        /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
+        /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
+        /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
+        /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
+        /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param>
+        /// <param name="enableUserData">Optional, include user data.</param>
+        /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+        /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
+        /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+        /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param>
+        /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
+        /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
+        /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
+        /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
+        /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
+        /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
+        /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
+        /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
+        /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
+        /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
+        /// <param name="isLocked">Optional filter by items that are locked.</param>
+        /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
+        /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
+        /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
+        /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
+        /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
+        /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
+        /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
+        /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
+        /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
+        /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+        /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+        /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+        /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
+        /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
+        /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+        /// <param name="enableImages">Optional, include image information in output.</param>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
+        [HttpGet("Users/{userId}/Items")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId(
+            [FromRoute] Guid userId,
+            [FromQuery] string? maxOfficialRating,
+            [FromQuery] bool? hasThemeSong,
+            [FromQuery] bool? hasThemeVideo,
+            [FromQuery] bool? hasSubtitles,
+            [FromQuery] bool? hasSpecialFeature,
+            [FromQuery] bool? hasTrailer,
+            [FromQuery] string? adjacentTo,
+            [FromQuery] int? parentIndexNumber,
+            [FromQuery] bool? hasParentalRating,
+            [FromQuery] bool? isHd,
+            [FromQuery] bool? is4K,
+            [FromQuery] string? locationTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
+            [FromQuery] bool? isMissing,
+            [FromQuery] bool? isUnaired,
+            [FromQuery] double? minCommunityRating,
+            [FromQuery] double? minCriticRating,
+            [FromQuery] DateTime? minPremiereDate,
+            [FromQuery] DateTime? minDateLastSaved,
+            [FromQuery] DateTime? minDateLastSavedForUser,
+            [FromQuery] DateTime? maxPremiereDate,
+            [FromQuery] bool? hasOverview,
+            [FromQuery] bool? hasImdbId,
+            [FromQuery] bool? hasTmdbId,
+            [FromQuery] bool? hasTvdbId,
+            [FromQuery] string? excludeItemIds,
+            [FromQuery] int? startIndex,
+            [FromQuery] int? limit,
+            [FromQuery] bool? recursive,
+            [FromQuery] string? searchTerm,
+            [FromQuery] string? sortOrder,
+            [FromQuery] string? parentId,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+            [FromQuery] string? excludeItemTypes,
+            [FromQuery] string? includeItemTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+            [FromQuery] bool? isFavorite,
+            [FromQuery] string? mediaTypes,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
+            [FromQuery] string? sortBy,
+            [FromQuery] bool? isPlayed,
+            [FromQuery] string? genres,
+            [FromQuery] string? officialRatings,
+            [FromQuery] string? tags,
+            [FromQuery] string? years,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+            [FromQuery] string? person,
+            [FromQuery] string? personIds,
+            [FromQuery] string? personTypes,
+            [FromQuery] string? studios,
+            [FromQuery] string? artists,
+            [FromQuery] string? excludeArtistIds,
+            [FromQuery] string? artistIds,
+            [FromQuery] string? albumArtistIds,
+            [FromQuery] string? contributingArtistIds,
+            [FromQuery] string? albums,
+            [FromQuery] string? albumIds,
+            [FromQuery] string? ids,
+            [FromQuery] string? videoTypes,
+            [FromQuery] string? minOfficialRating,
+            [FromQuery] bool? isLocked,
+            [FromQuery] bool? isPlaceHolder,
+            [FromQuery] bool? hasOfficialRating,
+            [FromQuery] bool? collapseBoxSetItems,
+            [FromQuery] int? minWidth,
+            [FromQuery] int? minHeight,
+            [FromQuery] int? maxWidth,
+            [FromQuery] int? maxHeight,
+            [FromQuery] bool? is3D,
+            [FromQuery] string? seriesStatus,
+            [FromQuery] string? nameStartsWithOrGreater,
+            [FromQuery] string? nameStartsWith,
+            [FromQuery] string? nameLessThan,
+            [FromQuery] string? studioIds,
+            [FromQuery] string? genreIds,
+            [FromQuery] bool enableTotalRecordCount = true,
+            [FromQuery] bool? enableImages = true)
+        {
+            return GetItems(
+                userId,
+                maxOfficialRating,
+                hasThemeSong,
+                hasThemeVideo,
+                hasSubtitles,
+                hasSpecialFeature,
+                hasTrailer,
+                adjacentTo,
+                parentIndexNumber,
+                hasParentalRating,
+                isHd,
+                is4K,
+                locationTypes,
+                excludeLocationTypes,
+                isMissing,
+                isUnaired,
+                minCommunityRating,
+                minCriticRating,
+                minPremiereDate,
+                minDateLastSaved,
+                minDateLastSavedForUser,
+                maxPremiereDate,
+                hasOverview,
+                hasImdbId,
+                hasTmdbId,
+                hasTvdbId,
+                excludeItemIds,
+                startIndex,
+                limit,
+                recursive,
+                searchTerm,
+                sortOrder,
+                parentId,
+                fields,
+                excludeItemTypes,
+                includeItemTypes,
+                filters,
+                isFavorite,
+                mediaTypes,
+                imageTypes,
+                sortBy,
+                isPlayed,
+                genres,
+                officialRatings,
+                tags,
+                years,
+                enableUserData,
+                imageTypeLimit,
+                enableImageTypes,
+                person,
+                personIds,
+                personTypes,
+                studios,
+                artists,
+                excludeArtistIds,
+                artistIds,
+                albumArtistIds,
+                contributingArtistIds,
+                albums,
+                albumIds,
+                ids,
+                videoTypes,
+                minOfficialRating,
+                isLocked,
+                isPlaceHolder,
+                hasOfficialRating,
+                collapseBoxSetItems,
+                minWidth,
+                minHeight,
+                maxWidth,
+                maxHeight,
+                is3D,
+                seriesStatus,
+                nameStartsWithOrGreater,
+                nameStartsWith,
+                nameLessThan,
+                studioIds,
+                genreIds,
+                enableTotalRecordCount,
+                enableImages);
+        }
+
         /// <summary>
         /// Gets items based on a query.
         /// </summary>

+ 40 - 2
Jellyfin.Api/Controllers/SubtitleController.cs

@@ -193,7 +193,6 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">File returned.</response>
         /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
         [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
-        [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesFile("text/*")]
         public async Task<ActionResult> GetSubtitle(
@@ -204,7 +203,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] long? endPositionTicks,
             [FromQuery] bool copyTimestamps = false,
             [FromQuery] bool addVttTimeMap = false,
-            [FromRoute] long startPositionTicks = 0)
+            [FromQuery] long startPositionTicks = 0)
         {
             if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
             {
@@ -249,6 +248,43 @@ namespace Jellyfin.Api.Controllers
                 MimeTypes.GetMimeType("file." + format));
         }
 
+        /// <summary>
+        /// Gets subtitles in a specified format.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="mediaSourceId">The media source id.</param>
+        /// <param name="index">The subtitle stream index.</param>
+        /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
+        /// <param name="format">The format of the returned subtitle.</param>
+        /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
+        /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
+        /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
+        /// <response code="200">File returned.</response>
+        /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
+        [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesFile("text/*")]
+        public Task<ActionResult> GetSubtitleWithTicks(
+            [FromRoute, Required] Guid itemId,
+            [FromRoute, Required] string mediaSourceId,
+            [FromRoute, Required] int index,
+            [FromRoute, Required] long startPositionTicks,
+            [FromRoute, Required] string format,
+            [FromQuery] long? endPositionTicks,
+            [FromQuery] bool copyTimestamps = false,
+            [FromQuery] bool addVttTimeMap = false)
+        {
+            return GetSubtitle(
+                itemId,
+                mediaSourceId,
+                index,
+                format,
+                endPositionTicks,
+                copyTimestamps,
+                addVttTimeMap,
+                startPositionTicks);
+        }
+
         /// <summary>
         /// Gets an HLS subtitle playlist.
         /// </summary>
@@ -335,6 +371,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="204">Subtitle uploaded.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Videos/{itemId}/Subtitles")]
+        [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> UploadSubtitle(
             [FromRoute, Required] Guid itemId,
             [FromBody, Required] UploadSubtitleDto body)
@@ -446,6 +483,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("FallbackFont/Fonts/{name}")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesFile("font/*")]
         public ActionResult GetFallbackFont([FromRoute, Required] string name)
         {
             var encodingOptions = _serverConfigurationManager.GetEncodingOptions();

+ 0 - 1
Jellyfin.Api/Controllers/TrailersController.cs

@@ -197,7 +197,6 @@ namespace Jellyfin.Api.Controllers
 
             return _itemsController
                 .GetItems(
-                    userId,
                     userId,
                     maxOfficialRating,
                     hasThemeSong,

+ 2 - 1
Jellyfin.Api/Controllers/VideoAttachmentsController.cs

@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
 using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
@@ -43,7 +44,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="404">Video or attachment not found.</response>
         /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
         [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")]
-        [Produces(MediaTypeNames.Application.Octet)]
+        [ProducesFile(MediaTypeNames.Application.Octet)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
         public async Task<ActionResult> GetAttachment(

+ 162 - 3
Jellyfin.Api/Controllers/VideosController.cs

@@ -326,15 +326,13 @@ namespace Jellyfin.Api.Controllers
         /// <param name="streamOptions">Optional. The streaming options.</param>
         /// <response code="200">Video stream returned.</response>
         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
-        [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")]
         [HttpGet("{itemId}/stream")]
-        [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")]
         [HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesVideoFile]
         public async Task<ActionResult> GetVideoStream(
             [FromRoute, Required] Guid itemId,
-            [FromRoute] string? container,
+            [FromQuery] string? container,
             [FromQuery] bool? @static,
             [FromQuery] string? @params,
             [FromQuery] string? tag,
@@ -529,5 +527,166 @@ namespace Jellyfin.Api.Controllers
                 _transcodingJobType,
                 cancellationTokenSource).ConfigureAwait(false);
         }
+
+        /// <summary>
+        /// Gets a video stream.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+        /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+        /// <param name="params">The streaming parameters.</param>
+        /// <param name="tag">The tag.</param>
+        /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+        /// <param name="playSessionId">The play session id.</param>
+        /// <param name="segmentContainer">The segment container.</param>
+        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="minSegments">The minimum number of segments.</param>
+        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+        /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+        /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+        /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+        /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+        /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+        /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+        /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+        /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+        /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+        /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+        /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+        /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+        /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+        /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+        /// <param name="maxRefFrames">Optional.</param>
+        /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+        /// <param name="requireAvc">Optional. Whether to require avc.</param>
+        /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+        /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+        /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+        /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+        /// <param name="liveStreamId">The live stream id.</param>
+        /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+        /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+        /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+        /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+        /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+        /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+        /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+        /// <param name="streamOptions">Optional. The streaming options.</param>
+        /// <response code="200">Video stream returned.</response>
+        /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+        [HttpGet("{itemId}/{stream=stream}.{container}")]
+        [HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [ProducesVideoFile]
+        public Task<ActionResult> GetVideoStreamByContainer(
+            [FromRoute, Required] Guid itemId,
+            [FromRoute, Required] string container,
+            [FromQuery] bool? @static,
+            [FromQuery] string? @params,
+            [FromQuery] string? tag,
+            [FromQuery] string? deviceProfileId,
+            [FromQuery] string? playSessionId,
+            [FromQuery] string? segmentContainer,
+            [FromQuery] int? segmentLength,
+            [FromQuery] int? minSegments,
+            [FromQuery] string? mediaSourceId,
+            [FromQuery] string? deviceId,
+            [FromQuery] string? audioCodec,
+            [FromQuery] bool? enableAutoStreamCopy,
+            [FromQuery] bool? allowVideoStreamCopy,
+            [FromQuery] bool? allowAudioStreamCopy,
+            [FromQuery] bool? breakOnNonKeyFrames,
+            [FromQuery] int? audioSampleRate,
+            [FromQuery] int? maxAudioBitDepth,
+            [FromQuery] int? audioBitRate,
+            [FromQuery] int? audioChannels,
+            [FromQuery] int? maxAudioChannels,
+            [FromQuery] string? profile,
+            [FromQuery] string? level,
+            [FromQuery] float? framerate,
+            [FromQuery] float? maxFramerate,
+            [FromQuery] bool? copyTimestamps,
+            [FromQuery] long? startTimeTicks,
+            [FromQuery] int? width,
+            [FromQuery] int? height,
+            [FromQuery] int? videoBitRate,
+            [FromQuery] int? subtitleStreamIndex,
+            [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+            [FromQuery] int? maxRefFrames,
+            [FromQuery] int? maxVideoBitDepth,
+            [FromQuery] bool? requireAvc,
+            [FromQuery] bool? deInterlace,
+            [FromQuery] bool? requireNonAnamorphic,
+            [FromQuery] int? transcodingMaxAudioChannels,
+            [FromQuery] int? cpuCoreLimit,
+            [FromQuery] string? liveStreamId,
+            [FromQuery] bool? enableMpegtsM2TsMode,
+            [FromQuery] string? videoCodec,
+            [FromQuery] string? subtitleCodec,
+            [FromQuery] string? transcodingReasons,
+            [FromQuery] int? audioStreamIndex,
+            [FromQuery] int? videoStreamIndex,
+            [FromQuery] EncodingContext context,
+            [FromQuery] Dictionary<string, string> streamOptions)
+        {
+            return GetVideoStream(
+                itemId,
+                container,
+                @static,
+                @params,
+                tag,
+                deviceProfileId,
+                playSessionId,
+                segmentContainer,
+                segmentLength,
+                minSegments,
+                mediaSourceId,
+                deviceId,
+                audioCodec,
+                enableAutoStreamCopy,
+                allowVideoStreamCopy,
+                allowAudioStreamCopy,
+                breakOnNonKeyFrames,
+                audioSampleRate,
+                maxAudioBitDepth,
+                audioBitRate,
+                audioChannels,
+                maxAudioChannels,
+                profile,
+                level,
+                framerate,
+                maxFramerate,
+                copyTimestamps,
+                startTimeTicks,
+                width,
+                height,
+                videoBitRate,
+                subtitleStreamIndex,
+                subtitleMethod,
+                maxRefFrames,
+                maxVideoBitDepth,
+                requireAvc,
+                deInterlace,
+                requireNonAnamorphic,
+                transcodingMaxAudioChannels,
+                cpuCoreLimit,
+                liveStreamId,
+                enableMpegtsM2TsMode,
+                videoCodec,
+                subtitleCodec,
+                transcodingReasons,
+                audioStreamIndex,
+                videoStreamIndex,
+                context,
+                streamOptions);
+        }
     }
 }

+ 1 - 1
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -1647,7 +1647,7 @@ namespace MediaBrowser.Model.Dlna
 
                                 // strip spaces to avoid having to encode
                                 var values = value
-                                    .Split('|', StringSplitOptions.RemoveEmptyEntries);
+                                    .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
 
                                 if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny)
                                 {