diff options
Diffstat (limited to 'Jellyfin.Api/Controllers')
40 files changed, 724 insertions, 814 deletions
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index b429cebec..ae45f647f 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers { return await _activityManager.GetPagedResultAsync(new ActivityLogQuery { - StartIndex = startIndex, + Skip = startIndex, Limit = limit, MinDate = minDate, HasUserId = hasUserId diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index 8c43d786a..720b22b1d 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -1,10 +1,8 @@ using System; using System.ComponentModel.DataAnnotations; -using System.Globalization; +using System.Threading.Tasks; using Jellyfin.Api.Constants; -using MediaBrowser.Controller; using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -18,24 +16,15 @@ namespace Jellyfin.Api.Controllers [Route("Auth")] public class ApiKeyController : BaseJellyfinApiController { - private readonly ISessionManager _sessionManager; - private readonly IServerApplicationHost _appHost; - private readonly IAuthenticationRepository _authRepo; + private readonly IAuthenticationManager _authenticationManager; /// <summary> /// Initializes a new instance of the <see cref="ApiKeyController"/> class. /// </summary> - /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param> - /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> - /// <param name="authRepo">Instance of <see cref="IAuthenticationRepository"/> interface.</param> - public ApiKeyController( - ISessionManager sessionManager, - IServerApplicationHost appHost, - IAuthenticationRepository authRepo) + /// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param> + public ApiKeyController(IAuthenticationManager authenticationManager) { - _sessionManager = sessionManager; - _appHost = appHost; - _authRepo = authRepo; + _authenticationManager = authenticationManager; } /// <summary> @@ -46,14 +35,15 @@ namespace Jellyfin.Api.Controllers [HttpGet("Keys")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<AuthenticationInfo>> GetKeys() + public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys() { - var result = _authRepo.Get(new AuthenticationInfoQuery - { - HasUser = false - }); + var keys = await _authenticationManager.GetApiKeys(); - return result; + return new QueryResult<AuthenticationInfo> + { + Items = keys, + TotalRecordCount = keys.Count + }; } /// <summary> @@ -65,17 +55,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Keys")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateKey([FromQuery, Required] string app) + public async Task<ActionResult> CreateKey([FromQuery, Required] string app) { - _authRepo.Create(new AuthenticationInfo - { - AppName = app, - AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), - DateCreated = DateTime.UtcNow, - DeviceId = _appHost.SystemId, - DeviceName = _appHost.FriendlyName, - AppVersion = _appHost.ApplicationVersionString - }); + await _authenticationManager.CreateApiKey(app).ConfigureAwait(false); + return NoContent(); } @@ -88,9 +71,10 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Keys/{key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RevokeKey([FromRoute, Required] string key) + public async Task<ActionResult> RevokeKey([FromRoute, Required] string key) { - _sessionManager.RevokeToken(key); + await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false); + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 4b2e5e7ea..154a56702 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -77,6 +77,8 @@ namespace Jellyfin.Api.Controllers /// <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="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Total record count.</param> /// <response code="200">Artists returned.</response> @@ -112,6 +114,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -150,7 +154,8 @@ namespace Jellyfin.Api.Controllers MinCommunityRating = minCommunityRating, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) @@ -276,6 +281,8 @@ namespace Jellyfin.Api.Controllers /// <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="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Total record count.</param> /// <response code="200">Album artists returned.</response> @@ -311,6 +318,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -349,7 +358,8 @@ namespace Jellyfin.Api.Controllers MinCommunityRating = minCommunityRating, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 8b1813b20..54ac06276 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers /// <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="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, vp8, vp9, vpx (deprecated), wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="transcodeReasons">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> @@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -168,7 +168,7 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, @@ -177,13 +177,13 @@ namespace Jellyfin.Api.Controllers SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, @@ -241,7 +241,7 @@ namespace Jellyfin.Api.Controllers /// <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="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, vp8, vp9, vpx (deprecated), wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="transcodeReasons">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> @@ -309,7 +309,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -333,7 +333,7 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, @@ -342,13 +342,13 @@ namespace Jellyfin.Api.Controllers SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 852d1e9cb..8a98d856c 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -58,7 +58,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? parentId, [FromQuery] bool isLocked = false) { - var userId = _authContext.GetAuthorizationInfo(Request).UserId; + var userId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).UserId; var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions { diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 049a4bed7..60529e990 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Text.Json; @@ -5,7 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; -using MediaBrowser.Common.Json; +using Jellyfin.Extensions.Json; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; @@ -94,6 +95,11 @@ namespace Jellyfin.Api.Controllers { var configurationType = _configurationManager.GetConfigurationType(key); var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false); + if (configuration == null) + { + throw new ArgumentException("Body doesn't contain a valid configuration"); + } + _configurationManager.SaveConfiguration(key, configuration); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index b3e3490c2..8292cf83b 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,8 +1,11 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Data.Dtos; +using Jellyfin.Data.Entities.Security; +using Jellyfin.Data.Queries; using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Querying; @@ -19,22 +22,18 @@ namespace Jellyfin.Api.Controllers public class DevicesController : BaseJellyfinApiController { private readonly IDeviceManager _deviceManager; - private readonly IAuthenticationRepository _authenticationRepository; private readonly ISessionManager _sessionManager; /// <summary> /// Initializes a new instance of the <see cref="DevicesController"/> class. /// </summary> /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param> - /// <param name="authenticationRepository">Instance of <see cref="IAuthenticationRepository"/> interface.</param> /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param> public DevicesController( IDeviceManager deviceManager, - IAuthenticationRepository authenticationRepository, ISessionManager sessionManager) { _deviceManager = deviceManager; - _authenticationRepository = authenticationRepository; _sessionManager = sessionManager; } @@ -47,10 +46,9 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns> [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { - var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; - return _deviceManager.GetDevices(deviceQuery); + return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false); } /// <summary> @@ -63,9 +61,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("Info")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id) + public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id) { - var deviceInfo = _deviceManager.GetDevice(id); + var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false); if (deviceInfo == null) { return NotFound(); @@ -84,9 +82,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("Options")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id) + public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id) { - var deviceInfo = _deviceManager.GetDeviceOptions(id); + var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false); if (deviceInfo == null) { return NotFound(); @@ -101,22 +99,14 @@ namespace Jellyfin.Api.Controllers /// <param name="id">Device Id.</param> /// <param name="deviceOptions">Device Options.</param> /// <response code="204">Device options updated.</response> - /// <response code="404">Device not found.</response> - /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> + /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Options")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateDeviceOptions( + public async Task<ActionResult> UpdateDeviceOptions( [FromQuery, Required] string id, - [FromBody, Required] DeviceOptions deviceOptions) + [FromBody, Required] DeviceOptionsDto deviceOptions) { - var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); - if (existingDeviceOptions == null) - { - return NotFound(); - } - - _deviceManager.UpdateDeviceOptions(id, deviceOptions); + await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false); return NoContent(); } @@ -130,19 +120,19 @@ namespace Jellyfin.Api.Controllers [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteDevice([FromQuery, Required] string id) + public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id) { - var existingDevice = _deviceManager.GetDevice(id); + var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false); if (existingDevice == null) { return NotFound(); } - var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; + var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false); - foreach (var session in sessions) + foreach (var session in sessions.Items) { - _sessionManager.Logout(session); + await _sessionManager.Logout(session).ConfigureAwait(false); } return NoContent(); diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index f6c23c5aa..fbfb47215 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -28,7 +28,6 @@ using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -52,8 +51,6 @@ namespace Jellyfin.Api.Controllers private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ILogger<DynamicHlsController> _logger; @@ -72,12 +69,11 @@ namespace Jellyfin.Api.Controllers /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param> /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public DynamicHlsController( ILibraryManager libraryManager, IUserManager userManager, @@ -87,15 +83,12 @@ namespace Jellyfin.Api.Controllers IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, ILogger<DynamicHlsController> logger, - DynamicHlsHelper dynamicHlsHelper) + DynamicHlsHelper dynamicHlsHelper, + EncodingHelper encodingHelper) { - _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); - _libraryManager = libraryManager; _userManager = userManager; _dlnaManager = dlnaManager; @@ -104,12 +97,12 @@ namespace Jellyfin.Api.Controllers _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _logger = logger; _dynamicHlsHelper = dynamicHlsHelper; + _encodingHelper = encodingHelper; + _encodingOptions = serverConfigurationManager.GetEncodingOptions(); } @@ -157,7 +150,7 @@ namespace Jellyfin.Api.Controllers /// <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="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, vp8, vp9, vpx (deprecated), wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="transcodeReasons">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> @@ -225,7 +218,7 @@ namespace Jellyfin.Api.Controllers var streamingRequest = new HlsVideoRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -249,7 +242,7 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, @@ -258,13 +251,13 @@ namespace Jellyfin.Api.Controllers SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, @@ -323,7 +316,7 @@ namespace Jellyfin.Api.Controllers /// <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="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, vp8, vp9, vpx (deprecated), wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="transcodeReasons">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> @@ -392,7 +385,7 @@ namespace Jellyfin.Api.Controllers var streamingRequest = new HlsAudioRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -416,7 +409,7 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, @@ -425,13 +418,13 @@ namespace Jellyfin.Api.Controllers SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, @@ -489,7 +482,7 @@ namespace Jellyfin.Api.Controllers /// <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="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, vp8, vp9, vpx (deprecated), wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="transcodeReasons">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> @@ -551,11 +544,11 @@ namespace Jellyfin.Api.Controllers [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new VideoRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -579,7 +572,7 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, @@ -588,13 +581,13 @@ namespace Jellyfin.Api.Controllers SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, @@ -716,11 +709,11 @@ namespace Jellyfin.Api.Controllers [FromQuery] EncodingContext? context, [FromQuery] Dictionary<string, string> streamOptions) { - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new StreamingRequestDto { Id = itemId, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -744,7 +737,7 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, @@ -753,13 +746,13 @@ namespace Jellyfin.Api.Controllers SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, @@ -820,7 +813,7 @@ namespace Jellyfin.Api.Controllers /// <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="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, vp8, vp9, vpx (deprecated), wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="transcodeReasons">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> @@ -890,7 +883,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -914,7 +907,7 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, @@ -923,13 +916,13 @@ namespace Jellyfin.Api.Controllers SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, @@ -1062,7 +1055,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -1086,7 +1079,7 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, @@ -1095,13 +1088,13 @@ namespace Jellyfin.Api.Controllers SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, @@ -1126,9 +1119,7 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, @@ -1146,7 +1137,7 @@ namespace Jellyfin.Api.Controllers var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase); var hlsVersion = isHlsInFmp4 ? "7" : "3"; - var builder = new StringBuilder(); + var builder = new StringBuilder(128); builder.AppendLine("#EXTM3U") .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") @@ -1199,6 +1190,7 @@ namespace Jellyfin.Api.Controllers throw new ArgumentException("StartTimeTicks is not allowed."); } + // CTS lifecycle is managed internally. var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; @@ -1211,14 +1203,12 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, TranscodingJobType, - cancellationTokenSource.Token) + cancellationToken) .ConfigureAwait(false); var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); @@ -1237,7 +1227,7 @@ namespace Jellyfin.Api.Controllers } var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); - await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); + await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false); var released = false; var startTranscoding = false; @@ -1333,24 +1323,28 @@ namespace Jellyfin.Api.Controllers return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); } - private double[] GetSegmentLengths(StreamState state) - { - var result = new List<double>(); + private static double[] GetSegmentLengths(StreamState state) + => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength); - var ticks = state.RunTimeTicks ?? 0; - - var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks; + internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength) + { + var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks; + var wholeSegments = runtimeTicks / segmentLengthTicks; + var remainingTicks = runtimeTicks % segmentLengthTicks; - while (ticks > 0) + var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1); + var segments = new double[segmentsLen]; + for (int i = 0; i < wholeSegments; i++) { - var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks; - - result.Add(TimeSpan.FromTicks(length).TotalSeconds); + segments[i] = segmentlength; + } - ticks -= length; + if (remainingTicks != 0) + { + segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds; } - return result.ToArray(); + return segments; } private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber) @@ -1386,18 +1380,13 @@ namespace Jellyfin.Api.Controllers } else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) { - var outputFmp4HeaderArg = string.Empty; - var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - if (isWindows) + var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch { // on Windows, the path of fmp4 header file needs to be configured - outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\""; - } - else - { + true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"", // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder - outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""; - } + false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"" + }; segmentFormat = "fmp4" + outputFmp4HeaderArg; } @@ -1507,7 +1496,7 @@ namespace Jellyfin.Api.Controllers args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } - args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true); + args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions); return args; } @@ -1772,9 +1761,9 @@ namespace Jellyfin.Api.Controllers private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem) { - var folder = Path.GetDirectoryName(playlist); + var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist)); - var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty; + var filePrefix = Path.GetFileNameWithoutExtension(playlist); try { diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 7bcf4674c..5aa457153 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -63,6 +63,8 @@ namespace Jellyfin.Api.Controllers /// <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="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Optional. Include total record count.</param> /// <response code="200">Genres returned.</response> @@ -84,6 +86,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -107,7 +111,8 @@ namespace Jellyfin.Api.Controllers NameStartsWithOrGreater = nameStartsWithOrGreater, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index d0ed45acb..473bdc523 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -61,7 +61,13 @@ namespace Jellyfin.Api.Controllers { // TODO: Deprecate with new iOS app var file = segmentId + Path.GetExtension(Request.Path); - file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); + var transcodePath = _serverConfigurationManager.GetTranscodePath(); + file = Path.GetFullPath(Path.Combine(transcodePath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath)) + { + return BadRequest("Invalid segment."); + } return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext); } @@ -81,7 +87,13 @@ namespace Jellyfin.Api.Controllers public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId) { var file = playlistId + Path.GetExtension(Request.Path); - file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); + var transcodePath = _serverConfigurationManager.GetTranscodePath(); + file = Path.GetFullPath(Path.Combine(transcodePath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8") + { + return BadRequest("Invalid segment."); + } return GetFileResult(file, file); } @@ -130,7 +142,12 @@ namespace Jellyfin.Api.Controllers var file = segmentId + Path.GetExtension(Request.Path); var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); - file = Path.Combine(transcodeFolderPath, file); + file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath)) + { + return BadRequest("Invalid segment."); + } var normalizedPlaylistId = playlistId; diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 198dbc51f..e1b808098 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers : type; var path = BaseItem.SupportedImageExtensions - .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i)) + .Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i))) .FirstOrDefault(System.IO.File.Exists); if (path == null) @@ -82,6 +82,11 @@ namespace Jellyfin.Api.Controllers return NotFound(); } + if (!path.StartsWith(_applicationPaths.GeneralPath)) + { + return BadRequest("Invalid image path."); + } + var contentType = MimeTypes.GetMimeType(path); return File(System.IO.File.OpenRead(path), contentType); } @@ -163,7 +168,8 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns> private ActionResult GetImageFile(string basePath, string theme, string? name) { - var themeFolder = Path.Combine(basePath, theme); + var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme)); + if (Directory.Exists(themeFolder)) { var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i)) @@ -171,12 +177,18 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { + if (!path.StartsWith(basePath)) + { + return BadRequest("Invalid image path."); + } + var contentType = MimeTypes.GetMimeType(path); + return PhysicalFile(path, contentType); } } - var allFolder = Path.Combine(basePath, "all"); + var allFolder = Path.GetFullPath(Path.Combine(basePath, "all")); if (Directory.Exists(allFolder)) { var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i)) @@ -184,6 +196,11 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { + if (!path.StartsWith(basePath)) + { + return BadRequest("Invalid image path."); + } + var contentType = MimeTypes.GetMimeType(path); return PhysicalFile(path, contentType); } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 89037749a..9dc280e13 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromQuery] int? index = null) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } @@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromRoute] int index) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } @@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromQuery] int? index = null) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); } @@ -234,7 +234,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromRoute] int index) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); } @@ -490,6 +490,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -519,8 +521,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, [FromQuery] string? tag, - [FromQuery] bool? cropWhitespace, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] ImageFormat? format, [FromQuery] bool? addPlayedIndicator, [FromQuery] double? percentPlayed, @@ -549,7 +553,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -570,6 +575,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -599,8 +606,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, [FromQuery] string? tag, - [FromQuery] bool? cropWhitespace, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] ImageFormat? format, [FromQuery] bool? addPlayedIndicator, [FromQuery] double? percentPlayed, @@ -628,7 +637,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -648,6 +658,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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">Determines the output format of the image - original,gif,jpg,png.</param> @@ -677,8 +689,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, [FromRoute, Required] string tag, - [FromQuery] bool? cropWhitespace, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromRoute, Required] ImageFormat format, [FromQuery] bool? addPlayedIndicator, [FromRoute, Required] double percentPlayed, @@ -707,7 +721,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -731,6 +746,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -760,7 +777,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -786,7 +805,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -810,6 +830,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -839,7 +861,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -865,7 +889,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -890,6 +915,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -919,7 +946,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -944,7 +973,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -968,6 +998,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -997,7 +1029,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1023,7 +1057,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1048,6 +1083,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -1077,7 +1114,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1102,7 +1141,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1126,6 +1166,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -1155,7 +1197,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1181,7 +1225,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1206,6 +1251,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -1235,7 +1282,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1260,7 +1309,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1284,6 +1334,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -1313,7 +1365,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1339,7 +1393,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1364,6 +1419,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -1393,7 +1450,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1418,7 +1477,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1442,6 +1502,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -1471,7 +1533,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1514,7 +1578,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1540,6 +1605,8 @@ namespace Jellyfin.Api.Controllers /// <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="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</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> @@ -1569,7 +1636,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, - [FromQuery] bool? cropWhitespace, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, [FromQuery] bool? addPlayedIndicator, [FromQuery] int? blur, [FromQuery] string? backgroundColor, @@ -1611,7 +1680,8 @@ namespace Jellyfin.Api.Controllers width, height, quality, - cropWhitespace, + fillWidth, + fillHeight, addPlayedIndicator, blur, backgroundColor, @@ -1695,7 +1765,8 @@ namespace Jellyfin.Api.Controllers int? width, int? height, int? quality, - bool? cropWhitespace, + int? fillWidth, + int? fillHeight, bool? addPlayedIndicator, int? blur, string? backgroundColor, @@ -1737,8 +1808,6 @@ namespace Jellyfin.Api.Controllers } } - cropWhitespace ??= imageType == ImageType.Logo || imageType == ImageType.Art; - var outputFormats = GetOutputFormats(format); TimeSpan? cacheDuration = null; @@ -1758,11 +1827,13 @@ namespace Jellyfin.Api.Controllers item, itemId, imageIndex, + width, height, - maxHeight, maxWidth, + maxHeight, + fillWidth, + fillHeight, quality, - width, addPlayedIndicator, percentPlayed, unplayedCount, @@ -1770,7 +1841,6 @@ namespace Jellyfin.Api.Controllers backgroundColor, foregroundLayer, imageInfo, - cropWhitespace.Value, outputFormats, cacheDuration, responseHeaders, @@ -1789,17 +1859,15 @@ namespace Jellyfin.Api.Controllers private ImageFormat[] GetClientSupportedFormats() { - var acceptTypes = Request.Headers[HeaderNames.Accept]; - var supportedFormats = new List<string>(); - if (acceptTypes.Count > 0) + var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); + for (var i = 0; i < supportedFormats.Length; i++) { - foreach (var type in acceptTypes) + // Remove charsets etc. (anything after semi-colon) + var type = supportedFormats[i]; + int index = type.IndexOf(';', StringComparison.Ordinal); + if (index != -1) { - int index = type.IndexOf(';', StringComparison.Ordinal); - if (index != -1) - { - supportedFormats.Add(type.Substring(0, index)); - } + supportedFormats[i] = type.Substring(0, index); } } @@ -1857,11 +1925,13 @@ namespace Jellyfin.Api.Controllers BaseItem? item, Guid itemId, int? index, + int? width, int? height, - int? maxHeight, int? maxWidth, + int? maxHeight, + int? fillWidth, + int? fillHeight, int? quality, - int? width, bool? addPlayedIndicator, double? percentPlayed, int? unplayedCount, @@ -1869,7 +1939,6 @@ namespace Jellyfin.Api.Controllers string? backgroundColor, string? foregroundLayer, ItemImageInfo imageInfo, - bool cropWhitespace, IReadOnlyCollection<ImageFormat> supportedFormats, TimeSpan? cacheDuration, IDictionary<string, string> headers, @@ -1882,7 +1951,6 @@ namespace Jellyfin.Api.Controllers var options = new ImageProcessingOptions { - CropWhiteSpace = cropWhitespace, Height = height, ImageIndex = index ?? 0, Image = imageInfo, @@ -1890,6 +1958,8 @@ namespace Jellyfin.Api.Controllers ItemId = itemId, MaxHeight = maxHeight, MaxWidth = maxWidth, + FillHeight = fillHeight, + FillWidth = fillWidth, Quality = quality ?? 100, Width = width, AddPlayedIndicator = addPlayedIndicator ?? false, diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index f232dffaa..4774ed4ef 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -229,42 +229,6 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Creates an instant playlist based on a given genre. - /// </summary> - /// <param name="id">The item id.</param> - /// <param name="userId">Optional. Filter by user id, and attach user data.</param> - /// <param name="limit">Optional. The maximum number of records to return.</param> - /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> - /// <param name="enableImages">Optional. Include image information in output.</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> - /// <response code="200">Instant playlist returned.</response> - /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> - [HttpGet("MusicGenres/{id}/InstantMix")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById( - [FromRoute, Required] Guid id, - [FromQuery] Guid? userId, - [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] bool? enableImages, - [FromQuery] bool? enableUserData, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) - { - var item = _libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); - var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); - return GetResult(items, user, limit, dtoOptions); - } - - /// <summary> /// Creates an instant playlist based on a given item. /// </summary> /// <param name="id">The item id.</param> @@ -352,8 +316,7 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> [HttpGet("MusicGenres/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] - [Obsolete("Use GetInstantMixFromMusicGenres instead")] - public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById2( + public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById( [FromQuery, Required] Guid id, [FromQuery] Guid? userId, [FromQuery] int? limit, @@ -363,15 +326,15 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - return GetInstantMixFromMusicGenreById( - id, - userId, - limit, - fields, - enableImages, - enableUserData, - imageTypeLimit, - enableImageTypes); + var item = _libraryManager.GetItemById(id); + var user = userId.HasValue && !userId.Equals(Guid.Empty) + ? _userManager.GetUserById(userId.Value) + : null; + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(Request) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); + var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); + return GetResult(items, user, limit, dtoOptions); } private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions) diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index dabd4deb7..9fa307858 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -238,48 +238,6 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Gets a remote image. - /// </summary> - /// <param name="imageUrl">The image url.</param> - /// <param name="providerName">The provider name.</param> - /// <response code="200">Remote image retrieved.</response> - /// <returns> - /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. - /// 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) - { - var urlHash = imageUrl.GetMD5(); - var pointerCachePath = GetFullCachePath(urlHash.ToString()); - - try - { - var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - if (System.IO.File.Exists(contentPath)) - { - return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath)); - } - } - catch (FileNotFoundException) - { - // Means the file isn't cached yet - } - catch (IOException) - { - // Means the file isn't cached yet - } - - await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); - var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath)); - } - - /// <summary> /// Applies search criteria to an item and refreshes metadata. /// </summary> /// <param name="itemId">Item id.</param> @@ -320,54 +278,5 @@ namespace Jellyfin.Api.Controllers return NoContent(); } - - /// <summary> - /// Downloads the image. - /// </summary> - /// <param name="providerName">Name of the provider.</param> - /// <param name="url">The URL.</param> - /// <param name="urlHash">The URL hash.</param> - /// <param name="pointerCachePath">The pointer cache path.</param> - /// <returns>Task.</returns> - private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath) - { - using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false); - if (result.Content.Headers.ContentType?.MediaType == null) - { - throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType)); - } - - var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1]; - var fullCachePath = GetFullCachePath(urlHash + "." + ext); - - var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid."); - Directory.CreateDirectory(directory); - using (var stream = result.Content) - { - // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using var fileStream = new FileStream( - fullCachePath, - FileMode.Create, - FileAccess.Write, - FileShare.None, - IODefaults.FileStreamBufferSize, - true); - - await stream.CopyToAsync(fileStream).ConfigureAwait(false); - } - - var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath)); - - Directory.CreateDirectory(pointerCacheDirectory); - await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false); - } - - /// <summary> - /// Gets the full cache path. - /// </summary> - /// <param name="filename">The filename.</param> - /// <returns>System.String.</returns> - private string GetFullCachePath(string filename) - => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); } } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index a9f4a5a58..64d7b2f3e 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -154,11 +154,11 @@ namespace Jellyfin.Api.Controllers }; if (!item.IsVirtualItem - && !(item is ICollectionFolder) - && !(item is UserView) - && !(item is AggregateFolder) - && !(item is LiveTvChannel) - && !(item is IItemByName) + && item is not ICollectionFolder + && item is not UserView + && item is not AggregateFolder + && item is not LiveTvChannel + && item is not IItemByName && item.SourceType == SourceType.Library) { var inheritedContentType = _libraryManager.GetInheritedContentType(item); diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 2c9760f6d..52eefc5c2 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -143,7 +143,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Items")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetItems( - [FromQuery] Guid? userId, + [FromQuery] Guid userId, [FromQuery] string? maxOfficialRating, [FromQuery] bool? hasThemeSong, [FromQuery] bool? hasThemeVideo, @@ -224,8 +224,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) + var user = !userId.Equals(Guid.Empty) + ? _userManager.GetUserById(userId) : null; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) @@ -241,13 +241,18 @@ namespace Jellyfin.Api.Controllers var item = _libraryManager.GetParentItem(parentId, userId); QueryResult<BaseItem> result; - if (!(item is Folder folder)) + if (item is not Folder folder) { folder = _libraryManager.GetUserRootFolder(); } - if (folder is IHasCollectionType hasCollectionType - && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + string? collectionType = null; + if (folder is IHasCollectionType hasCollectionType) + { + collectionType = hasCollectionType.CollectionType; + } + + if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) { recursive = true; includeItemTypes = new[] { BaseItemKind.Playlist }; @@ -270,16 +275,17 @@ namespace Jellyfin.Api.Controllers } } - if (!(item is UserRootFolder) + if (item is not UserRootFolder && !isInEnabledFolder && !user.HasPermission(PermissionKind.EnableAllFolders) - && !user.HasPermission(PermissionKind.EnableAllChannels)) + && !user.HasPermission(PermissionKind.EnableAllChannels) + && !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase)) { _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name); return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); } - if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder)) + if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder) { var query = new InternalItemsQuery(user!) { diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index f8e8825ef..0be853ca4 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -114,7 +114,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path)); + return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true); } /// <summary> @@ -331,10 +331,10 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public ActionResult DeleteItem(Guid itemId) + public async Task<ActionResult> DeleteItem(Guid itemId) { var item = _libraryManager.GetItemById(itemId); - var auth = _authContext.GetAuthorizationInfo(Request); + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var user = auth.User; if (!item.CanDelete(user)) @@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + public async Task<ActionResult> DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) { if (ids.Length == 0) { @@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers foreach (var i in ids) { var item = _libraryManager.GetItemById(i); - var auth = _authContext.GetAuthorizationInfo(Request); + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var user = auth.User; if (!item.CanDelete(user)) @@ -600,7 +600,7 @@ namespace Jellyfin.Api.Controllers { foreach (var item in dto.Updates) { - _libraryMonitor.ReportFileSystemChanged(item.Path); + _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null.")); } return NoContent(); @@ -627,7 +627,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - var auth = _authContext.GetAuthorizationInfo(Request); + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var user = auth.User; @@ -666,7 +666,7 @@ namespace Jellyfin.Api.Controllers } // TODO determine non-ASCII validity. - return PhysicalFile(path, MimeTypes.GetMimeType(path), filename); + return PhysicalFile(path, MimeTypes.GetMimeType(path), filename, true); } /// <summary> @@ -700,7 +700,7 @@ namespace Jellyfin.Api.Controllers : _libraryManager.RootFolder) : _libraryManager.GetItemById(itemId); - if (item is Episode || (item is IItemByName && !(item is MusicArtist))) + if (item is Episode || (item is IItemByName && item is not MusicArtist)) { return new QueryResult<BaseItemDto>(); } diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index be9127dd3..ec1170411 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers if (paths != null && paths.Length > 0) { - libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray(); + libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray(); } await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); @@ -212,7 +212,7 @@ namespace Jellyfin.Api.Controllers try { - var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo { Path = mediaPathDto.Path }; + var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null.")); _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath); } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 24ee833ef..b20eae750 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -429,10 +429,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Tuners/{tunerId}/Reset")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.DefaultAuthorization)] - public ActionResult ResetTuner([FromRoute, Required] string tunerId) + public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId) { - AssertUserCanManageLiveTv(); - _liveTvManager.ResetTuner(tunerId, CancellationToken.None); + await AssertUserCanManageLiveTv().ConfigureAwait(false); + await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -761,9 +761,9 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId) + public async Task<ActionResult> DeleteRecording([FromRoute, Required] Guid recordingId) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); var item = _libraryManager.GetItemById(recordingId); if (item == null) @@ -790,7 +790,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false); return NoContent(); } @@ -808,7 +808,7 @@ namespace Jellyfin.Api.Controllers [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -824,7 +824,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -882,7 +882,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false); return NoContent(); } @@ -900,7 +900,7 @@ namespace Jellyfin.Api.Controllers [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -916,7 +916,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo) { - AssertUserCanManageLiveTv(); + await AssertUserCanManageLiveTv().ConfigureAwait(false); await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -1172,7 +1172,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesVideoFile] - public async Task<ActionResult> GetLiveRecordingFile([FromRoute, Required] string recordingId) + public ActionResult GetLiveRecordingFile([FromRoute, Required] string recordingId) { var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId); @@ -1181,11 +1181,8 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - await using var memoryStream = new MemoryStream(); - await new ProgressiveFileCopier(path, null, _transcodingJobHelper, CancellationToken.None) - .WriteToAsync(memoryStream, CancellationToken.None) - .ConfigureAwait(false); - return File(memoryStream, MimeTypes.GetMimeType(path)); + var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper); + return new FileStreamResult(stream, MimeTypes.GetMimeType(path)); } /// <summary> @@ -1215,9 +1212,9 @@ namespace Jellyfin.Api.Controllers return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container)); } - private void AssertUserCanManageLiveTv() + private async Task AssertUserCanManageLiveTv() { - var user = _sessionContext.GetUser(Request); + var user = await _sessionContext.GetUser(Request).ConfigureAwait(false); if (user == null) { diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index e330f02b6..7c78928f7 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ParameterObsolete] bool? allowAudioStreamCopy, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto) { - var authInfo = _authContext.GetAuthorizationInfo(Request); + var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var profile = playbackInfoDto?.DeviceProfile; _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile); @@ -161,6 +161,11 @@ namespace Jellyfin.Api.Controllers liveStreamId) .ConfigureAwait(false); + if (info.ErrorCode != null) + { + return info; + } + if (profile != null) { // set device specific data @@ -302,27 +307,12 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="size">The bitrate. Defaults to 102400.</param> /// <response code="200">Test buffer returned.</response> - /// <response code="400">Size has to be a numer between 0 and 10,000,000.</response> /// <returns>A <see cref="FileResult"/> with specified bitrate.</returns> [HttpGet("Playback/BitrateTest")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [Produces(MediaTypeNames.Application.Octet)] [ProducesFile(MediaTypeNames.Application.Octet)] - public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400) + public ActionResult GetBitrateTestBytes([FromQuery][Range(1, 100_000_000, ErrorMessage = "The requested size must be greater than or equal to {1} and less than or equal to {2}")] int size = 102400) { - const int MaxSize = 10_000_000; - - if (size <= 0) - { - return BadRequest($"The requested size ({size}) is equal to or smaller than 0."); - } - - if (size > MaxSize) - { - return BadRequest($"The requested size ({size}) is larger than the max allowed value ({MaxSize})."); - } - byte[] buffer = ArrayPool<byte>.Shared.Rent(size); try { diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 4d788ad7d..010a3b19a 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -18,6 +18,7 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers @@ -89,7 +90,7 @@ namespace Jellyfin.Api.Controllers // nameof(LiveTvProgram) }, // IsMovie = true - OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) }, Limit = 7, ParentId = parentIdGuid, Recursive = true, @@ -110,7 +111,7 @@ namespace Jellyfin.Api.Controllers { IncludeItemTypes = itemTypes.ToArray(), IsMovie = true, - OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, Limit = 10, IsFavoriteOrLiked = true, ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(), @@ -120,7 +121,7 @@ namespace Jellyfin.Api.Controllers DtoOptions = dtoOptions }); - var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList(); + var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6)); // Get recently played directors var recentDirectors = GetDirectors(mostRecentMovies) .ToList(); @@ -191,7 +192,8 @@ namespace Jellyfin.Api.Controllers foreach (var name in names) { - var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + var items = _libraryManager.GetItemList( + new InternalItemsQuery(user) { Person = name, // Account for duplicates by imdb id, since the database doesn't support this yet @@ -299,9 +301,8 @@ namespace Jellyfin.Api.Controllers private IEnumerable<string> GetActors(IEnumerable<BaseItem> items) { - var people = _libraryManager.GetPeople(new InternalPeopleQuery + var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director }) { - ExcludePersonTypes = new[] { PersonType.Director }, MaxListOrder = 3 }); @@ -315,10 +316,9 @@ namespace Jellyfin.Api.Controllers private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items) { - var people = _libraryManager.GetPeople(new InternalPeopleQuery - { - PersonTypes = new[] { PersonType.Director } - }); + var people = _libraryManager.GetPeople(new InternalPeopleQuery( + new[] { PersonType.Director }, + Array.Empty<string>())); var itemIds = items.Select(i => i.Id).ToList(); diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 7f7058b5e..27eec2b9a 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -63,6 +63,8 @@ namespace Jellyfin.Api.Controllers /// <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="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> + /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableTotalRecordCount">Optional. Include total record count.</param> /// <response code="200">Music genres returned.</response> @@ -84,6 +86,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { @@ -107,7 +111,8 @@ namespace Jellyfin.Api.Controllers NameStartsWithOrGreater = nameStartsWithOrGreater, DtoOptions = dtoOptions, SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) }; if (parentId.HasValue) diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 70a94e27c..b98307f87 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -94,10 +94,10 @@ namespace Jellyfin.Api.Controllers } var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); - var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery + var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery( + personTypes, + excludePersonTypes) { - PersonTypes = personTypes, - ExcludePersonTypes = excludePersonTypes, NameContains = searchTerm, User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index f256c8c25..6dee1c219 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -72,13 +72,13 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> [HttpPost("Users/{userId}/PlayedItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<UserItemDataDto> MarkPlayedItem( + public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem( [FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed) { var user = _userManager.GetUserById(userId); - var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); var dto = UpdatePlayedStatus(user, itemId, true, datePlayed); foreach (var additionalUserInfo in session.AdditionalUsers) { @@ -98,10 +98,10 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> [HttpDelete("Users/{userId}/PlayedItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); - var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); var dto = UpdatePlayedStatus(user, itemId, false, null); foreach (var additionalUserInfo in session.AdditionalUsers) { @@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo) { playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); - playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); return NoContent(); } @@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo) { playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); - playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); return NoContent(); } @@ -171,10 +171,11 @@ namespace Jellyfin.Api.Controllers _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) { - await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); + await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); } - playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); return NoContent(); } @@ -220,7 +221,7 @@ namespace Jellyfin.Api.Controllers }; playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); - playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); return NoContent(); } @@ -278,7 +279,7 @@ namespace Jellyfin.Api.Controllers }; playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); - playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); return NoContent(); } @@ -320,10 +321,11 @@ namespace Jellyfin.Api.Controllers _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) { - await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); + await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); } - playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 24285bfb9..0ae6109bc 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -8,11 +8,10 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.PluginDtos; +using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Json; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; using Microsoft.AspNetCore.Authorization; @@ -208,12 +207,7 @@ namespace Jellyfin.Api.Controllers var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)); // Select the un-instanced one first. - var plugin = plugins.FirstOrDefault(p => p.Instance == null); - if (plugin == null) - { - // Then by the status. - plugin = plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault(); - } + var plugin = plugins.FirstOrDefault(p => p.Instance == null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault(); if (plugin != null) { diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index 4ac849181..87b78fe93 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -1,7 +1,10 @@ using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Net; using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Model.QuickConnect; using Microsoft.AspNetCore.Authorization; @@ -16,27 +19,29 @@ namespace Jellyfin.Api.Controllers public class QuickConnectController : BaseJellyfinApiController { private readonly IQuickConnect _quickConnect; + private readonly IAuthorizationContext _authContext; /// <summary> /// Initializes a new instance of the <see cref="QuickConnectController"/> class. /// </summary> /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param> - public QuickConnectController(IQuickConnect quickConnect) + /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> + public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext) { _quickConnect = quickConnect; + _authContext = authContext; } /// <summary> /// Gets the current quick connect state. /// </summary> /// <response code="200">Quick connect state returned.</response> - /// <returns>The current <see cref="QuickConnectState"/>.</returns> - [HttpGet("Status")] + /// <returns>Whether Quick Connect is enabled on the server or not.</returns> + [HttpGet("Enabled")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QuickConnectState> GetStatus() + public ActionResult<bool> GetEnabled() { - _quickConnect.ExpireRequests(); - return _quickConnect.State; + return _quickConnect.IsEnabled; } /// <summary> @@ -47,9 +52,17 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns> [HttpGet("Initiate")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QuickConnectResult> Initiate() + public async Task<ActionResult<QuickConnectResult>> Initiate() { - return _quickConnect.TryConnect(); + try + { + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); + return _quickConnect.TryConnect(auth); + } + catch (AuthenticationException) + { + return Unauthorized("Quick connect is disabled"); + } } /// <summary> @@ -72,42 +85,10 @@ namespace Jellyfin.Api.Controllers { return NotFound("Unknown secret"); } - } - - /// <summary> - /// Temporarily activates quick connect for five minutes. - /// </summary> - /// <response code="204">Quick connect has been temporarily activated.</response> - /// <response code="403">Quick connect is unavailable on this server.</response> - /// <returns>An <see cref="NoContentResult"/> on success.</returns> - [HttpPost("Activate")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public ActionResult Activate() - { - if (_quickConnect.State == QuickConnectState.Unavailable) + catch (AuthenticationException) { - return StatusCode(StatusCodes.Status403Forbidden, "Quick connect is unavailable"); + return Unauthorized("Quick connect is disabled"); } - - _quickConnect.Activate(); - return NoContent(); - } - - /// <summary> - /// Enables or disables quick connect. - /// </summary> - /// <param name="status">New <see cref="QuickConnectState"/>.</param> - /// <response code="204">Quick connect state set successfully.</response> - /// <returns>An <see cref="NoContentResult"/> on success.</returns> - [HttpPost("Available")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available) - { - _quickConnect.SetState(status); - return NoContent(); } /// <summary> @@ -121,7 +102,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public ActionResult<bool> Authorize([FromQuery, Required] string code) + public async Task<ActionResult<bool>> Authorize([FromQuery, Required] string code) { var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); if (!userId.HasValue) @@ -129,26 +110,14 @@ namespace Jellyfin.Api.Controllers return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id"); } - return _quickConnect.AuthorizeRequest(userId.Value, code); - } - - /// <summary> - /// Deauthorize all quick connect devices for the current user. - /// </summary> - /// <response code="200">All quick connect devices were deleted.</response> - /// <returns>The number of devices that were deleted.</returns> - [HttpPost("Deauthorize")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<int> Deauthorize() - { - var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); - if (!userId.HasValue) + try { - return 0; + return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false); + } + catch (AuthenticationException) + { + return Unauthorized("Quick connect is disabled"); } - - return _quickConnect.DeleteAllDevices(userId.Value); } } } diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index e226adc64..ec836f43e 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -146,58 +146,6 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Gets a remote image. - /// </summary> - /// <param name="imageUrl">The image url.</param> - /// <response code="200">Remote image returned.</response> - /// <response code="404">Remote image not found.</response> - /// <returns>Image Stream.</returns> - [HttpGet("Images/Remote")] - [Produces(MediaTypeNames.Application.Octet)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl) - { - var urlHash = imageUrl.ToString().GetMD5(); - var pointerCachePath = GetFullCachePath(urlHash.ToString()); - - string? contentPath = null; - var hasFile = false; - - try - { - contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - if (System.IO.File.Exists(contentPath)) - { - hasFile = true; - } - } - catch (FileNotFoundException) - { - // The file isn't cached yet - } - catch (IOException) - { - // The file isn't cached yet - } - - if (!hasFile) - { - await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); - contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); - } - - if (string.IsNullOrEmpty(contentPath)) - { - return NotFound(); - } - - var contentType = MimeTypes.GetMimeType(contentPath); - return PhysicalFile(contentPath, contentType); - } - - /// <summary> /// Downloads a remote image for an item. /// </summary> /// <param name="itemId">Item Id.</param> diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 6c22050a7..73bdf9018 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -228,10 +228,7 @@ namespace Jellyfin.Api.Controllers itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb); } - if (itemWithImage == null) - { - itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Thumb); - } + itemWithImage ??= GetParentWithImage<BaseItem>(item, ImageType.Thumb); if (itemWithImage != null) { diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index e2269a2ce..3a04cb3a4 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -124,7 +125,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/Viewing")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult DisplayContent( + public async Task<ActionResult> DisplayContent( [FromRoute, Required] string sessionId, [FromQuery, Required] string itemType, [FromQuery, Required] string itemId, @@ -137,11 +138,12 @@ namespace Jellyfin.Api.Controllers ItemType = itemType }; - _sessionManager.SendBrowseCommand( - RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, + await _sessionManager.SendBrowseCommand( + await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), sessionId, command, - CancellationToken.None); + CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -153,29 +155,42 @@ namespace Jellyfin.Api.Controllers /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param> /// <param name="itemIds">The ids of the items to play, comma delimited.</param> /// <param name="startPositionTicks">The starting position of the first item.</param> + /// <param name="mediaSourceId">Optional. The media source id.</param> + /// <param name="audioStreamIndex">Optional. The index of the audio stream to play.</param> + /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to play.</param> + /// <param name="startIndex">Optional. The start index.</param> /// <response code="204">Instruction sent to session.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Sessions/{sessionId}/Playing")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Play( + public async Task<ActionResult> Play( [FromRoute, Required] string sessionId, [FromQuery, Required] PlayCommand playCommand, [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds, - [FromQuery] long? startPositionTicks) + [FromQuery] long? startPositionTicks, + [FromQuery] string? mediaSourceId, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] int? startIndex) { var playRequest = new PlayRequest { ItemIds = itemIds, StartPositionTicks = startPositionTicks, - PlayCommand = playCommand + PlayCommand = playCommand, + MediaSourceId = mediaSourceId, + AudioStreamIndex = audioStreamIndex, + SubtitleStreamIndex = subtitleStreamIndex, + StartIndex = startIndex }; - _sessionManager.SendPlayCommand( - RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, + await _sessionManager.SendPlayCommand( + await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), sessionId, playRequest, - CancellationToken.None); + CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -192,14 +207,14 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/Playing/{command}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SendPlaystateCommand( + public async Task<ActionResult> SendPlaystateCommand( [FromRoute, Required] string sessionId, [FromRoute, Required] PlaystateCommand command, [FromQuery] long? seekPositionTicks, [FromQuery] string? controllingUserId) { - _sessionManager.SendPlaystateCommand( - RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, + await _sessionManager.SendPlaystateCommand( + await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), sessionId, new PlaystateRequest() { @@ -207,7 +222,8 @@ namespace Jellyfin.Api.Controllers ControllingUserId = controllingUserId, SeekPositionTicks = seekPositionTicks, }, - CancellationToken.None); + CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -222,18 +238,18 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/System/{command}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SendSystemCommand( + public async Task<ActionResult> SendSystemCommand( [FromRoute, Required] string sessionId, [FromRoute, Required] GeneralCommandType command) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); var generalCommand = new GeneralCommand { Name = command, ControllingUserId = currentSession.UserId }; - _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None); + await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None); return NoContent(); } @@ -248,11 +264,11 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/Command/{command}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SendGeneralCommand( + public async Task<ActionResult> SendGeneralCommand( [FromRoute, Required] string sessionId, [FromRoute, Required] GeneralCommandType command) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); var generalCommand = new GeneralCommand { @@ -260,7 +276,8 @@ namespace Jellyfin.Api.Controllers ControllingUserId = currentSession.UserId }; - _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None); + await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -275,11 +292,12 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/{sessionId}/Command")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SendFullGeneralCommand( + public async Task<ActionResult> SendFullGeneralCommand( [FromRoute, Required] string sessionId, [FromBody, Required] GeneralCommand command) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request) + .ConfigureAwait(false); if (command == null) { @@ -288,11 +306,12 @@ namespace Jellyfin.Api.Controllers command.ControllingUserId = currentSession.UserId; - _sessionManager.SendGeneralCommand( + await _sessionManager.SendGeneralCommand( currentSession.Id, sessionId, command, - CancellationToken.None); + CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -301,28 +320,27 @@ namespace Jellyfin.Api.Controllers /// Issues a command to a client to display a message to the user. /// </summary> /// <param name="sessionId">The session id.</param> - /// <param name="text">The message test.</param> - /// <param name="header">The message header.</param> - /// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param> + /// <param name="command">The <see cref="MessageCommand" /> object containing Header, Message Text, and TimeoutMs.</param> /// <response code="204">Message sent.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Sessions/{sessionId}/Message")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SendMessageCommand( + public async Task<ActionResult> SendMessageCommand( [FromRoute, Required] string sessionId, - [FromQuery, Required] string text, - [FromQuery] string? header, - [FromQuery] long? timeoutMs) + [FromBody, Required] MessageCommand command) { - var command = new MessageCommand + if (string.IsNullOrWhiteSpace(command.Header)) { - Header = string.IsNullOrEmpty(header) ? "Message from Server" : header, - TimeoutMs = timeoutMs, - Text = text - }; + command.Header = "Message from Server"; + } - _sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None); + await _sessionManager.SendMessageCommand( + await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), + sessionId, + command, + CancellationToken.None) + .ConfigureAwait(false); return NoContent(); } @@ -377,7 +395,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/Capabilities")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostCapabilities( + public async Task<ActionResult> PostCapabilities( [FromQuery] string? id, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, @@ -387,7 +405,7 @@ namespace Jellyfin.Api.Controllers { if (string.IsNullOrWhiteSpace(id)) { - id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); } _sessionManager.ReportCapabilities(id, new ClientCapabilities @@ -411,13 +429,13 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/Capabilities/Full")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostFullCapabilities( + public async Task<ActionResult> PostFullCapabilities( [FromQuery] string? id, [FromBody, Required] ClientCapabilitiesDto capabilities) { if (string.IsNullOrWhiteSpace(id)) { - id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); } _sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities()); @@ -435,11 +453,11 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/Viewing")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult ReportViewing( + public async Task<ActionResult> ReportViewing( [FromQuery] string? sessionId, [FromQuery, Required] string? itemId) { - string session = sessionId ?? RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); _sessionManager.ReportNowViewingItem(session, itemId); return NoContent(); @@ -453,11 +471,11 @@ namespace Jellyfin.Api.Controllers [HttpPost("Sessions/Logout")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult ReportSessionEnded() + public async Task<ActionResult> ReportSessionEnded() { - AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request); + AuthorizationInfo auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - _sessionManager.Logout(auth.Token); + await _sessionManager.Logout(auth.Token).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 16a47f2d8..11f67ee89 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -182,6 +182,10 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Gets subtitles in a specified format. /// </summary> + /// <param name="routeItemId">The (route) item id.</param> + /// <param name="routeMediaSourceId">The (route) media source id.</param> + /// <param name="routeIndex">The (route) subtitle stream index.</param> + /// <param name="routeFormat">The (route) format of the returned subtitle.</param> /// <param name="itemId">The item id.</param> /// <param name="mediaSourceId">The media source id.</param> /// <param name="index">The subtitle stream index.</param> @@ -189,22 +193,32 @@ namespace Jellyfin.Api.Controllers /// <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> - /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param> + /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param> /// <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/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile("text/*")] public async Task<ActionResult> GetSubtitle( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] string mediaSourceId, - [FromRoute, Required] int index, - [FromRoute, Required] string format, + [FromRoute, Required] Guid routeItemId, + [FromRoute, Required] string routeMediaSourceId, + [FromRoute, Required] int routeIndex, + [FromRoute, Required] string routeFormat, + [FromQuery, ParameterObsolete] Guid? itemId, + [FromQuery, ParameterObsolete] string? mediaSourceId, + [FromQuery, ParameterObsolete] int? index, + [FromQuery, ParameterObsolete] string? format, [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps = false, [FromQuery] bool addVttTimeMap = false, [FromQuery] long startPositionTicks = 0) { + // Set parameters to route value if not provided via query. + itemId ??= routeItemId; + mediaSourceId ??= routeMediaSourceId; + index ??= routeIndex; + format ??= routeFormat; + if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase)) { format = "json"; @@ -212,9 +226,9 @@ namespace Jellyfin.Api.Controllers if (string.IsNullOrEmpty(format)) { - var item = (Video)_libraryManager.GetItemById(itemId); + var item = (Video)_libraryManager.GetItemById(itemId.Value); - var idString = itemId.ToString("N", CultureInfo.InvariantCulture); + var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture); var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false) .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal)); @@ -226,7 +240,7 @@ namespace Jellyfin.Api.Controllers if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap) { - await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); + await using Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); using var reader = new StreamReader(stream); var text = await reader.ReadToEndAsync().ConfigureAwait(false); @@ -238,9 +252,9 @@ namespace Jellyfin.Api.Controllers return File( await EncodeSubtitles( - itemId, + itemId.Value, mediaSourceId, - index, + index.Value, format, startPositionTicks, endPositionTicks, @@ -251,30 +265,44 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Gets subtitles in a specified format. /// </summary> + /// <param name="routeItemId">The (route) item id.</param> + /// <param name="routeMediaSourceId">The (route) media source id.</param> + /// <param name="routeIndex">The (route) subtitle stream index.</param> + /// <param name="routeStartPositionTicks">The (route) start position of the subtitle in ticks.</param> + /// <param name="routeFormat">The (route) format of the returned subtitle.</param> /// <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="startPositionTicks">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}")] + [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")] [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, + [FromRoute, Required] Guid routeItemId, + [FromRoute, Required] string routeMediaSourceId, + [FromRoute, Required] int routeIndex, + [FromRoute, Required] long routeStartPositionTicks, + [FromRoute, Required] string routeFormat, + [FromQuery, ParameterObsolete] Guid? itemId, + [FromQuery, ParameterObsolete] string? mediaSourceId, + [FromQuery, ParameterObsolete] int? index, + [FromQuery, ParameterObsolete] long? startPositionTicks, + [FromQuery, ParameterObsolete] string? format, [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps = false, [FromQuery] bool addVttTimeMap = false) { return GetSubtitle( + routeItemId, + routeMediaSourceId, + routeIndex, + routeFormat, itemId, mediaSourceId, index, @@ -282,7 +310,7 @@ namespace Jellyfin.Api.Controllers endPositionTicks, copyTimestamps, addVttTimeMap, - startPositionTicks); + startPositionTicks ?? routeStartPositionTicks); } /// <summary> @@ -333,7 +361,7 @@ namespace Jellyfin.Api.Controllers long positionTicks = 0; - var accessToken = _authContext.GetAuthorizationInfo(Request).Token; + var accessToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token; while (positionTicks < runtime) { diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index a55f13e66..a811a29c3 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions().AddClientFields(Request); var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { - OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), + OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, MediaTypes = mediaType, IncludeItemTypes = type, IsVirtualItem = false, diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index f878f2329..1b3248c0c 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.SyncPlayDtos; @@ -51,10 +52,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("New")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayCreateGroup)] - public ActionResult SyncPlayCreateGroup( + public async Task<ActionResult> SyncPlayCreateGroup( [FromBody, Required] NewGroupRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new NewGroupRequest(requestData.GroupName); _syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -69,10 +70,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Join")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayJoinGroup)] - public ActionResult SyncPlayJoinGroup( + public async Task<ActionResult> SyncPlayJoinGroup( [FromBody, Required] JoinGroupRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new JoinGroupRequest(requestData.GroupId); _syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -86,9 +87,9 @@ namespace Jellyfin.Api.Controllers [HttpPost("Leave")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayLeaveGroup() + public async Task<ActionResult> SyncPlayLeaveGroup() { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new LeaveGroupRequest(); _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -102,9 +103,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("List")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.SyncPlayJoinGroup)] - public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups() + public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups() { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new ListGroupsRequest(); return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest)); } @@ -118,10 +119,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("SetNewQueue")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySetNewQueue( + public async Task<ActionResult> SyncPlaySetNewQueue( [FromBody, Required] PlayRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new PlayGroupRequest( requestData.PlayingQueue, requestData.PlayingItemPosition, @@ -139,10 +140,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("SetPlaylistItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySetPlaylistItem( + public async Task<ActionResult> SyncPlaySetPlaylistItem( [FromBody, Required] SetPlaylistItemRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -157,10 +158,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("RemoveFromPlaylist")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayRemoveFromPlaylist( + public async Task<ActionResult> SyncPlayRemoveFromPlaylist( [FromBody, Required] RemoveFromPlaylistRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -175,10 +176,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("MovePlaylistItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayMovePlaylistItem( + public async Task<ActionResult> SyncPlayMovePlaylistItem( [FromBody, Required] MovePlaylistItemRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -193,10 +194,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Queue")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayQueue( + public async Task<ActionResult> SyncPlayQueue( [FromBody, Required] QueueRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -210,9 +211,9 @@ namespace Jellyfin.Api.Controllers [HttpPost("Unpause")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayUnpause() + public async Task<ActionResult> SyncPlayUnpause() { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new UnpauseGroupRequest(); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -226,9 +227,9 @@ namespace Jellyfin.Api.Controllers [HttpPost("Pause")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayPause() + public async Task<ActionResult> SyncPlayPause() { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new PauseGroupRequest(); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -242,9 +243,9 @@ namespace Jellyfin.Api.Controllers [HttpPost("Stop")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayStop() + public async Task<ActionResult> SyncPlayStop() { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new StopGroupRequest(); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -259,10 +260,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Seek")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySeek( + public async Task<ActionResult> SyncPlaySeek( [FromBody, Required] SeekRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -277,10 +278,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Buffering")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayBuffering( + public async Task<ActionResult> SyncPlayBuffering( [FromBody, Required] BufferRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new BufferGroupRequest( requestData.When, requestData.PositionTicks, @@ -299,10 +300,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("Ready")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayReady( + public async Task<ActionResult> SyncPlayReady( [FromBody, Required] ReadyRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new ReadyGroupRequest( requestData.When, requestData.PositionTicks, @@ -321,10 +322,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("SetIgnoreWait")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySetIgnoreWait( + public async Task<ActionResult> SyncPlaySetIgnoreWait( [FromBody, Required] IgnoreWaitRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -339,10 +340,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("NextItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayNextItem( + public async Task<ActionResult> SyncPlayNextItem( [FromBody, Required] NextItemRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -357,10 +358,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("PreviousItem")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlayPreviousItem( + public async Task<ActionResult> SyncPlayPreviousItem( [FromBody, Required] PreviousItemRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -375,10 +376,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("SetRepeatMode")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySetRepeatMode( + public async Task<ActionResult> SyncPlaySetRepeatMode( [FromBody, Required] SetRepeatModeRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -393,10 +394,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("SetShuffleMode")] [ProducesResponseType(StatusCodes.Status204NoContent)] [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public ActionResult SyncPlaySetShuffleMode( + public async Task<ActionResult> SyncPlaySetShuffleMode( [FromBody, Required] SetShuffleModeRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -410,10 +411,10 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> [HttpPost("Ping")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SyncPlayPing( + public async Task<ActionResult> SyncPlayPing( [FromBody, Required] PingRequestDto requestData) { - var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); var syncPlayRequest = new PingGroupRequest(requestData.Ping); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index dd3836551..5cb7468b2 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -114,7 +114,7 @@ namespace Jellyfin.Api.Controllers [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetTrailers( - [FromQuery] Guid? userId, + [FromQuery] Guid userId, [FromQuery] string? maxOfficialRating, [FromQuery] bool? hasThemeSong, [FromQuery] bool? hasThemeVideo, diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index e1c67f830..7c5b8a43b 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -6,7 +6,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -65,6 +65,7 @@ namespace Jellyfin.Api.Controllers /// <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="enableUserData">Optional. Include user data.</param> + /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param> /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param> /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> @@ -81,6 +82,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, + [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool disableFirstEpisode = false) { @@ -97,7 +99,8 @@ namespace Jellyfin.Api.Controllers StartIndex = startIndex, UserId = userId ?? Guid.Empty, EnableTotalRecordCount = enableTotalRecordCount, - DisableFirstEpisode = disableFirstEpisode + DisableFirstEpisode = disableFirstEpisode, + NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue }, options); @@ -155,7 +158,7 @@ namespace Jellyfin.Api.Controllers var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) { IncludeItemTypes = new[] { nameof(Episode) }, - OrderBy = new[] { ItemSortBy.PremiereDate, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(), + OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, MinPremiereDate = minPremiereDate, StartIndex = startIndex, Limit = limit, @@ -225,7 +228,7 @@ namespace Jellyfin.Api.Controllers if (seasonId.HasValue) // Season id was supplied. Get episodes by season id. { var item = _libraryManager.GetItemById(seasonId.Value); - if (!(item is Season seasonItem)) + if (item is not Season seasonItem) { return NotFound("No season exists with Id " + seasonId); } @@ -234,7 +237,7 @@ namespace Jellyfin.Api.Controllers } else if (season.HasValue) // Season number was supplied. Get episodes by season number { - if (!(_libraryManager.GetItemById(seriesId) is Series series)) + if (_libraryManager.GetItemById(seriesId) is not Series series) { return NotFound("Series not found"); } @@ -249,7 +252,7 @@ namespace Jellyfin.Api.Controllers } else // No season number or season id was supplied. Returning all episodes. { - if (!(_libraryManager.GetItemById(seriesId) is Series series)) + if (_libraryManager.GetItemById(seriesId) is not Series series) { return NotFound("Series not found"); } @@ -333,7 +336,7 @@ namespace Jellyfin.Api.Controllers ? _userManager.GetUserById(userId.Value) : null; - if (!(_libraryManager.GetItemById(seriesId) is Series series)) + if (_libraryManager.GetItemById(seriesId) is not Series series) { return NotFound("Series not found"); } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 0c2e6f19f..20a02bf4a 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -116,9 +116,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableRedirection = true) { var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); - _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId; + (await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId; - var authInfo = _authorizationContext.GetAuthorizationInfo(Request); + var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false); _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); @@ -219,10 +219,10 @@ namespace Jellyfin.Api.Controllers AudioBitRate = audioBitRate ?? maxStreamingBitrate, StartTimeTicks = startTimeTicks, SubtitleMethod = SubtitleDeliveryMethod.Hls, - RequireAvc = true, - DeInterlace = true, - RequireNonAnamorphic = true, - EnableMpegtsM2TsMode = true, + RequireAvc = false, + DeInterlace = false, + RequireNonAnamorphic = false, + EnableMpegtsM2TsMode = false, TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), Context = EncodingContext.Static, StreamOptions = new Dictionary<string, string>(), @@ -298,9 +298,9 @@ namespace Jellyfin.Api.Controllers { Type = DlnaProfileType.Audio, Context = EncodingContext.Streaming, - Container = transcodingContainer, - AudioCodec = audioCodec, - Protocol = transcodingProtocol, + Container = transcodingContainer ?? "mp3", + AudioCodec = audioCodec ?? "mp3", + Protocol = transcodingProtocol ?? "http", BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture) } diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 43ee309b7..4263d4fe5 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -14,6 +14,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; @@ -21,6 +22,7 @@ using MediaBrowser.Model.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { @@ -36,6 +38,8 @@ namespace Jellyfin.Api.Controllers private readonly IDeviceManager _deviceManager; private readonly IAuthorizationContext _authContext; private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + private readonly IQuickConnect _quickConnectManager; /// <summary> /// Initializes a new instance of the <see cref="UserController"/> class. @@ -46,13 +50,17 @@ namespace Jellyfin.Api.Controllers /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> + /// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param> public UserController( IUserManager userManager, ISessionManager sessionManager, INetworkManager networkManager, IDeviceManager deviceManager, IAuthorizationContext authContext, - IServerConfigurationManager config) + IServerConfigurationManager config, + ILogger<UserController> logger, + IQuickConnect quickConnectManager) { _userManager = userManager; _sessionManager = sessionManager; @@ -60,6 +68,8 @@ namespace Jellyfin.Api.Controllers _deviceManager = deviceManager; _authContext = authContext; _config = config; + _logger = logger; + _quickConnectManager = quickConnectManager; } /// <summary> @@ -72,11 +82,11 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<UserDto>> GetUsers( + public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers( [FromQuery] bool? isHidden, [FromQuery] bool? isDisabled) { - var users = Get(isHidden, isDisabled, false, false); + var users = await Get(isHidden, isDisabled, false, false).ConfigureAwait(false); return Ok(users); } @@ -87,15 +97,15 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns> [HttpGet("Public")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<UserDto>> GetPublicUsers() + public async Task<ActionResult<IEnumerable<UserDto>>> GetPublicUsers() { // If the startup wizard hasn't been completed then just return all users if (!_config.Configuration.IsStartupWizardCompleted) { - return Ok(Get(false, false, false, false)); + return Ok(await Get(false, false, false, false).ConfigureAwait(false)); } - return Ok(Get(false, false, true, true)); + return Ok(await Get(false, false, true, true).ConfigureAwait(false)); } /// <summary> @@ -118,7 +128,7 @@ namespace Jellyfin.Api.Controllers return NotFound("User not found"); } - var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp()); + var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString()); return result; } @@ -136,7 +146,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId) { var user = _userManager.GetUserById(userId); - _sessionManager.RevokeUserTokens(user.Id, null); + await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false); await _userManager.DeleteUserAsync(userId).ConfigureAwait(false); return NoContent(); } @@ -172,11 +182,9 @@ namespace Jellyfin.Api.Controllers return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed."); } - // Password should always be null AuthenticateUserByName request = new AuthenticateUserByName { Username = user.Username, - Password = null, Pw = pw }; return await AuthenticateUserByName(request).ConfigureAwait(false); @@ -192,7 +200,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request) { - var auth = _authContext.GetAuthorizationInfo(Request); + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); try { @@ -203,8 +211,7 @@ namespace Jellyfin.Api.Controllers DeviceId = auth.DeviceId, DeviceName = auth.Device, Password = request.Pw, - PasswordSha1 = request.Password, - RemoteEndPoint = HttpContext.GetNormalizedRemoteIp(), + RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(), Username = request.Username }).ConfigureAwait(false); @@ -226,23 +233,11 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns> [HttpPost("AuthenticateWithQuickConnect")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<AuthenticationResult>> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) + public ActionResult<AuthenticationResult> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) { - var auth = _authContext.GetAuthorizationInfo(Request); - try { - var authRequest = new AuthenticationRequest - { - App = auth.Client, - AppVersion = auth.Version, - DeviceId = auth.DeviceId, - DeviceName = auth.Device, - }; - - return await _sessionManager.AuthenticateQuickConnect( - authRequest, - request.Token).ConfigureAwait(false); + return _quickConnectManager.GetAuthorizedRequest(request.Secret); } catch (SecurityException e) { @@ -269,7 +264,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromBody, Required] UpdateUserPassword request) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password."); } @@ -291,7 +286,7 @@ namespace Jellyfin.Api.Controllers user.Username, request.CurrentPw, request.CurrentPw, - HttpContext.GetNormalizedRemoteIp(), + HttpContext.GetNormalizedRemoteIp().ToString(), false).ConfigureAwait(false); if (success == null) @@ -301,9 +296,9 @@ namespace Jellyfin.Api.Controllers await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); - var currentToken = _authContext.GetAuthorizationInfo(Request).Token; + var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token; - _sessionManager.RevokeUserTokens(user.Id, currentToken); + await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); } return NoContent(); @@ -323,11 +318,11 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateUserEasyPassword( + public async Task<ActionResult> UpdateUserEasyPassword( [FromRoute, Required] Guid userId, [FromBody, Required] UpdateUserEasyPassword request) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password."); } @@ -341,11 +336,11 @@ namespace Jellyfin.Api.Controllers if (request.ResetPassword) { - _userManager.ResetEasyPassword(user); + await _userManager.ResetEasyPassword(user).ConfigureAwait(false); } else { - _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword); + await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false); } return NoContent(); @@ -369,7 +364,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromBody, Required] UserDto updateUser) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed."); } @@ -429,8 +424,8 @@ namespace Jellyfin.Api.Controllers return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system."); } - var currentToken = _authContext.GetAuthorizationInfo(Request).Token; - _sessionManager.RevokeUserTokens(user.Id, currentToken); + var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token; + await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); } await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false); @@ -454,7 +449,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromBody, Required] UserConfiguration userConfig) { - if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false)) + if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false)) { return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed"); } @@ -483,7 +478,7 @@ namespace Jellyfin.Api.Controllers await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); } - var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp()); + var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString()); return result; } @@ -498,8 +493,14 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest) { + var ip = HttpContext.GetNormalizedRemoteIp(); var isLocal = HttpContext.IsLocal() - || _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()); + || _networkManager.IsInLocalNetwork(ip); + + if (isLocal) + { + _logger.LogWarning("Password reset proccess initiated from outside the local network with IP: {IP}", ip); + } var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false); @@ -547,7 +548,7 @@ namespace Jellyfin.Api.Controllers return _userManager.GetUserDto(user); } - private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) + private async Task<IEnumerable<UserDto>> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) { var users = _userManager.Users; @@ -563,7 +564,7 @@ namespace Jellyfin.Api.Controllers if (filterByDevice) { - var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId; + var deviceId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId; if (!string.IsNullOrWhiteSpace(deviceId)) { @@ -581,7 +582,7 @@ namespace Jellyfin.Api.Controllers var result = users .OrderBy(u => u.Username) - .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp())); + .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString())); return result; } diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 1d70406ac..a33a0826c 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -9,7 +9,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 7bc5ecdf1..3d27371f6 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; +using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; @@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="OkResult"/> containing the user views.</returns> [HttpGet("Users/{userId}/Views")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetUserViews( + public async Task<ActionResult<QueryResult<BaseItemDto>>> GetUserViews( [FromRoute, Required] Guid userId, [FromQuery] bool? includeExternalContent, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews, @@ -86,7 +87,7 @@ namespace Jellyfin.Api.Controllers query.PresetViews = presetViews; } - var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty; + var app = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Client ?? string.Empty; if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1) { query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows }; diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 620eef568..8560df2ea 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -48,9 +48,6 @@ namespace Jellyfin.Api.Controllers private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ILogger<VideoHlsController> _logger; @@ -60,9 +57,6 @@ namespace Jellyfin.Api.Controllers /// Initializes a new instance of the <see cref="VideoHlsController"/> class. /// </summary> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> @@ -72,11 +66,9 @@ namespace Jellyfin.Api.Controllers /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param> /// <param name="logger">Instance of the <see cref="ILogger{VideoHlsController}"/>.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public VideoHlsController( IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, IDlnaManager dlnaManager, IUserManager userManger, IAuthorizationContext authorizationContext, @@ -85,10 +77,9 @@ namespace Jellyfin.Api.Controllers IServerConfigurationManager serverConfigurationManager, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - ILogger<VideoHlsController> logger) + ILogger<VideoHlsController> logger, + EncodingHelper encodingHelper) { - _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration); - _dlnaManager = dlnaManager; _authContext = authorizationContext; _userManager = userManger; @@ -96,12 +87,11 @@ namespace Jellyfin.Api.Controllers _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _logger = logger; + _encodingHelper = encodingHelper; + _encodingOptions = serverConfigurationManager.GetEncodingOptions(); } @@ -150,7 +140,7 @@ namespace Jellyfin.Api.Controllers /// <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="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, vp8, vp9, vpx (deprecated), wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="transcodeReasons">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> @@ -223,7 +213,7 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -247,7 +237,7 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, @@ -256,13 +246,13 @@ namespace Jellyfin.Api.Controllers SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, @@ -275,6 +265,7 @@ namespace Jellyfin.Api.Controllers EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true }; + // CTS lifecycle is managed internally. var cancellationTokenSource = new CancellationTokenSource(); using var state = await StreamingHelpers.GetStreamingState( streamingRequest, @@ -285,9 +276,7 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, @@ -377,8 +366,7 @@ namespace Jellyfin.Api.Controllers else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) { var outputFmp4HeaderArg = string.Empty; - var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - if (isWindows) + if (OperatingSystem.IsWindows()) { // on Windows, the path of fmp4 header file needs to be configured outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\""; @@ -496,7 +484,7 @@ namespace Jellyfin.Api.Controllers args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } - args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true); + args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions); return args; } diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 99654e7b0..5c5057c83 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -49,12 +49,10 @@ namespace Jellyfin.Api.Controllers private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; private readonly IHttpClientFactory _httpClientFactory; + private readonly EncodingHelper _encodingHelper; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; @@ -69,12 +67,10 @@ namespace Jellyfin.Api.Controllers /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> + /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public VideosController( ILibraryManager libraryManager, IUserManager userManager, @@ -84,12 +80,10 @@ namespace Jellyfin.Api.Controllers IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - IHttpClientFactory httpClientFactory) + IHttpClientFactory httpClientFactory, + EncodingHelper encodingHelper) { _libraryManager = libraryManager; _userManager = userManager; @@ -99,12 +93,10 @@ namespace Jellyfin.Api.Controllers _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; _httpClientFactory = httpClientFactory; + _encodingHelper = encodingHelper; } /// <summary> @@ -304,6 +296,8 @@ namespace Jellyfin.Api.Controllers /// <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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param> + /// <param name="maxHeight">Optional. The maximum 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> @@ -316,7 +310,7 @@ namespace Jellyfin.Api.Controllers /// <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="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, vp8, vp9, vpx (deprecated), wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="transcodeReasons">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> @@ -360,6 +354,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] long? startTimeTicks, [FromQuery] int? width, [FromQuery] int? height, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, [FromQuery] SubtitleDeliveryMethod? subtitleMethod, @@ -381,12 +377,13 @@ namespace Jellyfin.Api.Controllers [FromQuery] Dictionary<string, string> streamOptions) { var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; + // CTS lifecycle is managed internally. var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new VideoRequestDto { Id = itemId, Container = container, - Static = @static ?? true, + Static = @static ?? false, Params = @params, Tag = tag, DeviceProfileId = deviceProfileId, @@ -410,22 +407,24 @@ namespace Jellyfin.Api.Controllers Level = level, Framerate = framerate, MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? true, + CopyTimestamps = copyTimestamps ?? false, StartTimeTicks = startTimeTicks, Width = width, Height = height, + MaxWidth = maxWidth, + MaxHeight = maxHeight, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, MaxRefFrames = maxRefFrames, MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? true, - DeInterlace = deInterlace ?? true, - RequireNonAnamorphic = requireNonAnamorphic ?? true, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, TranscodingMaxAudioChannels = transcodingMaxAudioChannels, CpuCoreLimit = cpuCoreLimit, LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, VideoCodec = videoCodec, SubtitleCodec = subtitleCodec, TranscodeReasons = transcodeReasons, @@ -444,9 +443,7 @@ namespace Jellyfin.Api.Controllers _libraryManager, _serverConfigurationManager, _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, + _encodingHelper, _dlnaManager, _deviceManager, _transcodingJobHelper, @@ -459,9 +456,9 @@ namespace Jellyfin.Api.Controllers StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None) - { - AllowEndOfFile = false - }.WriteToAsync(Response.Body, CancellationToken.None) + { + AllowEndOfFile = false + }.WriteToAsync(Response.Body, CancellationToken.None) .ConfigureAwait(false); // TODO (moved from MediaBrowser.Api): Don't hardcode contentType @@ -498,9 +495,9 @@ namespace Jellyfin.Api.Controllers if (state.MediaSource.IsInfiniteStream) { await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None) - { - AllowEndOfFile = false - }.WriteToAsync(Response.Body, CancellationToken.None) + { + AllowEndOfFile = false + }.WriteToAsync(Response.Body, CancellationToken.None) .ConfigureAwait(false); return File(Response.Body, contentType); @@ -515,8 +512,7 @@ namespace Jellyfin.Api.Controllers // Need to start ffmpeg (because media can't be returned directly) var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); - var ffmpegCommandLineArguments = encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast"); + var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast"); return await FileStreamResponseHelpers.GetTranscodedFile( state, isHeadRequest, @@ -538,7 +534,7 @@ namespace Jellyfin.Api.Controllers /// <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="segmentLength">The segment length.</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> @@ -560,6 +556,8 @@ namespace Jellyfin.Api.Controllers /// <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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param> + /// <param name="maxHeight">Optional. The maximum 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> @@ -567,12 +565,12 @@ namespace Jellyfin.Api.Controllers /// <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="requireNonAnamorphic">Optional. Whether to require a non anamorphic 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="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, vp8, vp9, vpx (deprecated), wmv.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="transcodeReasons">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> @@ -581,8 +579,8 @@ 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}")] - [HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")] + [HttpGet("{itemId}/stream.{container}")] + [HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesVideoFile] public Task<ActionResult> GetVideoStreamByContainer( @@ -616,6 +614,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] long? startTimeTicks, [FromQuery] int? width, [FromQuery] int? height, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, [FromQuery] SubtitleDeliveryMethod? subtitleMethod, @@ -667,6 +667,8 @@ namespace Jellyfin.Api.Controllers startTimeTicks, width, height, + maxWidth, + maxHeight, videoBitRate, subtitleStreamIndex, subtitleMethod, |
