mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 18:08:53 -07:00
Merge remote-tracking branch 'upstream/master' into bad-route
This commit is contained in:
commit
efce4d4bf3
18
Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs
Normal file
18
Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
28
Jellyfin.Api/Attributes/ProducesFileAttribute.cs
Normal file
28
Jellyfin.Api/Attributes/ProducesFileAttribute.cs
Normal 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;
|
||||
}
|
||||
}
|
18
Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs
Normal file
18
Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
18
Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs
Normal file
18
Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
18
Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs
Normal file
18
Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
@ -89,6 +90,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[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,
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net.Mime;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.ConfigurationDtos;
|
||||
using MediaBrowser.Common.Json;
|
||||
@ -73,6 +75,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>Configuration.</returns>
|
||||
[HttpGet("Configuration/{key}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile(MediaTypeNames.Application.Json)]
|
||||
public ActionResult<object> GetNamedConfiguration([FromRoute, Required] string key)
|
||||
{
|
||||
return _configurationManager.GetConfiguration(key);
|
||||
|
@ -2,6 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Models;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller;
|
||||
@ -106,6 +108,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("web/ConfigurationPage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
|
||||
public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
|
||||
{
|
||||
IPlugin? plugin = null;
|
||||
|
@ -44,8 +44,9 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
|
||||
[HttpGet("{serverId}/description")]
|
||||
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
|
||||
{
|
||||
var url = GetAbsoluteUri();
|
||||
@ -63,8 +64,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("{serverId}/ContentDirectory")]
|
||||
[HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
|
||||
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
|
||||
{
|
||||
@ -79,8 +81,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("{serverId}/MediaReceiverRegistrar")]
|
||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
|
||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
|
||||
{
|
||||
@ -95,8 +98,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("{serverId}/ConnectionManager")]
|
||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
|
||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
|
||||
{
|
||||
@ -186,6 +190,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>Icon stream.</returns>
|
||||
[HttpGet("{serverId}/icons/{fileName}")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesImageFile]
|
||||
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
|
||||
{
|
||||
return GetIconInternal(fileName);
|
||||
@ -197,6 +203,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="fileName">The icon filename.</param>
|
||||
/// <returns>Icon stream.</returns>
|
||||
[HttpGet("icons/{fileName}")]
|
||||
[ProducesImageFile]
|
||||
public ActionResult GetIcon([FromRoute, Required] string fileName)
|
||||
{
|
||||
return GetIconInternal(fileName);
|
||||
|
@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.PlaybackDtos;
|
||||
@ -166,6 +167,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Videos/{itemId}/master.m3u8")]
|
||||
[HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesPlaylistFile]
|
||||
public async Task<ActionResult> GetMasterHlsVideoPlaylist(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string container,
|
||||
@ -333,6 +335,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Audio/{itemId}/master.m3u8")]
|
||||
[HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesPlaylistFile]
|
||||
public async Task<ActionResult> GetMasterHlsAudioPlaylist(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery, Required] string container,
|
||||
@ -498,6 +501,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("Videos/{itemId}/main.m3u8")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesPlaylistFile]
|
||||
public async Task<ActionResult> GetVariantHlsVideoPlaylist(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery, Required] string container,
|
||||
@ -663,6 +667,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("Audio/{itemId}/main.m3u8")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesPlaylistFile]
|
||||
public async Task<ActionResult> GetVariantHlsAudioPlaylist(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery, Required] string container,
|
||||
@ -830,6 +835,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesVideoFile]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> GetHlsVideoSegment(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
@ -999,6 +1005,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesAudioFile]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> GetHlsAudioSegment(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
|
@ -69,11 +69,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Validates path.
|
||||
/// </summary>
|
||||
/// <param name="validatePathDto">Validate request object.</param>
|
||||
/// <response code="200">Path validated.</response>
|
||||
/// <response code="204">Path validated.</response>
|
||||
/// <response code="404">Path not found.</response>
|
||||
/// <returns>Validation status.</returns>
|
||||
[HttpPost("ValidatePath")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto)
|
||||
{
|
||||
@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
return Ok();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
@ -55,6 +56,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
|
||||
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesAudioFile]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
||||
{
|
||||
@ -75,6 +77,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesPlaylistFile]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
||||
{
|
||||
@ -113,6 +116,7 @@ namespace Jellyfin.Api.Controllers
|
||||
// [Authenticated]
|
||||
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesVideoFile]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||
public ActionResult GetHlsVideoSegmentLegacy(
|
||||
[FromRoute, Required] string itemId,
|
||||
|
@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@ -65,7 +66,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<FileStreamResult> GetGeneralImage([FromRoute, Required] string name, [FromRoute, Required] string type)
|
||||
[ProducesImageFile]
|
||||
public ActionResult GetGeneralImage([FromRoute, Required] string name, [FromRoute, Required] string type)
|
||||
{
|
||||
var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
|
||||
? "folder"
|
||||
@ -110,7 +112,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<FileStreamResult> GetRatingImage(
|
||||
[ProducesImageFile]
|
||||
public ActionResult GetRatingImage(
|
||||
[FromRoute, Required] string theme,
|
||||
[FromRoute, Required] string name)
|
||||
{
|
||||
@ -143,7 +146,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<FileStreamResult> GetMediaInfoImage(
|
||||
[ProducesImageFile]
|
||||
public ActionResult GetMediaInfoImage(
|
||||
[FromRoute, Required] string theme,
|
||||
[FromRoute, Required] string name)
|
||||
{
|
||||
@ -157,7 +161,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="theme">Theme to search.</param>
|
||||
/// <param name="name">File name to search for.</param>
|
||||
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
|
||||
private ActionResult<FileStreamResult> GetImageFile(string basePath, string? theme, string? name)
|
||||
private ActionResult GetImageFile(string basePath, string? theme, string? name)
|
||||
{
|
||||
var themeFolder = Path.Combine(basePath, theme);
|
||||
if (Directory.Exists(themeFolder))
|
||||
@ -168,7 +172,7 @@ namespace Jellyfin.Api.Controllers
|
||||
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
|
||||
{
|
||||
var contentType = MimeTypes.GetMimeType(path);
|
||||
return File(System.IO.File.OpenRead(path), contentType);
|
||||
return PhysicalFile(path, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,7 +185,7 @@ namespace Jellyfin.Api.Controllers
|
||||
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
|
||||
{
|
||||
var contentType = MimeTypes.GetMimeType(path);
|
||||
return File(System.IO.File.OpenRead(path), contentType);
|
||||
return PhysicalFile(path, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@ -352,6 +353,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetItemImage(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
@ -430,6 +432,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetItemImage2(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
@ -508,6 +511,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetArtistImage(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
@ -586,6 +590,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetGenreImage(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
@ -664,6 +669,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetMusicGenreImage(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
@ -742,6 +748,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetPersonImage(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
@ -820,6 +827,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetStudioImage(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
@ -898,6 +906,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetUserImage(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
@ -1298,8 +1307,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read);
|
||||
return File(stream, imageContentType);
|
||||
return PhysicalFile(imagePath, imageContentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System.Net.Mime;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
@ -18,6 +19,7 @@ using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
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.
|
||||
/// </returns>
|
||||
[HttpGet("Items/RemoteSearch/Image")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetRemoteSearchImage(
|
||||
[FromQuery, Required] string imageUrl,
|
||||
[FromQuery, Required] string providerName)
|
||||
@ -260,8 +264,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
if (System.IO.File.Exists(contentPath))
|
||||
{
|
||||
await using var fileStreamExisting = System.IO.File.OpenRead(pointerCachePath);
|
||||
return new FileStreamResult(fileStreamExisting, MediaTypeNames.Application.Octet);
|
||||
return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath));
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
@ -274,10 +277,8 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
|
||||
|
||||
// Read the pointer file again
|
||||
await using var fileStream = System.IO.File.OpenRead(pointerCachePath);
|
||||
return new FileStreamResult(fileStream, MediaTypeNames.Application.Octet);
|
||||
var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -293,6 +294,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpPost("Items/RemoteSearch/Apply/{id}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> ApplySearchCriteria(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromBody, Required] RemoteSearchResult searchResult,
|
||||
|
@ -8,6 +8,7 @@ using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
@ -104,6 +105,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesFile("video/*", "audio/*")]
|
||||
public ActionResult GetFile([FromRoute, Required] Guid itemId)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
@ -112,8 +114,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
using var fileStream = new FileStream(item.Path, FileMode.Open, FileAccess.Read);
|
||||
return File(fileStream, MimeTypes.GetMimeType(item.Path));
|
||||
return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -618,6 +619,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.Download)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesFile("video/*", "audio/*")]
|
||||
public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
|
@ -10,6 +10,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
@ -1069,6 +1070,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile(MediaTypeNames.Application.Json)]
|
||||
public async Task<ActionResult> GetSchedulesDirectCountries()
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
@ -1177,6 +1179,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("LiveRecordings/{recordingId}/stream")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetLiveRecordingFile([FromRoute, Required] string recordingId)
|
||||
{
|
||||
var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
|
||||
@ -1207,6 +1210,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("LiveStreamFiles/{streamId}/stream.{container}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
|
||||
{
|
||||
var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false);
|
||||
|
@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.MediaInfoDtos;
|
||||
@ -286,6 +287,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesFile(MediaTypeNames.Application.Octet)]
|
||||
public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400)
|
||||
{
|
||||
const int MaxSize = 10_000_000;
|
||||
|
@ -7,6 +7,7 @@ using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
@ -155,6 +156,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetRemoteImage([FromQuery, Required] string imageUrl)
|
||||
{
|
||||
var urlHash = imageUrl.GetMD5();
|
||||
@ -192,7 +194,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
var contentType = MimeTypes.GetMimeType(contentPath);
|
||||
return File(System.IO.File.OpenRead(contentPath), contentType);
|
||||
return PhysicalFile(contentPath, contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -9,6 +9,7 @@ using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@ -162,6 +163,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesFile("text/*")]
|
||||
public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string id)
|
||||
{
|
||||
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}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile("text/*")]
|
||||
public async Task<ActionResult> GetSubtitle(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string mediaSourceId,
|
||||
@ -211,8 +214,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var subtitleStream = mediaSource.MediaStreams
|
||||
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
|
||||
|
||||
FileStream stream = new FileStream(subtitleStream.Path, FileMode.Open, FileAccess.Read);
|
||||
return File(stream, MimeTypes.GetMimeType(subtitleStream.Path));
|
||||
return PhysicalFile(subtitleStream.Path, MimeTypes.GetMimeType(subtitleStream.Path));
|
||||
}
|
||||
|
||||
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
|
||||
@ -251,6 +253,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesPlaylistFile]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> GetSubtitlePlaylist(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
|
@ -3,8 +3,10 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
@ -190,6 +192,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Logs/Log")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile(MediaTypeNames.Text.Plain)]
|
||||
public ActionResult GetLogFile([FromQuery, Required] string? name)
|
||||
{
|
||||
var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
|
||||
@ -197,7 +200,6 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
// For older files, assume fully static
|
||||
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");
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
@ -92,6 +93,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetUniversalAudioStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute] string? container,
|
||||
|
@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Deletes a user.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns>
|
||||
[HttpDelete("{userId}")]
|
||||
@ -255,7 +255,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</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="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>
|
||||
@ -313,7 +313,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</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="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>
|
||||
|
@ -5,6 +5,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.PlaybackDtos;
|
||||
@ -162,6 +163,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
|
||||
[HttpGet("Videos/{itemId}/live.m3u8")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesPlaylistFile]
|
||||
public async Task<ActionResult> GetLiveHlsStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] string? container,
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
@ -160,7 +161,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="NotFoundResult"/> if the video doesn't exist.</returns>
|
||||
[HttpDelete("{itemId}/AlternateSources")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteAlternateSources([FromRoute, Required] Guid itemId)
|
||||
{
|
||||
@ -330,6 +331,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")]
|
||||
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetVideoStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute] string? container,
|
||||
|
@ -16,8 +16,8 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Controllers;
|
||||
using Jellyfin.Server.Configuration;
|
||||
using Jellyfin.Server.Filters;
|
||||
using Jellyfin.Server.Formatters;
|
||||
using Jellyfin.Server.Middleware;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
@ -248,6 +248,8 @@ namespace Jellyfin.Server.Extensions
|
||||
|
||||
// TODO - remove when all types are supported in System.Text.Json
|
||||
c.AddSwaggerTypeMappings();
|
||||
|
||||
c.OperationFilter<FileResponseFilter>();
|
||||
});
|
||||
}
|
||||
|
||||
|
52
Jellyfin.Server/Filters/FileResponseFilter.cs
Normal file
52
Jellyfin.Server/Filters/FileResponseFilter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user