Set openapi schema type to file where possible

This commit is contained in:
crobibero 2020-09-01 17:26:49 -06:00
parent 2d198292a3
commit 2f33bee2a9
24 changed files with 232 additions and 24 deletions

View File

@ -0,0 +1,18 @@
namespace Jellyfin.Api.Attributes
{
/// <summary>
/// Produces file attribute of "image/*".
/// </summary>
public class ProducesAudioFileAttribute : ProducesFileAttribute
{
private const string ContentType = "audio/*";
/// <summary>
/// Initializes a new instance of the <see cref="ProducesAudioFileAttribute"/> class.
/// </summary>
public ProducesAudioFileAttribute()
: base(ContentType)
{
}
}
}

View File

@ -0,0 +1,28 @@
using System;
namespace Jellyfin.Api.Attributes
{
/// <summary>
/// Internal produces image attribute.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class ProducesFileAttribute : Attribute
{
private readonly string[] _contentTypes;
/// <summary>
/// Initializes a new instance of the <see cref="ProducesFileAttribute"/> class.
/// </summary>
/// <param name="contentTypes">Content types this endpoint produces.</param>
public ProducesFileAttribute(params string[] contentTypes)
{
_contentTypes = contentTypes;
}
/// <summary>
/// Gets the configured content types.
/// </summary>
/// <returns>the configured content types.</returns>
public string[] GetContentTypes() => _contentTypes;
}
}

View File

@ -0,0 +1,18 @@
namespace Jellyfin.Api.Attributes
{
/// <summary>
/// Produces file attribute of "image/*".
/// </summary>
public class ProducesImageFileAttribute : ProducesFileAttribute
{
private const string ContentType = "image/*";
/// <summary>
/// Initializes a new instance of the <see cref="ProducesImageFileAttribute"/> class.
/// </summary>
public ProducesImageFileAttribute()
: base(ContentType)
{
}
}
}

View File

@ -0,0 +1,18 @@
namespace Jellyfin.Api.Attributes
{
/// <summary>
/// Produces file attribute of "image/*".
/// </summary>
public class ProducesPlaylistFileAttribute : ProducesFileAttribute
{
private const string ContentType = "application/x-mpegURL";
/// <summary>
/// Initializes a new instance of the <see cref="ProducesPlaylistFileAttribute"/> class.
/// </summary>
public ProducesPlaylistFileAttribute()
: base(ContentType)
{
}
}
}

View File

@ -0,0 +1,18 @@
namespace Jellyfin.Api.Attributes
{
/// <summary>
/// Produces file attribute of "video/*".
/// </summary>
public class ProducesVideoFileAttribute : ProducesFileAttribute
{
private const string ContentType = "video/*";
/// <summary>
/// Initializes a new instance of the <see cref="ProducesVideoFileAttribute"/> class.
/// </summary>
public ProducesVideoFileAttribute()
: base(ContentType)
{
}
}
}

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
@ -88,6 +89,7 @@ namespace Jellyfin.Api.Controllers
[HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")] [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")] [HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStream( public async Task<ActionResult> GetAudioStream(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
[FromRoute] string? container, [FromRoute] string? container,

View File

@ -1,6 +1,8 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Net.Mime;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.ConfigurationDtos; using Jellyfin.Api.Models.ConfigurationDtos;
using MediaBrowser.Common.Json; using MediaBrowser.Common.Json;
@ -73,6 +75,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>Configuration.</returns> /// <returns>Configuration.</returns>
[HttpGet("Configuration/{key}")] [HttpGet("Configuration/{key}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile(MediaTypeNames.Application.Json)]
public ActionResult<object> GetNamedConfiguration([FromRoute] string? key) public ActionResult<object> GetNamedConfiguration([FromRoute] string? key)
{ {
return _configurationManager.GetConfiguration(key); return _configurationManager.GetConfiguration(key);

View File

@ -2,6 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mime;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Models; using Jellyfin.Api.Models;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -123,6 +125,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("web/ConfigurationPage")] [HttpGet("web/ConfigurationPage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
public ActionResult GetDashboardConfigurationPage([FromQuery] string? name) public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
{ {
IPlugin? plugin = null; IPlugin? plugin = null;

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Net.Mime;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna; using Emby.Dlna;
using Emby.Dlna.Main; using Emby.Dlna.Main;
@ -17,8 +19,6 @@ namespace Jellyfin.Api.Controllers
[Route("Dlna")] [Route("Dlna")]
public class DlnaServerController : BaseJellyfinApiController public class DlnaServerController : BaseJellyfinApiController
{ {
private const string XMLContentType = "text/xml; charset=UTF-8";
private readonly IDlnaManager _dlnaManager; private readonly IDlnaManager _dlnaManager;
private readonly IContentDirectory _contentDirectory; private readonly IContentDirectory _contentDirectory;
private readonly IConnectionManager _connectionManager; private readonly IConnectionManager _connectionManager;
@ -44,7 +44,8 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns> /// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
[HttpGet("{serverId}/description")] [HttpGet("{serverId}/description")]
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
[Produces(XMLContentType)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult GetDescriptionXml([FromRoute] string serverId) public ActionResult GetDescriptionXml([FromRoute] string serverId)
{ {
@ -62,7 +63,8 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns> /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
[HttpGet("{serverId}/ContentDirectory")] [HttpGet("{serverId}/ContentDirectory")]
[HttpGet("{serverId}/ContentDirectory.xml", Name = "GetContentDirectory_2")] [HttpGet("{serverId}/ContentDirectory.xml", Name = "GetContentDirectory_2")]
[Produces(XMLContentType)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetContentDirectory([FromRoute] string serverId) public ActionResult GetContentDirectory([FromRoute] string serverId)
@ -77,7 +79,8 @@ namespace Jellyfin.Api.Controllers
/// <returns>Dlna media receiver registrar xml.</returns> /// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/MediaReceiverRegistrar")] [HttpGet("{serverId}/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")] [HttpGet("{serverId}/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
[Produces(XMLContentType)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId) public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId)
@ -92,7 +95,8 @@ namespace Jellyfin.Api.Controllers
/// <returns>Dlna media receiver registrar xml.</returns> /// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/ConnectionManager")] [HttpGet("{serverId}/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager.xml", Name = "GetConnectionManager_2")] [HttpGet("{serverId}/ConnectionManager.xml", Name = "GetConnectionManager_2")]
[Produces(XMLContentType)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetConnectionManager([FromRoute] string serverId) public ActionResult GetConnectionManager([FromRoute] string serverId)
@ -183,7 +187,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>Icon stream.</returns> /// <returns>Icon stream.</returns>
[HttpGet("{serverId}/icons/{fileName}")] [HttpGet("{serverId}/icons/{fileName}")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName) [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesImageFile]
public ActionResult<FileResult> GetIconId([FromRoute] string serverId, [FromRoute] string fileName)
{ {
return GetIconInternal(fileName); return GetIconInternal(fileName);
} }
@ -194,12 +200,13 @@ namespace Jellyfin.Api.Controllers
/// <param name="fileName">The icon filename.</param> /// <param name="fileName">The icon filename.</param>
/// <returns>Icon stream.</returns> /// <returns>Icon stream.</returns>
[HttpGet("icons/{fileName}")] [HttpGet("icons/{fileName}")]
public ActionResult GetIcon([FromRoute] string fileName) [ProducesImageFile]
public ActionResult<FileResult> GetIcon([FromRoute] string fileName)
{ {
return GetIconInternal(fileName); return GetIconInternal(fileName);
} }
private ActionResult GetIconInternal(string fileName) private ActionResult<FileResult> GetIconInternal(string fileName)
{ {
var icon = _dlnaManager.GetIcon(fileName); var icon = _dlnaManager.GetIcon(fileName);
if (icon == null) if (icon == null)

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.PlaybackDtos;
@ -166,6 +167,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Videos/{itemId}/master.m3u8")] [HttpGet("Videos/{itemId}/master.m3u8")]
[HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")] [HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesPlaylistFile]
public async Task<ActionResult> GetMasterHlsVideoPlaylist( public async Task<ActionResult> GetMasterHlsVideoPlaylist(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
[FromRoute] string? container, [FromRoute] string? container,
@ -333,6 +335,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Audio/{itemId}/master.m3u8")] [HttpGet("Audio/{itemId}/master.m3u8")]
[HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")] [HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesPlaylistFile]
public async Task<ActionResult> GetMasterHlsAudioPlaylist( public async Task<ActionResult> GetMasterHlsAudioPlaylist(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
[FromRoute] string? container, [FromRoute] string? container,
@ -498,6 +501,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Videos/{itemId}/main.m3u8")] [HttpGet("Videos/{itemId}/main.m3u8")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesPlaylistFile]
public async Task<ActionResult> GetVariantHlsVideoPlaylist( public async Task<ActionResult> GetVariantHlsVideoPlaylist(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
[FromRoute] string? container, [FromRoute] string? container,
@ -663,6 +667,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/main.m3u8")] [HttpGet("Audio/{itemId}/main.m3u8")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesPlaylistFile]
public async Task<ActionResult> GetVariantHlsAudioPlaylist( public async Task<ActionResult> GetVariantHlsAudioPlaylist(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
[FromRoute] string? container, [FromRoute] string? container,
@ -830,6 +835,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] [HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesVideoFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> GetHlsVideoSegment( public async Task<ActionResult> GetHlsVideoSegment(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
@ -999,6 +1005,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesAudioFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> GetHlsAudioSegment( public async Task<ActionResult> GetHlsAudioSegment(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,

View File

@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@ -54,6 +55,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")] [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")] [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesAudioFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId) public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId)
{ {
@ -74,6 +76,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")] [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesPlaylistFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsPlaylistLegacy([FromRoute] string itemId, [FromRoute] string playlistId) public ActionResult GetHlsPlaylistLegacy([FromRoute] string itemId, [FromRoute] string playlistId)
{ {
@ -112,6 +115,7 @@ namespace Jellyfin.Api.Controllers
// [Authenticated] // [Authenticated]
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")] [HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesVideoFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsVideoSegmentLegacy( public ActionResult GetHlsVideoSegmentLegacy(
[FromRoute] string itemId, [FromRoute] string itemId,

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -351,6 +352,7 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")] [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetItemImage( public async Task<ActionResult> GetItemImage(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
[FromRoute] ImageType imageType, [FromRoute] ImageType imageType,
@ -507,6 +509,7 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")] [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetArtistImage( public async Task<ActionResult> GetArtistImage(
[FromRoute] string name, [FromRoute] string name,
[FromRoute] ImageType imageType, [FromRoute] ImageType imageType,
@ -585,6 +588,7 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")] [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetGenreImage( public async Task<ActionResult> GetGenreImage(
[FromRoute] string name, [FromRoute] string name,
[FromRoute] ImageType imageType, [FromRoute] ImageType imageType,
@ -663,6 +667,7 @@ namespace Jellyfin.Api.Controllers
[HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")] [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetMusicGenreImage( public async Task<ActionResult> GetMusicGenreImage(
[FromRoute] string name, [FromRoute] string name,
[FromRoute] ImageType imageType, [FromRoute] ImageType imageType,
@ -741,6 +746,7 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")] [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetPersonImage( public async Task<ActionResult> GetPersonImage(
[FromRoute] string name, [FromRoute] string name,
[FromRoute] ImageType imageType, [FromRoute] ImageType imageType,
@ -819,6 +825,7 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")] [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetStudioImage( public async Task<ActionResult> GetStudioImage(
[FromRoute] string name, [FromRoute] string name,
[FromRoute] ImageType imageType, [FromRoute] ImageType imageType,
@ -897,6 +904,7 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")] [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetUserImage( public async Task<ActionResult> GetUserImage(
[FromRoute] Guid userId, [FromRoute] Guid userId,
[FromRoute] ImageType imageType, [FromRoute] ImageType imageType,
@ -1297,8 +1305,7 @@ namespace Jellyfin.Api.Controllers
return NoContent(); return NoContent();
} }
var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read); return PhysicalFile(imagePath, imageContentType);
return File(stream, imageContentType);
} }
} }
} }

View File

@ -7,6 +7,7 @@ using System.Net.Mime;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -18,6 +19,7 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -248,6 +250,8 @@ namespace Jellyfin.Api.Controllers
/// The task result contains an <see cref="FileStreamResult"/> containing the images file stream. /// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
/// </returns> /// </returns>
[HttpGet("Items/RemoteSearch/Image")] [HttpGet("Items/RemoteSearch/Image")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesImageFile]
public async Task<ActionResult> GetRemoteSearchImage( public async Task<ActionResult> GetRemoteSearchImage(
[FromQuery, Required] string imageUrl, [FromQuery, Required] string imageUrl,
[FromQuery, Required] string providerName) [FromQuery, Required] string providerName)
@ -274,10 +278,7 @@ namespace Jellyfin.Api.Controllers
} }
await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
return PhysicalFile(pointerCachePath, MimeTypes.GetMimeType(pointerCachePath));
// Read the pointer file again
await using var fileStream = System.IO.File.OpenRead(pointerCachePath);
return new FileStreamResult(fileStream, MediaTypeNames.Application.Octet);
} }
/// <summary> /// <summary>
@ -293,6 +294,7 @@ namespace Jellyfin.Api.Controllers
/// </returns> /// </returns>
[HttpPost("Items/RemoteSearch/Apply/{id}")] [HttpPost("Items/RemoteSearch/Apply/{id}")]
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> ApplySearchCriteria( public async Task<ActionResult> ApplySearchCriteria(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
[FromBody, Required] RemoteSearchResult searchResult, [FromBody, Required] RemoteSearchResult searchResult,

View File

@ -8,6 +8,7 @@ using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
@ -104,6 +105,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesFile("video/*", "audio/*")]
public ActionResult GetFile([FromRoute] Guid itemId) public ActionResult GetFile([FromRoute] Guid itemId)
{ {
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);
@ -618,6 +620,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.Download)] [Authorize(Policy = Policies.Download)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesFile("video/*", "audio/*")]
public async Task<ActionResult> GetDownload([FromRoute] Guid itemId) public async Task<ActionResult> GetDownload([FromRoute] Guid itemId)
{ {
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);

View File

@ -9,6 +9,7 @@ using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
@ -1067,6 +1068,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("ListingProviders/SchedulesDirect/Countries")] [HttpGet("ListingProviders/SchedulesDirect/Countries")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile(MediaTypeNames.Application.Json)]
public async Task<ActionResult> GetSchedulesDirectCountries() public async Task<ActionResult> GetSchedulesDirectCountries()
{ {
var client = _httpClientFactory.CreateClient(); var client = _httpClientFactory.CreateClient();
@ -1175,6 +1177,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("LiveRecordings/{recordingId}/stream")] [HttpGet("LiveRecordings/{recordingId}/stream")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
public async Task<ActionResult> GetLiveRecordingFile([FromRoute] string recordingId) public async Task<ActionResult> GetLiveRecordingFile([FromRoute] string recordingId)
{ {
var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId); var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
@ -1205,6 +1208,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("LiveStreamFiles/{streamId}/stream.{container}")] [HttpGet("LiveStreamFiles/{streamId}/stream.{container}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
public async Task<ActionResult> GetLiveStreamFile([FromRoute] string streamId, [FromRoute] string container) public async Task<ActionResult> GetLiveStreamFile([FromRoute] string streamId, [FromRoute] string container)
{ {
var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false); var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false);

View File

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Net.Mime; using System.Net.Mime;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos; using Jellyfin.Api.Models.MediaInfoDtos;
@ -286,6 +287,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[Produces(MediaTypeNames.Application.Octet)] [Produces(MediaTypeNames.Application.Octet)]
[ProducesFile(MediaTypeNames.Application.Octet)]
public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400) public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400)
{ {
const int MaxSize = 10_000_000; const int MaxSize = 10_000_000;

View File

@ -9,6 +9,7 @@ using System.Net.Mime;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -162,6 +163,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Application.Octet)] [Produces(MediaTypeNames.Application.Octet)]
[ProducesFile("text/*")]
public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string? id) public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string? id)
{ {
var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false); var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
@ -185,6 +187,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")] [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile("text/*")]
public async Task<ActionResult> GetSubtitle( public async Task<ActionResult> GetSubtitle(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromRoute, Required] string? mediaSourceId, [FromRoute, Required] string? mediaSourceId,
@ -251,6 +254,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesPlaylistFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> GetSubtitlePlaylist( public async Task<ActionResult> GetSubtitlePlaylist(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,

View File

@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mime;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
@ -190,16 +192,13 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Logs/Log")] [HttpGet("Logs/Log")]
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile(MediaTypeNames.Text.Plain)]
public ActionResult GetLogFile([FromQuery, Required] string? name) public ActionResult GetLogFile([FromQuery, Required] string? name)
{ {
var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
.First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
// For older files, assume fully static return File(file.FullName, MediaTypeNames.Text.Plain);
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare);
return File(stream, "text/plain");
} }
/// <summary> /// <summary>

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
@ -91,6 +92,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status302Found)] [ProducesResponseType(StatusCodes.Status302Found)]
[ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream( public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
[FromRoute] string? container, [FromRoute] string? container,

View File

@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers
/// Deletes a user. /// Deletes a user.
/// </summary> /// </summary>
/// <param name="userId">The user id.</param> /// <param name="userId">The user id.</param>
/// <response code="200">User deleted.</response> /// <response code="204">User deleted.</response>
/// <response code="404">User not found.</response> /// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns>
[HttpDelete("{userId}")] [HttpDelete("{userId}")]
@ -255,7 +255,7 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="userId">The user id.</param> /// <param name="userId">The user id.</param>
/// <param name="request">The <see cref="UpdateUserPassword"/> request.</param> /// <param name="request">The <see cref="UpdateUserPassword"/> request.</param>
/// <response code="200">Password successfully reset.</response> /// <response code="204">Password successfully reset.</response>
/// <response code="403">User is not allowed to update the password.</response> /// <response code="403">User is not allowed to update the password.</response>
/// <response code="404">User not found.</response> /// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
@ -313,7 +313,7 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="userId">The user id.</param> /// <param name="userId">The user id.</param>
/// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param> /// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param>
/// <response code="200">Password successfully reset.</response> /// <response code="204">Password successfully reset.</response>
/// <response code="403">User is not allowed to update the password.</response> /// <response code="403">User is not allowed to update the password.</response>
/// <response code="404">User not found.</response> /// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>

View File

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.PlaybackDtos;
@ -161,6 +162,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileResult"/> containing the hls file.</returns> /// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
[HttpGet("Videos/{itemId}/live.m3u8")] [HttpGet("Videos/{itemId}/live.m3u8")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesPlaylistFile]
public async Task<ActionResult> GetLiveHlsStream( public async Task<ActionResult> GetLiveHlsStream(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
[FromQuery] string? container, [FromQuery] string? container,

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
@ -159,7 +160,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="NotFoundResult"/> if the video doesn't exist.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="NotFoundResult"/> if the video doesn't exist.</returns>
[HttpDelete("{itemId}/AlternateSources")] [HttpDelete("{itemId}/AlternateSources")]
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteAlternateSources([FromRoute] Guid itemId) public async Task<ActionResult> DeleteAlternateSources([FromRoute] Guid itemId)
{ {
@ -326,6 +327,7 @@ namespace Jellyfin.Api.Controllers
[HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")] [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")]
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")] [HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesVideoFile]
public async Task<ActionResult> GetVideoStream( public async Task<ActionResult> GetVideoStream(
[FromRoute] Guid itemId, [FromRoute] Guid itemId,
[FromRoute] string? container, [FromRoute] string? container,

View File

@ -16,6 +16,7 @@ using Jellyfin.Api.Auth.LocalAccessPolicy;
using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers; using Jellyfin.Api.Controllers;
using Jellyfin.Server.Filters;
using Jellyfin.Server.Formatters; using Jellyfin.Server.Formatters;
using Jellyfin.Server.Models; using Jellyfin.Server.Models;
using MediaBrowser.Common; using MediaBrowser.Common;
@ -249,6 +250,8 @@ namespace Jellyfin.Server.Extensions
// TODO - remove when all types are supported in System.Text.Json // TODO - remove when all types are supported in System.Text.Json
c.AddSwaggerTypeMappings(); c.AddSwaggerTypeMappings();
c.OperationFilter<FileResponseFilter>();
}); });
} }

View File

@ -0,0 +1,52 @@
using System;
using System.Linq;
using Jellyfin.Api.Attributes;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters
{
/// <inheritdoc />
public class FileResponseFilter : IOperationFilter
{
private const string SuccessCode = "200";
private static readonly OpenApiMediaType _openApiMediaType = new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = "file"
}
};
/// <inheritdoc />
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
foreach (var attribute in context.ApiDescription.ActionDescriptor.EndpointMetadata)
{
if (attribute is ProducesFileAttribute producesFileAttribute)
{
// Get operation response values.
var (_, value) = operation.Responses
.FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal));
// Operation doesn't have a response.
if (value == null)
{
continue;
}
// Clear existing responses.
value.Content.Clear();
// Add all content-types as file.
foreach (var contentType in producesFileAttribute.GetContentTypes())
{
value.Content.Add(contentType, _openApiMediaType);
}
break;
}
}
}
}
}