aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api/Controllers')
-rw-r--r--Jellyfin.Api/Controllers/ActivityLogController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ApiKeyController.cs53
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs14
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs4
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs8
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs48
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs3
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs70
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs7
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs4
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs20
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs69
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs95
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs14
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs12
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs14
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs4
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs42
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs28
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs10
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs7
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs26
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs9
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs91
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs87
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs5
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs78
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs10
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs1
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs87
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs6
-rw-r--r--Jellyfin.Api/Controllers/TimeSyncController.cs4
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs2
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs25
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs12
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs63
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs2
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs20
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs49
45 files changed, 436 insertions, 688 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..8e0332d3e 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -1,10 +1,7 @@
-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 +15,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 +34,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 +54,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 +70,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 a6e70e72d..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>
@@ -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>
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/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 445733c24..87cb418d9 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers
if (enableInMainMenu.HasValue)
{
- configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList();
+ configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
}
return configPages;
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/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 87b4577b6..2079476d0 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -137,7 +137,7 @@ namespace Jellyfin.Api.Controllers
}
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
- existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
+ existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 694d16ad9..4e8c01577 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -7,7 +7,9 @@ using System.Threading.Tasks;
using Emby.Dlna;
using Emby.Dlna.Main;
using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Dlna;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers
/// Dlna Server Controller.
/// </summary>
[Route("Dlna")]
+ [Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
public class DlnaServerController : BaseJellyfinApiController
{
private readonly IDlnaManager _dlnaManager;
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index b4154b361..42e82dd5b 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -28,7 +27,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;
@@ -151,7 +149,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>
@@ -317,7 +315,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>
@@ -483,7 +481,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>
@@ -545,7 +543,7 @@ 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,
@@ -710,7 +708,7 @@ 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,
@@ -814,7 +812,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>
@@ -1138,7 +1136,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")
@@ -1191,6 +1189,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;
@@ -1208,7 +1207,7 @@ namespace Jellyfin.Api.Controllers
_deviceManager,
_transcodingJobHelper,
TranscodingJobType,
- cancellationTokenSource.Token)
+ cancellationToken)
.ConfigureAwait(false);
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
@@ -1227,7 +1226,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;
@@ -1323,24 +1322,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>();
-
- var ticks = state.RunTimeTicks ?? 0;
+ private static double[] GetSegmentLengths(StreamState state)
+ => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength);
- 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)
@@ -1376,18 +1379,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;
}
@@ -1497,7 +1495,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;
}
@@ -1711,7 +1709,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
- return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath), false, HttpContext);
}
private long GetEndPositionTicks(StreamState state, int requestedIndex)
@@ -1762,9 +1760,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 473bdc523..71caa0fe0 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
return BadRequest("Invalid segment.");
}
- return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file), false, HttpContext);
}
/// <summary>
@@ -186,7 +186,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
- return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, HttpContext);
+ return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path), false, HttpContext);
}
}
}
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index e1b808098..99ab7f232 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
}
var contentType = MimeTypes.GetMimeType(path);
- return File(System.IO.File.OpenRead(path), contentType);
+ return File(AsyncFile.OpenRead(path), contentType);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 8f7500ac6..86933074d 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.");
}
@@ -106,7 +106,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null)
{
@@ -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.");
}
@@ -153,7 +153,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null)
{
@@ -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.");
}
@@ -341,7 +341,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
@@ -377,7 +377,7 @@ namespace Jellyfin.Api.Controllers
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
- var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
@@ -2007,7 +2007,7 @@ namespace Jellyfin.Api.Controllers
Response.Headers.Add(HeaderNames.CacheControl, "public");
}
- Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
+ Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture));
// if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
@@ -2026,7 +2026,7 @@ namespace Jellyfin.Api.Controllers
return NoContent();
}
- return PhysicalFile(imagePath, imageContentType);
+ return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
}
}
}
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index f232dffaa..a6c2e07c9 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -80,7 +80,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -116,7 +116,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -187,7 +187,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -223,43 +223,7 @@ namespace Jellyfin.Api.Controllers
: 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 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!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -295,7 +259,7 @@ namespace Jellyfin.Api.Controllers
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -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..448510c06 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -1,13 +1,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -17,7 +14,6 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -238,48 +234,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 +274,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..fd137f98f 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);
@@ -263,8 +263,8 @@ namespace Jellyfin.Api.Controllers
item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
}
- item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : (DateTime?)null;
- item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : (DateTime?)null;
+ item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
+ item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
item.ProductionYear = request.ProductionYear;
item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
item.CustomRating = request.CustomRating;
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 74cf3b162..f0d44e5cc 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,7 +241,7 @@ 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();
}
@@ -285,9 +285,9 @@ namespace Jellyfin.Api.Controllers
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!)
+ var query = new InternalItemsQuery(user)
{
IsPlayed = isPlayed,
MediaTypes = mediaTypes,
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 1d4bbe61e..0be853ca4 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -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;
@@ -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..b131530c9 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
-using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Mime;
@@ -429,10 +428,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 +760,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 +789,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 +807,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 +823,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 +881,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 +899,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 +915,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 +1171,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 +1180,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>
@@ -1203,21 +1199,21 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
- public async Task<ActionResult> GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
+ public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
{
- var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false);
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
if (liveStreamInfo == null)
{
return NotFound();
}
- var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
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..b422eb78c 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
@@ -179,7 +184,7 @@ namespace Jellyfin.Api.Controllers
audioStreamIndex,
subtitleStreamIndex,
maxAudioChannels,
- info!.PlaySessionId!,
+ info.PlaySessionId!,
userId ?? Guid.Empty,
enableDirectPlay.Value,
enableDirectStream.Value,
@@ -302,31 +307,16 @@ 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
{
- new Random().NextBytes(buffer);
+ Random.Shared.NextBytes(buffer);
return File(buffer, MediaTypeNames.Application.Octet);
}
finally
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index d0a2358ae..99c90d19e 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -300,9 +300,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
});
@@ -316,10 +315,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 adec86a10..0ae6109bc 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -8,8 +8,8 @@ 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.Net;
@@ -207,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..35921ede8 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -4,11 +4,10 @@ using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net.Http;
-using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@@ -16,7 +15,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -146,58 +144,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>
@@ -237,36 +183,5 @@ namespace Jellyfin.Api.Controllers
{
return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
}
-
- /// <summary>
- /// Downloads the image.
- /// </summary>
- /// <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(Uri url, Guid urlHash, string pointerCachePath)
- {
- var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
- using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
- if (response.Content.Headers.ContentType?.MediaType == null)
- {
- throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType));
- }
-
- var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1];
- var fullCachePath = GetFullCachePath(urlHash + "." + ext);
-
- var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
- Directory.CreateDirectory(fullCacheDirectory);
- // 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 response.Content.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, CancellationToken.None)
- .ConfigureAwait(false);
- }
}
}
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 7bd0b6918..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();
}
@@ -162,7 +164,7 @@ namespace Jellyfin.Api.Controllers
[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,
@@ -183,11 +185,12 @@ namespace Jellyfin.Api.Controllers
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();
}
@@ -204,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()
{
@@ -219,7 +222,8 @@ namespace Jellyfin.Api.Controllers
ControllingUserId = controllingUserId,
SeekPositionTicks = seekPositionTicks,
},
- CancellationToken.None);
+ CancellationToken.None)
+ .ConfigureAwait(false);
return NoContent();
}
@@ -234,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();
}
@@ -260,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
{
@@ -272,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();
}
@@ -287,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)
{
@@ -300,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();
}
@@ -319,7 +326,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/{sessionId}/Message")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SendMessageCommand(
+ public async Task<ActionResult> SendMessageCommand(
[FromRoute, Required] string sessionId,
[FromBody, Required] MessageCommand command)
{
@@ -328,7 +335,12 @@ namespace Jellyfin.Api.Controllers
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();
}
@@ -383,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,
@@ -393,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
@@ -417,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());
@@ -441,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();
@@ -459,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 1669a659d..db8307f28 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers
{
var video = (Video)_libraryManager.GetItemById(itemId);
- return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false);
+ return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
/// <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/{routeItemId}/routeMediaSourceId/Subtitles/{routeIndex}/Stream.{routeFormat}")]
+ [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesFile("text/*")]
public async Task<ActionResult> GetSubtitle(
@@ -361,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)
{
@@ -376,7 +376,7 @@ namespace Jellyfin.Api.Controllers
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
var url = string.Format(
- CultureInfo.CurrentCulture,
+ CultureInfo.InvariantCulture,
"stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
positionTicks.ToString(CultureInfo.InvariantCulture),
endPositionTicks.ToString(CultureInfo.InvariantCulture),
@@ -417,6 +417,8 @@ namespace Jellyfin.Api.Controllers
IsForced = body.IsForced,
Stream = memoryStream
}).ConfigureAwait(false);
+ _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index a811a29c3..97eec4bd2 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index f878f2329..c6b70f3d2 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,11 +158,11 @@ 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 syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds);
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
+ var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem);
_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/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index bbbe5fb8d..904738bb4 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -66,7 +66,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<SystemInfo> GetSystemInfo()
{
- return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
+ return _appHost.GetSystemInfo(Request);
}
/// <summary>
@@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
{
- return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
+ return _appHost.GetPublicSystemInfo(Request);
}
/// <summary>
@@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers
// For older files, assume fully static
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
- FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare);
+ FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
return File(stream, "text/plain; charset=utf-8");
}
diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs
index 7df51c7af..e7c5a7125 100644
--- a/Jellyfin.Api/Controllers/TimeSyncController.cs
+++ b/Jellyfin.Api/Controllers/TimeSyncController.cs
@@ -21,10 +21,10 @@ namespace Jellyfin.Api.Controllers
public ActionResult<UtcTimeResponse> GetUtcTime()
{
// Important to keep the following line at the beginning
- var requestReceptionTime = DateTime.UtcNow.ToUniversalTime();
+ var requestReceptionTime = DateTime.UtcNow;
// Important to keep the following line at the end
- var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime();
+ var responseTransmissionTime = DateTime.UtcNow;
// Implementing NTP on such a high level results in this useless
// information being sent. On the other hand it enables future additions.
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 59400db2a..5dd773331 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,12 +82,13 @@ 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)
{
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
var result = _tvSeriesManager.GetNextUp(
new NextUpQuery
@@ -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);
@@ -144,13 +147,13 @@ namespace Jellyfin.Api.Controllers
? _userManager.GetUserById(userId.Value)
: null;
- var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
+ var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1);
var parentIdGuid = parentId ?? Guid.Empty;
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
@@ -220,12 +223,12 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
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");
}
@@ -347,7 +350,7 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index dcdd8b367..bc9527a0b 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);
@@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers
null,
null,
maxAudioChannels,
- info!.PlaySessionId!,
+ info.PlaySessionId!,
userId ?? Guid.Empty,
true,
true,
@@ -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 b13db4baa..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;
@@ -38,6 +39,7 @@ namespace Jellyfin.Api.Controllers
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.
@@ -49,6 +51,7 @@ namespace Jellyfin.Api.Controllers
/// <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,
@@ -56,7 +59,8 @@ namespace Jellyfin.Api.Controllers
IDeviceManager deviceManager,
IAuthorizationContext authContext,
IServerConfigurationManager config,
- ILogger<UserController> logger)
+ ILogger<UserController> logger,
+ IQuickConnect quickConnectManager)
{
_userManager = userManager;
_sessionManager = sessionManager;
@@ -65,6 +69,7 @@ namespace Jellyfin.Api.Controllers
_authContext = authContext;
_config = config;
_logger = logger;
+ _quickConnectManager = quickConnectManager;
}
/// <summary>
@@ -77,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);
}
@@ -92,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>
@@ -141,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();
}
@@ -195,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
{
@@ -228,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)
{
@@ -271,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.");
}
@@ -303,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();
@@ -325,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.");
}
@@ -343,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();
@@ -371,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.");
}
@@ -431,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);
@@ -456,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");
}
@@ -555,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;
@@ -571,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))
{
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 308334b23..ef25db8c9 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -20,12 +19,10 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
@@ -140,7 +137,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>
@@ -265,7 +262,11 @@ namespace Jellyfin.Api.Controllers
EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
};
+ // CTS lifecycle is managed internally.
var cancellationTokenSource = new CancellationTokenSource();
+ // Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
+ // since it gets disposed when ffmpeg exits
+ var cancellationToken = cancellationTokenSource.Token;
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,
Request,
@@ -280,7 +281,7 @@ namespace Jellyfin.Api.Controllers
_deviceManager,
_transcodingJobHelper,
TranscodingJobType,
- cancellationTokenSource.Token)
+ cancellationToken)
.ConfigureAwait(false);
TranscodingJobDto? job = null;
@@ -289,7 +290,7 @@ namespace Jellyfin.Api.Controllers
if (!System.IO.File.Exists(playlistPath))
{
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+ await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (!System.IO.File.Exists(playlistPath))
@@ -316,7 +317,7 @@ namespace Jellyfin.Api.Controllers
minSegments = state.MinSegments;
if (minSegments > 0)
{
- await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
+ await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -365,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 + "\"";
@@ -484,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 8dbb6aaa5..e1cbc6f33 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -25,14 +25,12 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
namespace Jellyfin.Api.Controllers
{
@@ -296,6 +294,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>
@@ -308,7 +308,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>
@@ -352,6 +352,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,
@@ -373,6 +375,7 @@ 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
{
@@ -406,6 +409,8 @@ namespace Jellyfin.Api.Controllers
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
@@ -448,14 +453,15 @@ 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)
- .ConfigureAwait(false);
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
+ if (liveStreamInfo == null)
+ {
+ return NotFound();
+ }
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
- return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
+ return File(liveStream, MimeTypes.GetMimeType("file.ts"));
}
// Static remote stream
@@ -487,13 +493,8 @@ namespace Jellyfin.Api.Controllers
if (state.MediaSource.IsInfiniteStream)
{
- await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
-
- return File(Response.Body, contentType);
+ var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ return File(liveStream, contentType);
}
return FileStreamResponseHelpers.GetStaticFileResult(
@@ -527,7 +528,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>
@@ -549,6 +550,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>
@@ -556,12 +559,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>
@@ -570,8 +573,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(
@@ -605,6 +608,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,
@@ -656,6 +661,8 @@ namespace Jellyfin.Api.Controllers
startTimeTicks,
width,
height,
+ maxWidth,
+ maxHeight,
videoBitRate,
subtitleStreamIndex,
subtitleMethod,