diff options
| author | Claus Vium <cvium@users.noreply.github.com> | 2022-10-07 09:57:16 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-10-07 09:57:16 +0200 |
| commit | 81b04ddbb54201d2e07310e3c700cca8a94d8955 (patch) | |
| tree | a4e9aeb70e35b3e47a7d8af17b328e88a1c77ed0 /Jellyfin.Api | |
| parent | 6bf71c0fd355e9c95a1e142019d9bc5cce34200d (diff) | |
| parent | 14027f962ce074623fd89967ca9565bbeb785066 (diff) | |
Merge branch 'master' into providermanager-cleanup
Diffstat (limited to 'Jellyfin.Api')
56 files changed, 744 insertions, 684 deletions
diff --git a/Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs b/Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs new file mode 100644 index 0000000000..d3a6ac9c81 --- /dev/null +++ b/Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs @@ -0,0 +1,25 @@ +using Emby.Dlna; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Api.Attributes; + +/// <inheritdoc /> +public sealed class DlnaEnabledAttribute : ActionFilterAttribute +{ + /// <inheritdoc /> + public override void OnActionExecuting(ActionExecutingContext context) + { + var serverConfigurationManager = context.HttpContext.RequestServices.GetRequiredService<IServerConfigurationManager>(); + + var enabled = serverConfigurationManager.GetDlnaConfiguration().EnableServer; + + if (!enabled) + { + context.Result = new StatusCodeResult(StatusCodes.Status503ServiceUnavailable); + } + } +} diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs index af8727552c..4dcf5976a2 100644 --- a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs +++ b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs @@ -25,11 +25,6 @@ namespace Jellyfin.Api.Attributes /// <param name="template">The route template. May not be null.</param> public HttpSubscribeAttribute(string template) : base(_supportedMethods, template) - { - if (template == null) - { - throw new ArgumentNullException(nameof(template)); - } - } + => ArgumentNullException.ThrowIfNull(template); } } diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs index 1c0b70e719..d0238424a7 100644 --- a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs +++ b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs @@ -25,11 +25,6 @@ namespace Jellyfin.Api.Attributes /// <param name="template">The route template. May not be null.</param> public HttpUnsubscribeAttribute(string template) : base(_supportedMethods, template) - { - if (template == null) - { - throw new ArgumentNullException(nameof(template)); - } - } + => ArgumentNullException.ThrowIfNull(template); } } diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index 13d3257dff..92ee1dd59a 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; @@ -51,21 +52,21 @@ namespace Jellyfin.Api.Auth bool requiredDownloadPermission = false) { // ApiKey is currently global admin, always allow. - var isApiKey = ClaimHelpers.GetIsApiKey(claimsPrincipal); + var isApiKey = claimsPrincipal.GetIsApiKey(); if (isApiKey) { return true; } // Ensure claim has userId. - var userId = ClaimHelpers.GetUserId(claimsPrincipal); - if (!userId.HasValue) + var userId = claimsPrincipal.GetUserId(); + if (userId.Equals(default)) { return false; } // Ensure userId links to a valid user. - var user = _userManager.GetUserById(userId.Value); + var user = _userManager.GetUserById(userId); if (user == null) { return false; diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs index e6c04eb082..cdd7d8a52b 100644 --- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs +++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; using MediaBrowser.Common.Net; @@ -44,14 +45,14 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy return Task.CompletedTask; } - var userId = ClaimHelpers.GetUserId(context.User); - var user = _userManager.GetUserById(userId!.Value); + var userId = context.User.GetUserId(); + var user = _userManager.GetUserById(userId); if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess) { if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups || user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups - || _syncPlayManager.IsUserActive(userId.Value)) + || _syncPlayManager.IsUserActive(userId)) { context.Succeed(requirement); } @@ -85,7 +86,7 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy } else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup) { - if (_syncPlayManager.IsUserActive(userId.Value)) + if (_syncPlayManager.IsUserActive(userId)) { context.Succeed(requirement); } diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index 59d6b75137..0c63d24b70 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Net.Mime; +using Jellyfin.Api.Results; using Jellyfin.Extensions.Json; using Microsoft.AspNetCore.Mvc; @@ -15,5 +17,40 @@ namespace Jellyfin.Api JsonDefaults.PascalCaseMediaType)] public class BaseJellyfinApiController : ControllerBase { + /// <summary> + /// Create a new <see cref="OkResult{T}"/>. + /// </summary> + /// <param name="value">The value to return.</param> + /// <typeparam name="T">The type to return.</typeparam> + /// <returns>The <see cref="ActionResult{T}"/>.</returns> + protected ActionResult<IEnumerable<T>> Ok<T>(List<T> value) + => new OkResult<IEnumerable<T>>(value); + + /// <summary> + /// Create a new <see cref="OkResult{T}"/>. + /// </summary> + /// <param name="value">The value to return.</param> + /// <typeparam name="T">The type to return.</typeparam> + /// <returns>The <see cref="ActionResult{T}"/>.</returns> + protected ActionResult<IEnumerable<T>> Ok<T>(IReadOnlyList<T> value) + => new OkResult<IEnumerable<T>>(value); + + /// <summary> + /// Create a new <see cref="OkResult{T}"/>. + /// </summary> + /// <param name="value">The value to return.</param> + /// <typeparam name="T">The type to return.</typeparam> + /// <returns>The <see cref="ActionResult{T}"/>.</returns> + protected ActionResult<IEnumerable<T>> Ok<T>(IEnumerable<T>? value) + => new OkResult<IEnumerable<T>?>(value); + + /// <summary> + /// Create a new <see cref="OkResult{T}"/>. + /// </summary> + /// <param name="value">The value to return.</param> + /// <typeparam name="T">The type to return.</typeparam> + /// <returns>The <see cref="ActionResult{T}"/>.</returns> + protected ActionResult<T> Ok<T>(T value) + => new OkResult<T>(value); } } diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 44796bcc4c..c059cb198a 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -120,7 +120,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true) { var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = null; @@ -323,7 +323,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true) { var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = null; @@ -463,7 +463,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId) { - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); var item = _libraryManager.GetArtist(name, dtoOptions); diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 54ac06276e..94f7a7b827 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -207,7 +207,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> diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs index 98fd224307..ed073a687e 100644 --- a/Jellyfin.Api/Controllers/ClientLogController.cs +++ b/Jellyfin.Api/Controllers/ClientLogController.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.ClientLogDtos; using MediaBrowser.Controller.ClientEvent; @@ -69,10 +70,10 @@ namespace Jellyfin.Api.Controllers private (string ClientName, string ClientVersion) GetRequestInformation() { - var clientName = ClaimHelpers.GetClient(HttpContext.User) ?? "unknown-client"; - var clientVersion = ClaimHelpers.GetIsApiKey(HttpContext.User) + var clientName = HttpContext.User.GetClient() ?? "unknown-client"; + var clientVersion = HttpContext.User.GetIsApiKey() ? "apikey" - : ClaimHelpers.GetVersion(HttpContext.User) ?? "unknown-version"; + : HttpContext.User.GetVersion() ?? "unknown-version"; return (clientName, clientVersion); } diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 8a98d856c4..effc9ed7aa 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -6,7 +6,6 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Collections; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -23,22 +22,18 @@ namespace Jellyfin.Api.Controllers { private readonly ICollectionManager _collectionManager; private readonly IDtoService _dtoService; - private readonly IAuthorizationContext _authContext; /// <summary> /// Initializes a new instance of the <see cref="CollectionController"/> class. /// </summary> /// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param> /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param> - /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param> public CollectionController( ICollectionManager collectionManager, - IDtoService dtoService, - IAuthorizationContext authContext) + IDtoService dtoService) { _collectionManager = collectionManager; _dtoService = dtoService; - _authContext = authContext; } /// <summary> @@ -58,7 +53,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? parentId, [FromQuery] bool isLocked = false) { - var userId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).UserId; + var userId = User.GetUserId(); var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions { @@ -69,7 +64,7 @@ namespace Jellyfin.Api.Controllers UserIds = new[] { userId } }).ConfigureAwait(false); - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); var dto = _dtoService.GetBaseItemDto(item, dtoOptions); diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 60529e9904..bbe1633120 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -2,7 +2,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Text.Json; -using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; @@ -86,21 +85,23 @@ namespace Jellyfin.Api.Controllers /// Updates named configuration. /// </summary> /// <param name="key">Configuration key.</param> + /// <param name="configuration">Configuration.</param> /// <response code="204">Named configuration updated.</response> /// <returns>Update status.</returns> [HttpPost("Configuration/{key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task<ActionResult> UpdateNamedConfiguration([FromRoute, Required] string key) + public ActionResult UpdateNamedConfiguration([FromRoute, Required] string key, [FromBody, Required] JsonDocument configuration) { var configurationType = _configurationManager.GetConfigurationType(key); - var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false); - if (configuration == null) + var deserializedConfiguration = configuration.Deserialize(configurationType, _serializerOptions); + + if (deserializedConfiguration == null) { throw new ArgumentException("Body doesn't contain a valid configuration"); } - _configurationManager.SaveConfiguration(key, configuration); + _configurationManager.SaveConfiguration(key, deserializedConfiguration); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 27eb223390..64ee5680ce 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -89,12 +89,9 @@ namespace Jellyfin.Api.Controllers // Load all custom display preferences var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client); - if (customDisplayPreferences != null) + foreach (var (key, value) in customDisplayPreferences) { - foreach (var (key, value) in customDisplayPreferences) - { - dto.CustomPrefs.TryAdd(key, value); - } + dto.CustomPrefs.TryAdd(key, value); } // This will essentially be a noop if no changes have been made, but new prefs must be saved at least. diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index b1c576c330..8859d60207 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers /// Dlna Server Controller. /// </summary> [Route("Dlna")] + [DlnaEnabled] [Authorize(Policy = Policies.AnonymousLanAccessPolicy)] public class DlnaServerController : BaseJellyfinApiController { @@ -53,17 +54,12 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult GetDescriptionXml([FromRoute, Required] string serverId) + public ActionResult<string> GetDescriptionXml([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - var url = GetAbsoluteUri(); - var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); - var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); - return Ok(xml); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + var url = GetAbsoluteUri(); + var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); + var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); + return Ok(xml); } /// <summary> @@ -81,14 +77,9 @@ namespace Jellyfin.Api.Controllers [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult GetContentDirectory([FromRoute, Required] string serverId) + public ActionResult<string> GetContentDirectory([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return Ok(_contentDirectory.GetServiceXml()); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return Ok(_contentDirectory.GetServiceXml()); } /// <summary> @@ -106,14 +97,9 @@ namespace Jellyfin.Api.Controllers [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId) + public ActionResult<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return Ok(_mediaReceiverRegistrar.GetServiceXml()); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return Ok(_mediaReceiverRegistrar.GetServiceXml()); } /// <summary> @@ -131,14 +117,9 @@ namespace Jellyfin.Api.Controllers [Produces(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult GetConnectionManager([FromRoute, Required] string serverId) + public ActionResult<string> GetConnectionManager([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return Ok(_connectionManager.GetServiceXml()); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return Ok(_connectionManager.GetServiceXml()); } /// <summary> @@ -155,12 +136,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); } /// <summary> @@ -177,12 +153,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); } /// <summary> @@ -199,12 +170,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); } /// <summary> @@ -224,12 +190,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) { - if (DlnaEntryPoint.Enabled) - { - return ProcessEventRequest(_mediaReceiverRegistrar); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return ProcessEventRequest(_mediaReceiverRegistrar); } /// <summary> @@ -249,12 +210,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) { - if (DlnaEntryPoint.Enabled) - { - return ProcessEventRequest(_contentDirectory); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return ProcessEventRequest(_contentDirectory); } /// <summary> @@ -274,12 +230,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) { - if (DlnaEntryPoint.Enabled) - { - return ProcessEventRequest(_connectionManager); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return ProcessEventRequest(_connectionManager); } /// <summary> @@ -299,12 +250,7 @@ namespace Jellyfin.Api.Controllers [ProducesImageFile] public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName) { - if (DlnaEntryPoint.Enabled) - { - return GetIconInternal(fileName); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return GetIconInternal(fileName); } /// <summary> @@ -322,12 +268,7 @@ namespace Jellyfin.Api.Controllers [ProducesImageFile] public ActionResult GetIcon([FromRoute, Required] string fileName) { - if (DlnaEntryPoint.Enabled) - { - return GetIconInternal(fileName); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return GetIconInternal(fileName); } private ActionResult GetIconInternal(string fileName) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index f8e8d975cc..0f4d3c1ebc 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -20,7 +20,6 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.IO; @@ -46,7 +45,6 @@ namespace Jellyfin.Api.Controllers private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDlnaManager _dlnaManager; - private readonly IAuthorizationContext _authContext; private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; @@ -65,7 +63,6 @@ namespace Jellyfin.Api.Controllers /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> @@ -80,7 +77,6 @@ namespace Jellyfin.Api.Controllers ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, - IAuthorizationContext authContext, IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, @@ -95,7 +91,6 @@ namespace Jellyfin.Api.Controllers _libraryManager = libraryManager; _userManager = userManager; _dlnaManager = dlnaManager; - _authContext = authContext; _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; @@ -121,7 +116,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> @@ -285,10 +280,9 @@ namespace Jellyfin.Api.Controllers // 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( + var state = await StreamingHelpers.GetStreamingState( streamingRequest, - Request, - _authContext, + HttpContext, _mediaSourceManager, _userManager, _libraryManager, @@ -1393,8 +1387,7 @@ namespace Jellyfin.Api.Controllers { using var state = await StreamingHelpers.GetStreamingState( streamingRequest, - Request, - _authContext, + HttpContext, _mediaSourceManager, _userManager, _libraryManager, @@ -1414,7 +1407,8 @@ namespace Jellyfin.Api.Controllers state.RunTimeTicks ?? 0, state.Request.SegmentContainer ?? string.Empty, "hls1/main/", - Request.QueryString.ToString()); + Request.QueryString.ToString(), + EncodingHelper.IsCopyCodec(state.OutputVideoCodec)); var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request); return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8")); @@ -1431,10 +1425,9 @@ namespace Jellyfin.Api.Controllers var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - using var state = await StreamingHelpers.GetStreamingState( + var state = await StreamingHelpers.GetStreamingState( streamingRequest, - Request, - _authContext, + HttpContext, _mediaSourceManager, _userManager, _libraryManager, @@ -1711,20 +1704,30 @@ namespace Jellyfin.Api.Controllers return audioTranscodeParams; } + // flac and opus are experimental in mp4 muxer + var strictArgs = string.Empty; + + if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)) + { + strictArgs = " -strict -2"; + } + if (EncodingHelper.IsCopyCodec(audioCodec)) { var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); + var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs; if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) { - return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0" + bitStreamArgs; + return copyArgs + " -copypriorss:a:0 0"; } - return "-codec:a:0 copy -strict -2" + bitStreamArgs; + return copyArgs; } - var args = "-codec:a:0 " + audioCodec; + var args = "-codec:a:0 " + audioCodec + strictArgs; var channels = state.OutputAudioChannels; @@ -1773,13 +1776,25 @@ namespace Jellyfin.Api.Controllers var args = "-codec:v:0 " + codec; - // Prefer hvc1 to hev1. if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) { - args += " -tag:v:0 hvc1"; + if (EncodingHelper.IsCopyCodec(codec) + && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase))) + { + // Prefer dvh1 to dvhe + args += " -tag:v:0 dvh1 -strict -2"; + } + else + { + // Prefer hvc1 to hev1 + args += " -tag:v:0 hvc1"; + } } // if (state.EnableMpegtsM2TsMode) @@ -1809,7 +1824,7 @@ namespace Jellyfin.Api.Controllers // Set the key frame params for video encoding to match the hls segment time. args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber); - // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now. + // Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now. if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) { args += " -bf 0"; diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index e28a50750a..611643bd8a 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true) { var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); User? user = userId is null || userId.Value.Equals(default) @@ -157,7 +157,7 @@ namespace Jellyfin.Api.Controllers public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) { var dtoOptions = new DtoOptions() - .AddClientFields(Request); + .AddClientFields(User); Genre? item; if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase)) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 05d80ba35d..f092bd8820 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -17,7 +17,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Branding; using MediaBrowser.Model.Drawing; @@ -44,11 +43,9 @@ namespace Jellyfin.Api.Controllers private readonly IProviderManager _providerManager; private readonly IImageProcessor _imageProcessor; private readonly IFileSystem _fileSystem; - private readonly IAuthorizationContext _authContext; private readonly ILogger<ImageController> _logger; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IApplicationPaths _appPaths; - private readonly IImageEncoder _imageEncoder; /// <summary> /// Initializes a new instance of the <see cref="ImageController"/> class. @@ -58,33 +55,27 @@ namespace Jellyfin.Api.Controllers /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{ImageController}"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> - /// <param name="imageEncoder">Instance of the <see cref="IImageEncoder"/> interface.</param> public ImageController( IUserManager userManager, ILibraryManager libraryManager, IProviderManager providerManager, IImageProcessor imageProcessor, IFileSystem fileSystem, - IAuthorizationContext authContext, ILogger<ImageController> logger, IServerConfigurationManager serverConfigurationManager, - IApplicationPaths appPaths, - IImageEncoder imageEncoder) + IApplicationPaths appPaths) { _userManager = userManager; _libraryManager = libraryManager; _providerManager = providerManager; _imageProcessor = imageProcessor; _fileSystem = fileSystem; - _authContext = authContext; _logger = logger; _serverConfigurationManager = serverConfigurationManager; _appPaths = appPaths; - _imageEncoder = imageEncoder; } /// <summary> @@ -108,7 +99,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromQuery] int? index = null) { - if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } @@ -155,7 +146,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromRoute] int index) { - if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } @@ -201,7 +192,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromQuery] int? index = null) { - if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); } @@ -245,7 +236,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromRoute] int index) { - if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); } @@ -1724,6 +1715,11 @@ namespace Jellyfin.Api.Controllers [FromQuery, Range(0, 100)] int quality = 90) { var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + if (!brandingOptions.SplashscreenEnabled) + { + return NotFound(); + } + string splashscreenPath; if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation) @@ -1776,6 +1772,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Uploads a custom splashscreen. + /// The body is expected to the image contents base64 encoded. /// </summary> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <response code="204">Successfully uploaded new splashscreen.</response> @@ -1799,7 +1796,13 @@ namespace Jellyfin.Api.Controllers return BadRequest("Error reading mimetype from uploaded image"); } - var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType.Value)); + var extension = MimeTypes.ToExtension(mimeType.Value); + if (string.IsNullOrEmpty(extension)) + { + return BadRequest("Error converting mimetype to an image extension"); + } + + var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); brandingOptions.SplashscreenLocation = filePath; _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); @@ -1812,6 +1815,29 @@ namespace Jellyfin.Api.Controllers return NoContent(); } + /// <summary> + /// Delete a custom splashscreen. + /// </summary> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + /// <response code="204">Successfully deleted the custom splashscreen.</response> + /// <response code="403">User does not have permission to delete splashscreen..</response> + [HttpDelete("Branding/Splashscreen")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult DeleteCustomSplashscreen() + { + var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation) + && System.IO.File.Exists(brandingOptions.SplashscreenLocation)) + { + System.IO.File.Delete(brandingOptions.SplashscreenLocation); + brandingOptions.SplashscreenLocation = null; + _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); + } + + return NoContent(); + } + private static async Task<MemoryStream> GetMemoryStream(Stream inputStream) { using var reader = new StreamReader(inputStream); diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 9abea5938f..2e0d3cb99e 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -79,7 +79,7 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -115,7 +115,7 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -151,7 +151,7 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -186,7 +186,7 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -222,7 +222,7 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -258,7 +258,7 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -331,7 +331,7 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 2794a06f30..80ae5abcbf 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; @@ -63,7 +64,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Gets items based on a query. /// </summary> - /// <param name="userId">The user id supplied as query parameter.</param> + /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param> /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="hasThemeSong">Optional filter by items with theme songs.</param> /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param> @@ -89,6 +90,11 @@ namespace Jellyfin.Api.Controllers /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> + /// <param name="isMovie">Optional filter for live tv movies.</param> + /// <param name="isSeries">Optional filter for live tv series.</param> + /// <param name="isNews">Optional filter for live tv news.</param> + /// <param name="isKids">Optional filter for live tv kids.</param> + /// <param name="isSports">Optional filter for live tv sports.</param> /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param> /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="limit">Optional. The maximum number of records to return.</param> @@ -147,14 +153,14 @@ 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, [FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasTrailer, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] int? parentIndexNumber, [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, @@ -173,6 +179,11 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasImdbId, [FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTvdbId, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, @@ -228,9 +239,20 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - var user = userId.Equals(default) ? null : _userManager.GetUserById(userId); + var isApiKey = User.GetIsApiKey(); + // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method + var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default) + ? _userManager.GetUserById(userId.Value) + : null; + + // beyond this point, we're either using an api key or we have a valid user + if (!isApiKey && user is null) + { + return BadRequest("userId is required"); + } + var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); if (includeItemTypes.Length == 1 @@ -260,30 +282,39 @@ namespace Jellyfin.Api.Controllers includeItemTypes = new[] { BaseItemKind.Playlist }; } - var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels); + var enabledChannels = isApiKey + ? Array.Empty<Guid>() + : user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels); - bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1 + // api keys are always enabled for all folders + bool isInEnabledFolder = isApiKey + || Array.IndexOf(user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1 // Assume all folders inside an EnabledChannel are enabled || Array.IndexOf(enabledChannels, item.Id) != -1 // Assume all items inside an EnabledChannel are enabled || Array.IndexOf(enabledChannels, item.ChannelId) != -1; - var collectionFolders = _libraryManager.GetCollectionFolders(item); - foreach (var collectionFolder in collectionFolders) + if (!isInEnabledFolder) { - if (user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id)) + var collectionFolders = _libraryManager.GetCollectionFolders(item); + foreach (var collectionFolder in collectionFolders) { - isInEnabledFolder = true; + // api keys never enter this block, so user is never null + if (user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id)) + { + isInEnabledFolder = true; + } } } + // api keys are always enabled for all folders, so user is never null if (item is not UserRootFolder && !isInEnabledFolder - && !user.HasPermission(PermissionKind.EnableAllFolders) + && !user!.HasPermission(PermissionKind.EnableAllFolders) && !user.HasPermission(PermissionKind.EnableAllChannels) && !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase)) { - _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name); + _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user.Username, item.Name); return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); } @@ -316,6 +347,11 @@ namespace Jellyfin.Api.Controllers Is3D = is3D, HasTvdbId = hasTvdbId, HasTmdbId = hasTmdbId, + IsMovie = isMovie, + IsSeries = isSeries, + IsNews = isNews, + IsKids = isKids, + IsSports = isSports, HasOverview = hasOverview, HasOfficialRating = hasOfficialRating, HasParentalRating = hasParentalRating, @@ -515,8 +551,8 @@ namespace Jellyfin.Api.Controllers /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param> /// <param name="isHd">Optional filter by items that are HD or not.</param> /// <param name="is4K">Optional filter by items that are 4K or not.</param> - /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param> - /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param> + /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param> + /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param> /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param> /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param> /// <param name="minCommunityRating">Optional filter by minimum community rating.</param> @@ -529,42 +565,47 @@ namespace Jellyfin.Api.Controllers /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> - /// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param> + /// <param name="isMovie">Optional filter for live tv movies.</param> + /// <param name="isSeries">Optional filter for live tv series.</param> + /// <param name="isNews">Optional filter for live tv news.</param> + /// <param name="isKids">Optional filter for live tv kids.</param> + /// <param name="isSports">Optional filter for live tv sports.</param> + /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param> /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param> /// <param name="searchTerm">Optional. Filter based on a search term.</param> /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> - /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> - /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param> - /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param> - /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param> + /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> + /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> + /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param> + /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param> /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param> /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param> - /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> + /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> /// <param name="isPlayed">Optional filter by items that are played, or not.</param> - /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param> - /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param> - /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param> - /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param> + /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param> + /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param> + /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param> + /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param> /// <param name="enableUserData">Optional, include user data.</param> /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param> /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param> /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param> - /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param> - /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param> - /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param> + /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param> + /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param> + /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param> /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param> /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param> /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param> - /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param> - /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param> + /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param> + /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param> /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param> - /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param> + /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param> /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="isLocked">Optional filter by items that are locked.</param> /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param> @@ -575,12 +616,12 @@ namespace Jellyfin.Api.Controllers /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param> /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param> /// <param name="is3D">Optional filter by items that are 3D, or not.</param> - /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param> + /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param> /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> - /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param> - /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param> + /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param> + /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param> /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns> @@ -594,7 +635,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasTrailer, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] int? parentIndexNumber, [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, @@ -613,6 +654,11 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasImdbId, [FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTvdbId, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, @@ -695,6 +741,11 @@ namespace Jellyfin.Api.Controllers hasImdbId, hasTmdbId, hasTvdbId, + isMovie, + isSeries, + isNews, + isKids, + isSports, excludeItemIds, startIndex, limit, @@ -793,7 +844,7 @@ namespace Jellyfin.Api.Controllers var user = _userManager.GetUserById(userId); var parentIdGuid = parentId ?? Guid.Empty; var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var ancestorIds = Array.Empty<Guid>(); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 4cc17dd0fc..e9492a6a47 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -24,7 +24,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Configuration; @@ -50,7 +49,6 @@ namespace Jellyfin.Api.Controllers private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDtoService _dtoService; - private readonly IAuthorizationContext _authContext; private readonly IActivityManager _activityManager; private readonly ILocalizationManager _localization; private readonly ILibraryMonitor _libraryMonitor; @@ -64,7 +62,6 @@ namespace Jellyfin.Api.Controllers /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> - /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param> /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> /// <param name="libraryMonitor">Instance of the <see cref="ILibraryMonitor"/> interface.</param> @@ -75,7 +72,6 @@ namespace Jellyfin.Api.Controllers ILibraryManager libraryManager, IUserManager userManager, IDtoService dtoService, - IAuthorizationContext authContext, IActivityManager activityManager, ILocalizationManager localization, ILibraryMonitor libraryMonitor, @@ -86,7 +82,6 @@ namespace Jellyfin.Api.Controllers _libraryManager = libraryManager; _userManager = userManager; _dtoService = dtoService; - _authContext = authContext; _activityManager = activityManager; _localization = localization; _libraryMonitor = libraryMonitor; @@ -184,7 +179,7 @@ namespace Jellyfin.Api.Controllers item = parent; } - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); var items = themeItems .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .ToArray(); @@ -250,7 +245,7 @@ namespace Jellyfin.Api.Controllers item = parent; } - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); var items = themeItems .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .ToArray(); @@ -331,11 +326,10 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task<ActionResult> DeleteItem(Guid itemId) + public ActionResult DeleteItem(Guid itemId) { var item = _libraryManager.GetItemById(itemId); - var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - var user = auth.User; + var user = _userManager.GetUserById(User.GetUserId()); if (!item.CanDelete(user)) { @@ -361,7 +355,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task<ActionResult> DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) { if (ids.Length == 0) { @@ -371,8 +365,7 @@ namespace Jellyfin.Api.Controllers foreach (var i in ids) { var item = _libraryManager.GetItemById(i); - var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - var user = auth.User; + var user = _userManager.GetUserById(User.GetUserId()); if (!item.CanDelete(user)) { @@ -453,7 +446,7 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); BaseItem? parent = item.GetParent(); while (parent != null) @@ -505,7 +498,7 @@ namespace Jellyfin.Api.Controllers items = items.Where(i => i.IsHidden == val).ToList(); } - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions); return new QueryResult<BaseItemDto>(resultArray); } @@ -622,9 +615,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - - var user = auth.User; + var user = _userManager.GetUserById(User.GetUserId()); if (user != null) { @@ -643,7 +634,7 @@ namespace Jellyfin.Api.Controllers if (user != null) { - await LogDownloadAsync(item, user, auth).ConfigureAwait(false); + await LogDownloadAsync(item, user).ConfigureAwait(false); } var path = item.Path; @@ -704,7 +695,7 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request); + .AddClientFields(User); var program = item as IHasProgramAttributes; bool? isMovie = item is Movie || (program != null && program.IsMovie) || item is Trailer; @@ -892,16 +883,16 @@ namespace Jellyfin.Api.Controllers : item; } - private async Task LogDownloadAsync(BaseItem item, User user, AuthorizationInfo auth) + private async Task LogDownloadAsync(BaseItem item, User user) { try { await _activityManager.CreateAsync(new ActivityLog( string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name), "UserDownloadingContent", - auth.UserId) + User.GetUserId()) { - ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device), + ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()), }).ConfigureAwait(false); } catch diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 05340099bf..394df0f58b 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -17,6 +17,7 @@ using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -24,6 +25,7 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; @@ -45,10 +47,10 @@ namespace Jellyfin.Api.Controllers private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; - private readonly ISessionContext _sessionContext; private readonly IMediaSourceManager _mediaSourceManager; private readonly IConfigurationManager _configurationManager; private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly ISessionManager _sessionManager; /// <summary> /// Initializes a new instance of the <see cref="LiveTvController"/> class. @@ -58,30 +60,30 @@ namespace Jellyfin.Api.Controllers /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> - /// <param name="sessionContext">Instance of the <see cref="ISessionContext"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> + /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> public LiveTvController( ILiveTvManager liveTvManager, IUserManager userManager, IHttpClientFactory httpClientFactory, ILibraryManager libraryManager, IDtoService dtoService, - ISessionContext sessionContext, IMediaSourceManager mediaSourceManager, IConfigurationManager configurationManager, - TranscodingJobHelper transcodingJobHelper) + TranscodingJobHelper transcodingJobHelper, + ISessionManager sessionManager) { _liveTvManager = liveTvManager; _userManager = userManager; _httpClientFactory = httpClientFactory; _libraryManager = libraryManager; _dtoService = dtoService; - _sessionContext = sessionContext; _mediaSourceManager = mediaSourceManager; _configurationManager = configurationManager; _transcodingJobHelper = transcodingJobHelper; + _sessionManager = sessionManager; } /// <summary> @@ -154,7 +156,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool addCurrentProgram = true) { var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var channelResult = _liveTvManager.GetInternalChannels( @@ -219,7 +221,7 @@ namespace Jellyfin.Api.Controllers : _libraryManager.GetItemById(channelId); var dtoOptions = new DtoOptions() - .AddClientFields(Request); + .AddClientFields(User); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } @@ -272,7 +274,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true) { var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); return _liveTvManager.GetRecordings( @@ -410,7 +412,7 @@ namespace Jellyfin.Api.Controllers var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); var dtoOptions = new DtoOptions() - .AddClientFields(Request); + .AddClientFields(User); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } @@ -599,7 +601,7 @@ namespace Jellyfin.Api.Controllers } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); } @@ -653,7 +655,7 @@ namespace Jellyfin.Api.Controllers } var dtoOptions = new DtoOptions { Fields = body.Fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); } @@ -719,7 +721,7 @@ namespace Jellyfin.Api.Controllers }; var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); } @@ -1210,9 +1212,16 @@ namespace Jellyfin.Api.Controllers private async Task AssertUserCanManageLiveTv() { - var user = await _sessionContext.GetUser(Request).ConfigureAwait(false); - - if (user == null) + var user = _userManager.GetUserById(User.GetUserId()); + var session = await _sessionManager.LogSessionActivity( + User.GetClient(), + User.GetVersion(), + User.GetDeviceId(), + User.GetDevice(), + HttpContext.GetNormalizedRemoteIp().ToString(), + user).ConfigureAwait(false); + + if (session.UserId.Equals(default)) { throw new SecurityException("Anonymous live tv management is not allowed."); } diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index 75df18204d..c111e92186 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -6,13 +6,12 @@ using System.Net.Mime; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.MediaInfoDtos; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Dlna; using MediaBrowser.Model.MediaInfo; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -32,7 +31,6 @@ namespace Jellyfin.Api.Controllers private readonly IMediaSourceManager _mediaSourceManager; private readonly IDeviceManager _deviceManager; private readonly ILibraryManager _libraryManager; - private readonly IAuthorizationContext _authContext; private readonly ILogger<MediaInfoController> _logger; private readonly MediaInfoHelper _mediaInfoHelper; @@ -42,21 +40,18 @@ namespace Jellyfin.Api.Controllers /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> - /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param> /// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param> public MediaInfoController( IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, - IAuthorizationContext authContext, ILogger<MediaInfoController> logger, MediaInfoHelper mediaInfoHelper) { _mediaSourceManager = mediaSourceManager; _deviceManager = deviceManager; _libraryManager = libraryManager; - _authContext = authContext; _logger = logger; _mediaInfoHelper = mediaInfoHelper; } @@ -123,14 +118,12 @@ namespace Jellyfin.Api.Controllers [FromQuery, ParameterObsolete] bool? allowAudioStreamCopy, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto) { - var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - var profile = playbackInfoDto?.DeviceProfile; _logger.LogDebug("GetPostedPlaybackInfo profile: {@Profile}", profile); if (profile == null) { - var caps = _deviceManager.GetCapabilities(authInfo.DeviceId); + var caps = _deviceManager.GetCapabilities(User.GetDeviceId()); if (caps != null) { profile = caps.DeviceProfile; @@ -177,7 +170,7 @@ namespace Jellyfin.Api.Controllers item, mediaSource, profile, - authInfo, + User, maxStreamingBitrate ?? profile.MaxStreamingBitrate, startTimeTicks ?? 0, mediaSourceId ?? string.Empty, @@ -204,7 +197,7 @@ namespace Jellyfin.Api.Controllers if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId)) { var openStreamResult = await _mediaInfoHelper.OpenMediaSource( - Request, + HttpContext, new LiveStreamRequest { AudioStreamIndex = audioStreamIndex, @@ -277,7 +270,7 @@ namespace Jellyfin.Api.Controllers EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true, DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http } }; - return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false); + return await _mediaInfoHelper.OpenMediaSource(HttpContext, request).ConfigureAwait(false); } /// <summary> diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 420dd99233..8195fc7609 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -72,7 +72,7 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request); + .AddClientFields(User); var categories = new List<RecommendationDto>(); @@ -170,7 +170,7 @@ namespace Jellyfin.Api.Controllers } } - return Ok(categories.OrderBy(i => i.RecommendationType)); + return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable()); } private IEnumerable<RecommendationDto> GetWithDirector( diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 0499b29857..f4fb5f44ab 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true) { var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); User? user = userId is null || userId.Value.Equals(default) @@ -145,7 +145,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) { - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); MusicGenre? item; diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 5dd49ef2fd..9690aead0c 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers /// <response code="204">Package repositories saved.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Repositories")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos) { diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index be4b9eded5..42be969b29 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -79,7 +79,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableImages = true) { var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = userId is null || userId.Value.Equals(default) @@ -98,7 +98,10 @@ namespace Jellyfin.Api.Controllers Limit = limit ?? 0 }); - return new QueryResult<BaseItemDto>(peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray()); + return new QueryResult<BaseItemDto>( + peopleItems + .Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)) + .ToArray()); } /// <summary> @@ -116,7 +119,7 @@ namespace Jellyfin.Api.Controllers public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId) { var dtoOptions = new DtoOptions() - .AddClientFields(Request); + .AddClientFields(User); var item = _libraryManager.GetPerson(name); if (item == null) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index ad85f2fb24..fb045f891e 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -200,7 +200,7 @@ namespace Jellyfin.Api.Controllers } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 6dee1c2192..0dd4bf8035 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -3,11 +3,11 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; @@ -29,7 +29,6 @@ namespace Jellyfin.Api.Controllers private readonly IUserDataManager _userDataRepository; private readonly ILibraryManager _libraryManager; private readonly ISessionManager _sessionManager; - private readonly IAuthorizationContext _authContext; private readonly ILogger<PlaystateController> _logger; private readonly TranscodingJobHelper _transcodingJobHelper; @@ -40,7 +39,6 @@ namespace Jellyfin.Api.Controllers /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> - /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> /// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param> public PlaystateController( @@ -48,7 +46,6 @@ namespace Jellyfin.Api.Controllers IUserDataManager userDataRepository, ILibraryManager libraryManager, ISessionManager sessionManager, - IAuthorizationContext authContext, ILoggerFactory loggerFactory, TranscodingJobHelper transcodingJobHelper) { @@ -56,7 +53,6 @@ namespace Jellyfin.Api.Controllers _userDataRepository = userDataRepository; _libraryManager = libraryManager; _sessionManager = sessionManager; - _authContext = authContext; _logger = loggerFactory.CreateLogger<PlaystateController>(); _transcodingJobHelper = transcodingJobHelper; @@ -78,7 +74,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed) { var user = _userManager.GetUserById(userId); - var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); + var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var dto = UpdatePlayedStatus(user, itemId, true, datePlayed); foreach (var additionalUserInfo in session.AdditionalUsers) { @@ -101,7 +97,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) { var user = _userManager.GetUserById(userId); - var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); + var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var dto = UpdatePlayedStatus(user, itemId, false, null); foreach (var additionalUserInfo in session.AdditionalUsers) { @@ -123,7 +119,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo) { playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); - playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); + playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); return NoContent(); } @@ -139,7 +135,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo) { playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); - playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); + playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); return NoContent(); } @@ -171,11 +167,10 @@ namespace Jellyfin.Api.Controllers _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) { - var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); } - playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); + playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); return NoContent(); } @@ -221,7 +216,7 @@ namespace Jellyfin.Api.Controllers }; playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); - playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); + playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); return NoContent(); } @@ -279,7 +274,7 @@ namespace Jellyfin.Api.Controllers }; playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); - playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); + playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);; await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); return NoContent(); } @@ -321,11 +316,10 @@ namespace Jellyfin.Api.Controllers _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) { - var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); } - playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); + playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);; await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index 87b78fe93f..77d88475ff 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; @@ -39,7 +40,7 @@ namespace Jellyfin.Api.Controllers /// <returns>Whether Quick Connect is enabled on the server or not.</returns> [HttpGet("Enabled")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<bool> GetEnabled() + public ActionResult<bool> GetQuickConnectEnabled() { return _quickConnect.IsEnabled; } @@ -52,7 +53,7 @@ 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 async Task<ActionResult<QuickConnectResult>> Initiate() + public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect() { try { @@ -75,7 +76,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Connect")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult<QuickConnectResult> Connect([FromQuery, Required] string secret) + public ActionResult<QuickConnectResult> GetQuickConnectState([FromQuery, Required] string secret) { try { @@ -102,17 +103,17 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task<ActionResult<bool>> Authorize([FromQuery, Required] string code) + public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code) { - var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); - if (!userId.HasValue) + var userId = User.GetUserId(); + if (userId.Equals(default)) { return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id"); } try { - return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false); + return await _quickConnect.AuthorizeRequest(userId, code).ConfigureAwait(false); } catch (AuthenticationException) { diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 6ffedccbd4..aeed0c0d61 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -6,6 +6,7 @@ using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -59,9 +60,9 @@ namespace Jellyfin.Api.Controllers /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param> /// <param name="searchTerm">The search term to filter on.</param> - /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimeted.</param> - /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimeted.</param> - /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimeted.</param> + /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimited.</param> + /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimited.</param> + /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimited.</param> /// <param name="parentId">If specified, only children of the parent are returned.</param> /// <param name="isMovie">Optional filter for movies.</param> /// <param name="isSeries">Optional filter for series.</param> @@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Description("Gets search hints based on a search term")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<SearchHintResult> Get( + public ActionResult<SearchHintResult> GetSearchHints( [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] Guid? userId, @@ -139,7 +140,7 @@ namespace Jellyfin.Api.Controllers IndexNumber = item.IndexNumber, ParentIndexNumber = item.ParentIndexNumber, Id = item.Id, - Type = item.GetClientTypeName(), + Type = item.GetBaseItemKind(), MediaType = item.MediaType, MatchedTerm = hintInfo.MatchedTerm, RunTimeTicks = item.RunTimeTicks, @@ -148,8 +149,10 @@ namespace Jellyfin.Api.Controllers EndDate = item.EndDate }; - // legacy +#pragma warning disable CS0618 + // Kept for compatibility with older clients result.ItemId = result.Id; +#pragma warning restore CS0618 if (item.IsFolder) { @@ -187,7 +190,7 @@ namespace Jellyfin.Api.Controllers result.AlbumArtist = album.AlbumArtist; break; case Audio song: - result.AlbumArtist = song.AlbumArtists?[0]; + result.AlbumArtist = song.AlbumArtists?.FirstOrDefault(); result.Artists = song.Artists; MusicAlbum musicAlbum = song.AlbumEntity; diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 860bccb9bd..28415555ed 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -5,13 +5,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.SessionDtos; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; @@ -29,7 +29,6 @@ namespace Jellyfin.Api.Controllers { private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; - private readonly IAuthorizationContext _authContext; private readonly IDeviceManager _deviceManager; /// <summary> @@ -37,17 +36,14 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param> /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param> - /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param> /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param> public SessionController( ISessionManager sessionManager, IUserManager userManager, - IAuthorizationContext authContext, IDeviceManager deviceManager) { _sessionManager = sessionManager; _userManager = userManager; - _authContext = authContext; _deviceManager = deviceManager; } @@ -139,7 +135,7 @@ namespace Jellyfin.Api.Controllers }; await _sessionManager.SendBrowseCommand( - await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), + await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), sessionId, command, CancellationToken.None) @@ -186,7 +182,7 @@ namespace Jellyfin.Api.Controllers }; await _sessionManager.SendPlayCommand( - await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), + await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), sessionId, playRequest, CancellationToken.None) @@ -214,7 +210,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? controllingUserId) { await _sessionManager.SendPlaystateCommand( - await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), + await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), sessionId, new PlaystateRequest() { @@ -242,14 +238,14 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string sessionId, [FromRoute, Required] GeneralCommandType command) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var generalCommand = new GeneralCommand { Name = command, ControllingUserId = currentSession.UserId }; - await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None); + await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -268,7 +264,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string sessionId, [FromRoute, Required] GeneralCommandType command) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var generalCommand = new GeneralCommand { @@ -296,8 +292,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string sessionId, [FromBody, Required] GeneralCommand command) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request) - .ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); if (command == null) { @@ -336,7 +331,7 @@ namespace Jellyfin.Api.Controllers } await _sessionManager.SendMessageCommand( - await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false), + await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), sessionId, command, CancellationToken.None) @@ -405,7 +400,7 @@ namespace Jellyfin.Api.Controllers { if (string.IsNullOrWhiteSpace(id)) { - id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); + id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); } _sessionManager.ReportCapabilities(id, new ClientCapabilities @@ -435,7 +430,7 @@ namespace Jellyfin.Api.Controllers { if (string.IsNullOrWhiteSpace(id)) { - id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); + id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); } _sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities()); @@ -457,7 +452,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? sessionId, [FromQuery, Required] string? itemId) { - string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false); + string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); _sessionManager.ReportNowViewingItem(session, itemId); return NoContent(); @@ -473,9 +468,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> ReportSessionEnded() { - AuthorizationInfo auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - - await _sessionManager.Logout(auth.Token).ConfigureAwait(false); + await _sessionManager.Logout(User.GetToken()).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 053c7baaa1..1288fb5124 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true) { var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = userId is null || userId.Value.Equals(default) @@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId) { - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); var item = _libraryManager.GetStudio(name); if (userId.HasValue && !userId.Equals(default)) diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 16acedcf35..1258a9876b 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -11,13 +11,13 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.SubtitleDtos; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Entities; @@ -45,7 +45,6 @@ namespace Jellyfin.Api.Controllers private readonly IMediaSourceManager _mediaSourceManager; private readonly IProviderManager _providerManager; private readonly IFileSystem _fileSystem; - private readonly IAuthorizationContext _authContext; private readonly ILogger<SubtitleController> _logger; /// <summary> @@ -58,7 +57,6 @@ namespace Jellyfin.Api.Controllers /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param> /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param> /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> - /// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param> /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param> public SubtitleController( IServerConfigurationManager serverConfigurationManager, @@ -68,7 +66,6 @@ namespace Jellyfin.Api.Controllers IMediaSourceManager mediaSourceManager, IProviderManager providerManager, IFileSystem fileSystem, - IAuthorizationContext authContext, ILogger<SubtitleController> logger) { _serverConfigurationManager = serverConfigurationManager; @@ -78,7 +75,6 @@ namespace Jellyfin.Api.Controllers _mediaSourceManager = mediaSourceManager; _providerManager = providerManager; _fileSystem = fileSystem; - _authContext = authContext; _logger = logger; } @@ -361,7 +357,7 @@ namespace Jellyfin.Api.Controllers long positionTicks = 0; - var accessToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token; + var accessToken = User.GetToken(); while (positionTicks < runtime) { diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index e9c46dcf3a..1cf528153f 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -67,7 +67,7 @@ namespace Jellyfin.Api.Controllers ? null : _userManager.GetUserById(userId); - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index c6b70f3d20..e194fc556e 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.SyncPlayDtos; -using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.SyncPlay.PlaybackRequests; @@ -24,23 +24,23 @@ namespace Jellyfin.Api.Controllers public class SyncPlayController : BaseJellyfinApiController { private readonly ISessionManager _sessionManager; - private readonly IAuthorizationContext _authorizationContext; private readonly ISyncPlayManager _syncPlayManager; + private readonly IUserManager _userManager; /// <summary> /// Initializes a new instance of the <see cref="SyncPlayController"/> class. /// </summary> /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> - /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> public SyncPlayController( ISessionManager sessionManager, - IAuthorizationContext authorizationContext, - ISyncPlayManager syncPlayManager) + ISyncPlayManager syncPlayManager, + IUserManager userManager) { _sessionManager = sessionManager; - _authorizationContext = authorizationContext; _syncPlayManager = syncPlayManager; + _userManager = userManager; } /// <summary> @@ -55,7 +55,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlayCreateGroup( [FromBody, Required] NewGroupRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new NewGroupRequest(requestData.GroupName); _syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -73,7 +73,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlayJoinGroup( [FromBody, Required] JoinGroupRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new JoinGroupRequest(requestData.GroupId); _syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.SyncPlayIsInGroup)] public async Task<ActionResult> SyncPlayLeaveGroup() { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new LeaveGroupRequest(); _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -105,7 +105,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.SyncPlayJoinGroup)] public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups() { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new ListGroupsRequest(); return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest)); } @@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlaySetNewQueue( [FromBody, Required] PlayRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new PlayGroupRequest( requestData.PlayingQueue, requestData.PlayingItemPosition, @@ -143,7 +143,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlaySetPlaylistItem( [FromBody, Required] SetPlaylistItemRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -161,7 +161,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlayRemoveFromPlaylist( [FromBody, Required] RemoveFromPlaylistRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -179,7 +179,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlayMovePlaylistItem( [FromBody, Required] MovePlaylistItemRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -197,7 +197,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlayQueue( [FromBody, Required] QueueRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -213,7 +213,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.SyncPlayIsInGroup)] public async Task<ActionResult> SyncPlayUnpause() { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new UnpauseGroupRequest(); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -229,7 +229,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.SyncPlayIsInGroup)] public async Task<ActionResult> SyncPlayPause() { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new PauseGroupRequest(); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -245,7 +245,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.SyncPlayIsInGroup)] public async Task<ActionResult> SyncPlayStop() { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new StopGroupRequest(); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -263,7 +263,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlaySeek( [FromBody, Required] SeekRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -281,7 +281,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlayBuffering( [FromBody, Required] BufferRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new BufferGroupRequest( requestData.When, requestData.PositionTicks, @@ -303,7 +303,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlayReady( [FromBody, Required] ReadyRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new ReadyGroupRequest( requestData.When, requestData.PositionTicks, @@ -325,7 +325,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlaySetIgnoreWait( [FromBody, Required] IgnoreWaitRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -343,7 +343,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlayNextItem( [FromBody, Required] NextItemRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlayPreviousItem( [FromBody, Required] PreviousItemRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -379,7 +379,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlaySetRepeatMode( [FromBody, Required] SetRepeatModeRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -397,7 +397,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlaySetShuffleMode( [FromBody, Required] SetShuffleModeRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); @@ -414,7 +414,7 @@ namespace Jellyfin.Api.Controllers public async Task<ActionResult> SyncPlayPing( [FromBody, Required] PingRequestDto requestData) { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false); + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new PingGroupRequest(requestData.Ping); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); return NoContent(); diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 5cb7468b24..b296d1c960 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; @@ -31,7 +32,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Finds movies and trailers similar to a given trailer. /// </summary> - /// <param name="userId">The user id.</param> + /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param> /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="hasThemeSong">Optional filter by items with theme songs.</param> /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param> @@ -57,6 +58,11 @@ namespace Jellyfin.Api.Controllers /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> + /// <param name="isMovie">Optional filter for live tv movies.</param> + /// <param name="isSeries">Optional filter for live tv series.</param> + /// <param name="isNews">Optional filter for live tv news.</param> + /// <param name="isKids">Optional filter for live tv kids.</param> + /// <param name="isSports">Optional filter for live tv sports.</param> /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param> /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="limit">Optional. The maximum number of records to return.</param> @@ -114,14 +120,14 @@ 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, [FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasTrailer, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] int? parentIndexNumber, [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, @@ -140,6 +146,11 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasImdbId, [FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTvdbId, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, @@ -224,6 +235,11 @@ namespace Jellyfin.Api.Controllers hasImdbId, hasTmdbId, hasTvdbId, + isMovie, + isSeries, + isNews, + isKids, + isSports, excludeItemIds, startIndex, limit, diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 179a53fd54..ea13ceb91c 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -77,7 +77,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? seriesId, + [FromQuery] Guid? seriesId, [FromQuery] Guid? parentId, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, @@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableRewatching = false) { var options = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var result = _tvSeriesManager.GetNextUp( @@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers var parentIdGuid = parentId ?? Guid.Empty; var options = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) @@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? season, [FromQuery] Guid? seasonId, [FromQuery] bool? isMissing, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] Guid? startItemId, [FromQuery] int? startIndex, [FromQuery] int? limit, @@ -223,7 +223,7 @@ namespace Jellyfin.Api.Controllers List<BaseItem> episodes; var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); if (seasonId.HasValue) // Season id was supplied. Get episodes by season id. @@ -278,9 +278,9 @@ namespace Jellyfin.Api.Controllers } // This must be the last filter - if (!string.IsNullOrEmpty(adjacentTo)) + if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default)) { - episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo).ToList(); + episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList(); } if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) @@ -326,7 +326,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? isSpecialSeason, [FromQuery] bool? isMissing, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -349,7 +349,7 @@ namespace Jellyfin.Api.Controllers }); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .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 6fcafd426c..01e13b4fe3 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -6,17 +6,15 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -30,8 +28,6 @@ namespace Jellyfin.Api.Controllers [Route("")] public class UniversalAudioController : BaseJellyfinApiController { - private readonly IAuthorizationContext _authorizationContext; - private readonly IDeviceManager _deviceManager; private readonly ILibraryManager _libraryManager; private readonly ILogger<UniversalAudioController> _logger; private readonly MediaInfoHelper _mediaInfoHelper; @@ -41,24 +37,18 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Initializes a new instance of the <see cref="UniversalAudioController"/> class. /// </summary> - /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> - /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param> /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param> /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param> /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> public UniversalAudioController( - IAuthorizationContext authorizationContext, - IDeviceManager deviceManager, ILibraryManager libraryManager, ILogger<UniversalAudioController> logger, MediaInfoHelper mediaInfoHelper, AudioHelper audioHelper, DynamicHlsHelper dynamicHlsHelper) { - _authorizationContext = authorizationContext; - _deviceManager = deviceManager; _libraryManager = libraryManager; _logger = logger; _mediaInfoHelper = mediaInfoHelper; @@ -117,76 +107,57 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableRedirection = true) { var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); - (await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId; - var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - - _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); - - if (deviceProfile == null) + if (!userId.HasValue || userId.Value.Equals(Guid.Empty)) { - var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId); - if (clientCapabilities != null) - { - deviceProfile = clientCapabilities.DeviceProfile; - } + userId = User.GetUserId(); } + _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); + var info = await _mediaInfoHelper.GetPlaybackInfo( itemId, userId, mediaSourceId) .ConfigureAwait(false); - if (deviceProfile != null) - { - // set device specific data - var item = _libraryManager.GetItemById(itemId); - - foreach (var sourceInfo in info.MediaSources) - { - _mediaInfoHelper.SetDeviceSpecificData( - item, - sourceInfo, - deviceProfile, - authInfo, - maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate, - startTimeTicks ?? 0, - mediaSourceId ?? string.Empty, - null, - null, - maxAudioChannels, - info.PlaySessionId!, - userId ?? Guid.Empty, - true, - true, - true, - true, - true, - Request.HttpContext.GetNormalizedRemoteIp()); - } + // set device specific data + var item = _libraryManager.GetItemById(itemId); - _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); + foreach (var sourceInfo in info.MediaSources) + { + _mediaInfoHelper.SetDeviceSpecificData( + item, + sourceInfo, + deviceProfile, + User, + maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate, + startTimeTicks ?? 0, + mediaSourceId ?? string.Empty, + null, + null, + maxAudioChannels, + info.PlaySessionId!, + userId ?? Guid.Empty, + true, + true, + true, + true, + true, + Request.HttpContext.GetNormalizedRemoteIp()); } - if (info.MediaSources != null) + _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); + + foreach (var source in info.MediaSources) { - foreach (var source in info.MediaSources) - { - _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile!, DlnaProfileType.Video); - } + _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video); } - var mediaSource = info.MediaSources![0]; - if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http) + var mediaSource = info.MediaSources[0]; + if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value) { - if (enableRedirection) - { - if (mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value) - { - return Redirect(mediaSource.Path); - } - } + return Redirect(mediaSource.Path); } var isStatic = mediaSource.SupportsDirectStream; @@ -249,7 +220,7 @@ namespace Jellyfin.Api.Controllers BreakOnNonKeyFrames = breakOnNonKeyFrames, AudioSampleRate = maxAudioSampleRate, MaxAudioChannels = maxAudioChannels, - AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate), + AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate), MaxAudioBitDepth = maxAudioBitDepth, AudioChannels = maxAudioChannels, CopyTimestamps = true, diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 6d15d91858..ff653fe6bb 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; @@ -82,11 +83,11 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers( + public ActionResult<IEnumerable<UserDto>> GetUsers( [FromQuery] bool? isHidden, [FromQuery] bool? isDisabled) { - var users = await Get(isHidden, isDisabled, false, false).ConfigureAwait(false); + var users = Get(isHidden, isDisabled, false, false); return Ok(users); } @@ -97,15 +98,15 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns> [HttpGet("Public")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<IEnumerable<UserDto>>> GetPublicUsers() + public ActionResult<IEnumerable<UserDto>> GetPublicUsers() { // If the startup wizard hasn't been completed then just return all users if (!_config.Configuration.IsStartupWizardCompleted) { - return Ok(await Get(false, false, false, false).ConfigureAwait(false)); + return Ok(Get(false, false, false, false)); } - return Ok(await Get(false, false, true, true).ConfigureAwait(false)); + return Ok(Get(false, false, true, true)); } /// <summary> @@ -264,7 +265,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromBody, Required] UpdateUserPassword request) { - if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password."); } @@ -282,21 +283,24 @@ namespace Jellyfin.Api.Controllers } else { - var success = await _userManager.AuthenticateUser( - user.Username, - request.CurrentPw, - request.CurrentPw, - HttpContext.GetNormalizedRemoteIp().ToString(), - false).ConfigureAwait(false); - - if (success == null) + if (!User.IsInRole(UserRoles.Administrator)) { - return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered."); + var success = await _userManager.AuthenticateUser( + user.Username, + request.CurrentPw, + request.CurrentPw, + HttpContext.GetNormalizedRemoteIp().ToString(), + false).ConfigureAwait(false); + + if (success == null) + { + return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered."); + } } await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); - var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token; + var currentToken = User.GetToken(); await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); } @@ -322,7 +326,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromBody, Required] UpdateUserEasyPassword request) { - if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false)) + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password."); } @@ -364,7 +368,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromBody, Required] UserDto updateUser) { - if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false)) + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed."); } @@ -424,7 +428,7 @@ namespace Jellyfin.Api.Controllers return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system."); } - var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token; + var currentToken = User.GetToken(); await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); } @@ -449,7 +453,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromBody, Required] UserConfiguration userConfig) { - if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false)) + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) { return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed"); } @@ -499,7 +503,7 @@ namespace Jellyfin.Api.Controllers if (isLocal) { - _logger.LogWarning("Password reset proccess initiated from outside the local network with IP: {IP}", ip); + _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip); } var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false); @@ -533,13 +537,13 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] public ActionResult<UserDto> GetCurrentUser() { - var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); - if (userId is null) + var userId = User.GetUserId(); + if (userId.Equals(default)) { return BadRequest(); } - var user = _userManager.GetUserById(userId.Value); + var user = _userManager.GetUserById(userId); if (user == null) { return BadRequest(); @@ -548,7 +552,7 @@ namespace Jellyfin.Api.Controllers return _userManager.GetUserDto(user); } - private async Task<IEnumerable<UserDto>> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) + private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) { var users = _userManager.Users; @@ -564,7 +568,7 @@ namespace Jellyfin.Api.Controllers if (filterByDevice) { - var deviceId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId; + var deviceId = User.GetDeviceId(); if (!string.IsNullOrWhiteSpace(deviceId)) { diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index e45f9b58cd..ee8a17b62d 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -8,7 +8,6 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; -using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -82,7 +81,7 @@ namespace Jellyfin.Api.Controllers await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } @@ -99,7 +98,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); var item = _libraryManager.GetUserRootFolder(); - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } @@ -121,7 +120,7 @@ namespace Jellyfin.Api.Controllers : _libraryManager.GetItemById(itemId); var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false); - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray(); return new QueryResult<BaseItemDto>(dtos); @@ -201,7 +200,7 @@ namespace Jellyfin.Api.Controllers ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); if (item is IHasTrailers hasTrailers) { @@ -231,10 +230,11 @@ namespace Jellyfin.Api.Controllers ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); return Ok(item - .GetExtras(BaseItem.DisplayExtraTypes) + .GetExtras() + .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value)) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))); } @@ -280,7 +280,7 @@ namespace Jellyfin.Api.Controllers } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var list = _userViewManager.GetLatestItems( diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 5cc8c906fb..85d154cac2 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; -using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; @@ -11,9 +10,7 @@ using Jellyfin.Api.Models.UserViewDtos; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; @@ -32,7 +29,6 @@ namespace Jellyfin.Api.Controllers private readonly IUserManager _userManager; private readonly IUserViewManager _userViewManager; private readonly IDtoService _dtoService; - private readonly IAuthorizationContext _authContext; private readonly ILibraryManager _libraryManager; /// <summary> @@ -41,19 +37,16 @@ namespace Jellyfin.Api.Controllers /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> - /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> public UserViewsController( IUserManager userManager, IUserViewManager userViewManager, IDtoService dtoService, - IAuthorizationContext authContext, ILibraryManager libraryManager) { _userManager = userManager; _userViewManager = userViewManager; _dtoService = dtoService; - _authContext = authContext; _libraryManager = libraryManager; } @@ -92,7 +85,7 @@ namespace Jellyfin.Api.Controllers var folders = _userViewManager.GetUserViews(query); - var dtoOptions = new DtoOptions().AddClientFields(Request); + var dtoOptions = new DtoOptions().AddClientFields(User); var fields = dtoOptions.Fields.ToList(); fields.Add(ItemFields.PrimaryImageAspectRatio); @@ -138,7 +131,8 @@ namespace Jellyfin.Api.Controllers Name = i.Name, Id = i.Id.ToString("N", CultureInfo.InvariantCulture) }) - .OrderBy(i => i.Name)); + .OrderBy(i => i.Name) + .AsEnumerable()); } } } diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 62c05331ed..bf08ad376b 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -43,7 +43,6 @@ namespace Jellyfin.Api.Controllers private readonly IUserManager _userManager; private readonly IDtoService _dtoService; private readonly IDlnaManager _dlnaManager; - private readonly IAuthorizationContext _authContext; private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; @@ -61,7 +60,6 @@ namespace Jellyfin.Api.Controllers /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> @@ -74,7 +72,6 @@ namespace Jellyfin.Api.Controllers IUserManager userManager, IDtoService dtoService, IDlnaManager dlnaManager, - IAuthorizationContext authContext, IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, @@ -87,7 +84,6 @@ namespace Jellyfin.Api.Controllers _userManager = userManager; _dtoService = dtoService; _dlnaManager = dlnaManager; - _authContext = authContext; _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; @@ -120,7 +116,7 @@ namespace Jellyfin.Api.Controllers : _libraryManager.GetItemById(itemId); var dtoOptions = new DtoOptions(); - dtoOptions = dtoOptions.AddClientFields(Request); + dtoOptions = dtoOptions.AddClientFields(User); BaseItemDto[] items; if (item is Video video) @@ -427,10 +423,9 @@ namespace Jellyfin.Api.Controllers StreamOptions = streamOptions }; - using var state = await StreamingHelpers.GetStreamingState( + var state = await StreamingHelpers.GetStreamingState( streamingRequest, - Request, - _authContext, + HttpContext, _mediaSourceManager, _userManager, _libraryManager, diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 7c02e25501..b732bdff3c 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableImages = true) { var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(Request) + .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = userId is null || userId.Value.Equals(default) @@ -179,7 +179,7 @@ namespace Jellyfin.Api.Controllers } var dtoOptions = new DtoOptions() - .AddClientFields(Request); + .AddClientFields(User); if (userId.HasValue && !userId.Value.Equals(default)) { diff --git a/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs b/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000000..6b3e78d4d1 --- /dev/null +++ b/Jellyfin.Api/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using System.Security.Claims; +using Jellyfin.Api.Constants; + +namespace Jellyfin.Api.Extensions; + +/// <summary> +/// Extensions for <see cref="ClaimsPrincipal"/>. +/// </summary> +public static class ClaimsPrincipalExtensions +{ + /// <summary> + /// Get user id from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>User id.</returns> + public static Guid GetUserId(this ClaimsPrincipal user) + { + var value = GetClaimValue(user, InternalClaimTypes.UserId); + return string.IsNullOrEmpty(value) + ? default + : Guid.Parse(value); + } + + /// <summary> + /// Get device id from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>Device id.</returns> + public static string? GetDeviceId(this ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.DeviceId); + + /// <summary> + /// Get device from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>Device.</returns> + public static string? GetDevice(this ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Device); + + /// <summary> + /// Get client from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>Client.</returns> + public static string? GetClient(this ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Client); + + /// <summary> + /// Get version from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>Version.</returns> + public static string? GetVersion(this ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Version); + + /// <summary> + /// Get token from claims. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>Token.</returns> + public static string? GetToken(this ClaimsPrincipal user) + => GetClaimValue(user, InternalClaimTypes.Token); + + /// <summary> + /// Gets a flag specifying whether the request is using an api key. + /// </summary> + /// <param name="user">Current claims principal.</param> + /// <returns>The flag specifying whether the request is using an api key.</returns> + public static bool GetIsApiKey(this ClaimsPrincipal user) + { + var claimValue = GetClaimValue(user, InternalClaimTypes.IsApiKey); + return !string.IsNullOrEmpty(claimValue) + && bool.TryParse(claimValue, out var parsedClaimValue) + && parsedClaimValue; + } + + private static string? GetClaimValue(in ClaimsPrincipal user, string name) + => user.Claims.FirstOrDefault(claim => claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase))?.Value; +} diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs index 5e338b67da..9e784f7c45 100644 --- a/Jellyfin.Api/Extensions/DtoExtensions.cs +++ b/Jellyfin.Api/Extensions/DtoExtensions.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Jellyfin.Api.Helpers; +using System.Security.Claims; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Entities; @@ -22,14 +22,14 @@ namespace Jellyfin.Api.Extensions /// Legacy order: 2. /// </remarks> /// <param name="dtoOptions">DtoOptions object.</param> - /// <param name="request">Current request.</param> + /// <param name="user">Current claims principal.</param> /// <returns>Modified DtoOptions object.</returns> internal static DtoOptions AddClientFields( - this DtoOptions dtoOptions, HttpRequest request) + this DtoOptions dtoOptions, ClaimsPrincipal user) { dtoOptions.Fields ??= Array.Empty<ItemFields>(); - string? client = ClaimHelpers.GetClient(request.HttpContext.User); + string? client = user.GetClient(); // No client in claim if (string.IsNullOrEmpty(client)) diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs index 27497cd599..bc83ff48a8 100644 --- a/Jellyfin.Api/Helpers/AudioHelper.cs +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -11,7 +11,6 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; @@ -25,7 +24,6 @@ namespace Jellyfin.Api.Helpers public class AudioHelper { private readonly IDlnaManager _dlnaManager; - private readonly IAuthorizationContext _authContext; private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; private readonly IMediaSourceManager _mediaSourceManager; @@ -41,7 +39,6 @@ namespace Jellyfin.Api.Helpers /// Initializes a new instance of the <see cref="AudioHelper"/> class. /// </summary> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> @@ -54,7 +51,6 @@ namespace Jellyfin.Api.Helpers /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public AudioHelper( IDlnaManager dlnaManager, - IAuthorizationContext authContext, IUserManager userManager, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, @@ -67,7 +63,6 @@ namespace Jellyfin.Api.Helpers EncodingHelper encodingHelper) { _dlnaManager = dlnaManager; - _authContext = authContext; _userManager = userManager; _libraryManager = libraryManager; _mediaSourceManager = mediaSourceManager; @@ -102,8 +97,7 @@ namespace Jellyfin.Api.Helpers using var state = await StreamingHelpers.GetStreamingState( streamingRequest, - _httpContextAccessor.HttpContext.Request, - _authContext, + _httpContextAccessor.HttpContext, _mediaSourceManager, _userManager, _libraryManager, diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs deleted file mode 100644 index c1c2f93b49..0000000000 --- a/Jellyfin.Api/Helpers/ClaimHelpers.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Linq; -using System.Security.Claims; -using Jellyfin.Api.Constants; - -namespace Jellyfin.Api.Helpers -{ - /// <summary> - /// Claim Helpers. - /// </summary> - public static class ClaimHelpers - { - /// <summary> - /// Get user id from claims. - /// </summary> - /// <param name="user">Current claims principal.</param> - /// <returns>User id.</returns> - public static Guid? GetUserId(in ClaimsPrincipal user) - { - var value = GetClaimValue(user, InternalClaimTypes.UserId); - return string.IsNullOrEmpty(value) - ? null - : Guid.Parse(value); - } - - /// <summary> - /// Get device id from claims. - /// </summary> - /// <param name="user">Current claims principal.</param> - /// <returns>Device id.</returns> - public static string? GetDeviceId(in ClaimsPrincipal user) - => GetClaimValue(user, InternalClaimTypes.DeviceId); - - /// <summary> - /// Get device from claims. - /// </summary> - /// <param name="user">Current claims principal.</param> - /// <returns>Device.</returns> - public static string? GetDevice(in ClaimsPrincipal user) - => GetClaimValue(user, InternalClaimTypes.Device); - - /// <summary> - /// Get client from claims. - /// </summary> - /// <param name="user">Current claims principal.</param> - /// <returns>Client.</returns> - public static string? GetClient(in ClaimsPrincipal user) - => GetClaimValue(user, InternalClaimTypes.Client); - - /// <summary> - /// Get version from claims. - /// </summary> - /// <param name="user">Current claims principal.</param> - /// <returns>Version.</returns> - public static string? GetVersion(in ClaimsPrincipal user) - => GetClaimValue(user, InternalClaimTypes.Version); - - /// <summary> - /// Get token from claims. - /// </summary> - /// <param name="user">Current claims principal.</param> - /// <returns>Token.</returns> - public static string? GetToken(in ClaimsPrincipal user) - => GetClaimValue(user, InternalClaimTypes.Token); - - /// <summary> - /// Gets a flag specifying whether the request is using an api key. - /// </summary> - /// <param name="user">Current claims principal.</param> - /// <returns>The flag specifying whether the request is using an api key.</returns> - public static bool GetIsApiKey(in ClaimsPrincipal user) - { - var claimValue = GetClaimValue(user, InternalClaimTypes.IsApiKey); - return !string.IsNullOrEmpty(claimValue) - && bool.TryParse(claimValue, out var parsedClaimValue) - && parsedClaimValue; - } - - private static string? GetClaimValue(in ClaimsPrincipal user, string name) - { - return user?.Identities - .SelectMany(c => c.Claims) - .Where(claim => claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase)) - .Select(claim => claim.Value) - .FirstOrDefault(); - } - } -} diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 02af2e4353..fa392e5674 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -7,6 +7,7 @@ using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -15,7 +16,6 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Net; @@ -34,7 +34,6 @@ namespace Jellyfin.Api.Helpers private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IDlnaManager _dlnaManager; - private readonly IAuthorizationContext _authContext; private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; @@ -51,7 +50,6 @@ namespace Jellyfin.Api.Helpers /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> @@ -65,7 +63,6 @@ namespace Jellyfin.Api.Helpers ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, - IAuthorizationContext authContext, IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, @@ -79,7 +76,6 @@ namespace Jellyfin.Api.Helpers _libraryManager = libraryManager; _userManager = userManager; _dlnaManager = dlnaManager; - _authContext = authContext; _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; @@ -128,8 +124,7 @@ namespace Jellyfin.Api.Helpers using var state = await StreamingHelpers.GetStreamingState( streamingRequest, - _httpContextAccessor.HttpContext.Request, - _authContext, + _httpContextAccessor.HttpContext, _mediaSourceManager, _userManager, _libraryManager, @@ -216,7 +211,7 @@ namespace Jellyfin.Api.Helpers var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main"); sdrVideoUrl += "&AllowVideoStreamCopy=false"; - var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0; + var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0; var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; @@ -483,7 +478,7 @@ namespace Jellyfin.Api.Helpers state.Request.MediaSourceId, stream.Index.ToString(CultureInfo.InvariantCulture), 30.ToString(CultureInfo.InvariantCulture), - ClaimHelpers.GetToken(user)); + user.GetToken()); var line = string.Format( CultureInfo.InvariantCulture, diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 31b9798365..4441ae023e 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -2,9 +2,11 @@ using System.Globalization; using System.Linq; using System.Net; +using System.Security.Claims; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Extensions; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; @@ -15,7 +17,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -39,7 +40,6 @@ namespace Jellyfin.Api.Helpers private readonly ILogger<MediaInfoHelper> _logger; private readonly INetworkManager _networkManager; private readonly IDeviceManager _deviceManager; - private readonly IAuthorizationContext _authContext; /// <summary> /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class. @@ -52,7 +52,6 @@ namespace Jellyfin.Api.Helpers /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param> /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> - /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> public MediaInfoHelper( IUserManager userManager, ILibraryManager libraryManager, @@ -61,8 +60,7 @@ namespace Jellyfin.Api.Helpers IServerConfigurationManager serverConfigurationManager, ILogger<MediaInfoHelper> logger, INetworkManager networkManager, - IDeviceManager deviceManager, - IAuthorizationContext authContext) + IDeviceManager deviceManager) { _userManager = userManager; _libraryManager = libraryManager; @@ -72,7 +70,6 @@ namespace Jellyfin.Api.Helpers _logger = logger; _networkManager = networkManager; _deviceManager = deviceManager; - _authContext = authContext; } /// <summary> @@ -147,7 +144,7 @@ namespace Jellyfin.Api.Helpers /// <param name="item">Item to set data for.</param> /// <param name="mediaSource">Media source info.</param> /// <param name="profile">Device profile.</param> - /// <param name="auth">Authorization info.</param> + /// <param name="claimsPrincipal">Current claims principal.</param> /// <param name="maxBitrate">Max bitrate.</param> /// <param name="startTimeTicks">Start time ticks.</param> /// <param name="mediaSourceId">Media source id.</param> @@ -166,7 +163,7 @@ namespace Jellyfin.Api.Helpers BaseItem item, MediaSourceInfo mediaSource, DeviceProfile profile, - AuthorizationInfo auth, + ClaimsPrincipal claimsPrincipal, int? maxBitrate, long startTimeTicks, string mediaSourceId, @@ -188,7 +185,7 @@ namespace Jellyfin.Api.Helpers { MediaSources = new[] { mediaSource }, Context = EncodingContext.Streaming, - DeviceId = auth.DeviceId, + DeviceId = claimsPrincipal.GetDeviceId(), ItemId = item.Id, Profile = profile, MaxAudioChannels = maxAudioChannels, @@ -256,9 +253,17 @@ namespace Jellyfin.Api.Helpers streamInfo.StartPositionTicks = startTimeTicks; mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay; + // Players do not handle this being set according to PlayMethod - mediaSource.SupportsDirectStream = options.EnableDirectStream ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream : streamInfo.PlayMethod == PlayMethod.DirectPlay; - mediaSource.SupportsTranscoding = streamInfo.PlayMethod == PlayMethod.DirectStream || mediaSource.TranscodingContainer != null; + mediaSource.SupportsDirectStream = + options.EnableDirectStream + ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream + : streamInfo.PlayMethod == PlayMethod.DirectPlay; + + mediaSource.SupportsTranscoding = + streamInfo.PlayMethod == PlayMethod.DirectStream + || mediaSource.TranscodingContainer != null + || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context); if (item is Audio) { @@ -282,7 +287,7 @@ namespace Jellyfin.Api.Helpers mediaSource.SupportsDirectPlay = false; mediaSource.SupportsDirectStream = false; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-'); mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; mediaSource.TranscodingContainer = streamInfo.Container; @@ -290,10 +295,10 @@ namespace Jellyfin.Api.Helpers } else { - if (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream) + if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream)) { streamInfo.PlayMethod = PlayMethod.Transcode; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-'); if (!allowVideoStreamCopy) { @@ -308,7 +313,8 @@ namespace Jellyfin.Api.Helpers } // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + // The token must not be null + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!); mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; } @@ -376,19 +382,17 @@ namespace Jellyfin.Api.Helpers /// <summary> /// Open media source. /// </summary> - /// <param name="httpRequest">Http Request.</param> + /// <param name="httpContext">Http Context.</param> /// <param name="request">Live stream request.</param> /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns> - public async Task<LiveStreamResponse> OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request) + public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request) { - var authInfo = await _authContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false); - var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false); var profile = request.DeviceProfile; if (profile == null) { - var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId); + var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId()); if (clientCapabilities != null) { profile = clientCapabilities.DeviceProfile; @@ -403,7 +407,7 @@ namespace Jellyfin.Api.Helpers item, result.MediaSource, profile, - authInfo, + httpContext.User, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, result.MediaSource.Id, @@ -417,7 +421,7 @@ namespace Jellyfin.Api.Helpers true, true, true, - httpRequest.HttpContext.GetNormalizedRemoteIp()); + httpContext.GetNormalizedRemoteIp()); } else { diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 20427d7fab..8c5af013aa 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Extensions; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -55,37 +58,42 @@ namespace Jellyfin.Api.Helpers /// <summary> /// Checks if the user can update an entry. /// </summary> - /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> - /// <param name="requestContext">The <see cref="HttpRequest"/>.</param> + /// <param name="userManager">An instance of the <see cref="IUserManager"/> interface.</param> + /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param> /// <param name="userId">The user id.</param> /// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param> /// <returns>A <see cref="bool"/> whether the user can update the entry.</returns> - internal static async Task<bool> AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences) + internal static bool AssertCanUpdateUser(IUserManager userManager, ClaimsPrincipal claimsPrincipal, Guid userId, bool restrictUserPreferences) { - var auth = await authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false); - - var authenticatedUser = auth.User; + var authenticatedUserId = claimsPrincipal.GetUserId(); + var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator); // If they're going to update the record of another user, they must be an administrator - if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator)) - || (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess)) + if (!userId.Equals(authenticatedUserId) && !isAdministrator) { return false; } - return true; + // TODO the EnableUserPreferenceAccess policy does not seem to be used elsewhere + if (!restrictUserPreferences || isAdministrator) + { + return true; + } + + var user = userManager.GetUserById(userId); + return user.EnableUserPreferenceAccess; } - internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request) + internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext) { - var authorization = await authContext.GetAuthorizationInfo(request).ConfigureAwait(false); - var user = authorization.User; + var userId = httpContext.User.GetUserId(); + var user = userManager.GetUserById(userId); var session = await sessionManager.LogSessionActivity( - authorization.Client, - authorization.Version, - authorization.DeviceId, - authorization.Device, - request.HttpContext.GetNormalizedRemoteIp().ToString(), + httpContext.User.GetClient(), + httpContext.User.GetVersion(), + httpContext.User.GetDeviceId(), + httpContext.User.GetDevice(), + httpContext.GetNormalizedRemoteIp().ToString(), user).ConfigureAwait(false); if (session == null) @@ -96,9 +104,9 @@ namespace Jellyfin.Api.Helpers return session; } - internal static async Task<string> GetSessionId(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request) + internal static async Task<string> GetSessionId(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext) { - var session = await GetSession(sessionManager, authContext, request).ConfigureAwait(false); + var session = await GetSession(sessionManager, userManager, httpContext).ConfigureAwait(false); return session.Id; } diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 34dab75b82..3705737739 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; @@ -14,7 +15,6 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -33,8 +33,7 @@ namespace Jellyfin.Api.Helpers /// Gets the current streaming state. /// </summary> /// <param name="streamingRequest">The <see cref="StreamingRequestDto"/>.</param> - /// <param name="httpRequest">The <see cref="HttpRequest"/>.</param> - /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> + /// <param name="httpContext">The <see cref="HttpContext"/>.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> @@ -49,8 +48,7 @@ namespace Jellyfin.Api.Helpers /// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns> public static async Task<StreamState> GetStreamingState( StreamingRequestDto streamingRequest, - HttpRequest httpRequest, - IAuthorizationContext authorizationContext, + HttpContext httpContext, IMediaSourceManager mediaSourceManager, IUserManager userManager, ILibraryManager libraryManager, @@ -63,6 +61,7 @@ namespace Jellyfin.Api.Helpers TranscodingJobType transcodingJobType, CancellationToken cancellationToken) { + var httpRequest = httpContext.Request; // Parse the DLNA time seek header if (!streamingRequest.StartTimeTicks.HasValue) { @@ -101,10 +100,10 @@ namespace Jellyfin.Api.Helpers EnableDlnaHeaders = enableDlnaHeaders }; - var auth = await authorizationContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false); - if (!auth.UserId.Equals(default)) + var userId = httpContext.User.GetUserId(); + if (!userId.Equals(default)) { - state.User = userManager.GetUserById(auth.UserId); + state.User = userManager.GetUserById(userId); } if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec)) @@ -179,7 +178,7 @@ namespace Jellyfin.Api.Helpers { containerInternal = streamingRequest.Static ? StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio) - : GetOutputFileExtension(state); + : GetOutputFileExtension(state, mediaSource); } state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); @@ -235,7 +234,7 @@ namespace Jellyfin.Api.Helpers ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static); var ext = string.IsNullOrWhiteSpace(state.OutputContainer) - ? GetOutputFileExtension(state) + ? GetOutputFileExtension(state, mediaSource) : ("." + state.OutputContainer); state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); @@ -312,7 +311,7 @@ namespace Jellyfin.Api.Helpers responseHeaders.Add( "contentFeatures.dlna.org", - ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty); + ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty); } } @@ -409,8 +408,9 @@ namespace Jellyfin.Api.Helpers /// Gets the output file extension. /// </summary> /// <param name="state">The state.</param> + /// <param name="mediaSource">The mediaSource.</param> /// <returns>System.String.</returns> - private static string? GetOutputFileExtension(StreamState state) + private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) { var ext = Path.GetExtension(state.RequestedUrl); @@ -425,7 +425,7 @@ namespace Jellyfin.Api.Helpers var videoCodec = state.Request.VideoCodec; if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) || - string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)) + string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) { return ".ts"; } @@ -474,6 +474,13 @@ namespace Jellyfin.Api.Helpers } } + // Fallback to the container of mediaSource + if (!string.IsNullOrEmpty(mediaSource?.Container)) + { + var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase); + return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim(); + } + return null; } @@ -533,6 +540,7 @@ namespace Jellyfin.Api.Helpers state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetVideoProfile, + state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 416418dc68..c663c6e310 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Data.Enums; @@ -17,7 +18,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; @@ -46,7 +46,6 @@ namespace Jellyfin.Api.Helpers private readonly IAttachmentExtractor _attachmentExtractor; private readonly IApplicationPaths _appPaths; - private readonly IAuthorizationContext _authorizationContext; private readonly EncodingHelper _encodingHelper; private readonly IFileSystem _fileSystem; private readonly ILogger<TranscodingJobHelper> _logger; @@ -55,6 +54,7 @@ namespace Jellyfin.Api.Helpers private readonly IServerConfigurationManager _serverConfigurationManager; private readonly ISessionManager _sessionManager; private readonly ILoggerFactory _loggerFactory; + private readonly IUserManager _userManager; /// <summary> /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class. @@ -67,9 +67,9 @@ namespace Jellyfin.Api.Helpers /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> - /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> public TranscodingJobHelper( IAttachmentExtractor attachmentExtractor, IApplicationPaths appPaths, @@ -79,9 +79,9 @@ namespace Jellyfin.Api.Helpers IMediaEncoder mediaEncoder, IServerConfigurationManager serverConfigurationManager, ISessionManager sessionManager, - IAuthorizationContext authorizationContext, EncodingHelper encodingHelper, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory, + IUserManager userManager) { _attachmentExtractor = attachmentExtractor; _appPaths = appPaths; @@ -91,9 +91,9 @@ namespace Jellyfin.Api.Helpers _mediaEncoder = mediaEncoder; _serverConfigurationManager = serverConfigurationManager; _sessionManager = sessionManager; - _authorizationContext = authorizationContext; _encodingHelper = encodingHelper; _loggerFactory = loggerFactory; + _userManager = userManager; DeleteEncodedMediaCache(); @@ -512,8 +512,9 @@ namespace Jellyfin.Api.Helpers if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) { - var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false); - if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)) + var userId = request.HttpContext.User.GetUserId(); + var user = userId.Equals(default) ? null : _userManager.GetUserById(userId); + if (user != null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)) { this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state); @@ -654,8 +655,8 @@ namespace Jellyfin.Api.Helpers { if (EnableThrottling(state)) { - transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem); - state.TranscodingThrottler.Start(); + transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder); + transcodingJob.TranscodingThrottler.Start(); } } @@ -663,18 +664,11 @@ namespace Jellyfin.Api.Helpers { var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - // enable throttling when NOT using hardware acceleration - if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) - { - return state.InputProtocol == MediaProtocol.File && - state.RunTimeTicks.HasValue && - state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && - state.IsInputVideo && - state.VideoType == VideoType.VideoFile && - !EncodingHelper.IsCopyCodec(state.OutputVideoCodec); - } - - return false; + return state.InputProtocol == MediaProtocol.File && + state.RunTimeTicks.HasValue && + state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && + state.IsInputVideo && + state.VideoType == VideoType.VideoFile; } /// <summary> diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 2b0436e342..7e64cf645b 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -17,10 +17,10 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.3" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.9" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> - <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.0" /> + <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" /> </ItemGroup> <ItemGroup> @@ -36,7 +36,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs index 7a1ca252cf..99376873c0 100644 --- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs +++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -17,6 +18,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos private readonly ILogger<TranscodingThrottler> _logger; private readonly IConfigurationManager _config; private readonly IFileSystem _fileSystem; + private readonly IMediaEncoder _mediaEncoder; private Timer? _timer; private bool _isPaused; @@ -27,12 +29,14 @@ namespace Jellyfin.Api.Models.PlaybackDtos /// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param> /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem) + /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> + public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder) { _job = job; _logger = logger; _config = config; _fileSystem = fileSystem; + _mediaEncoder = mediaEncoder; } /// <summary> @@ -55,7 +59,8 @@ namespace Jellyfin.Api.Models.PlaybackDtos try { - await _job.Process!.StandardInput.WriteLineAsync().ConfigureAwait(false); + var resumeKey = _mediaEncoder.IsPkeyPauseSupported ? "u" : Environment.NewLine; + await _job.Process!.StandardInput.WriteAsync(resumeKey).ConfigureAwait(false); _isPaused = false; } catch (Exception ex) @@ -125,11 +130,13 @@ namespace Jellyfin.Api.Models.PlaybackDtos { if (!_isPaused) { - _logger.LogDebug("Sending pause command to ffmpeg"); + var pauseKey = _mediaEncoder.IsPkeyPauseSupported ? "p" : "c"; + + _logger.LogDebug("Sending pause command [{Key}] to ffmpeg", pauseKey); try { - await _job.Process!.StandardInput.WriteAsync("c").ConfigureAwait(false); + await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false); _isPaused = true; } catch (Exception ex) diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs index cbabf087bc..8182e3c9e8 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -48,11 +48,6 @@ namespace Jellyfin.Api.Models.StreamingDtos } /// <summary> - /// Gets or sets the transcoding throttler. - /// </summary> - public TranscodingThrottler? TranscodingThrottler { get; set; } - - /// <summary> /// Gets the video request. /// </summary> public VideoRequestDto? VideoRequest => Request as VideoRequestDto; @@ -174,7 +169,7 @@ namespace Jellyfin.Api.Models.StreamingDtos /// <summary> /// Disposes the stream state. /// </summary> - /// <param name="disposing">Whether the object is currently beeing disposed.</param> + /// <param name="disposing">Whether the object is currently being disposed.</param> protected virtual void Dispose(bool disposing) { if (_disposed) @@ -191,11 +186,8 @@ namespace Jellyfin.Api.Models.StreamingDtos { _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult(); } - - TranscodingThrottler?.Dispose(); } - TranscodingThrottler = null; TranscodingJob = null; _disposed = true; diff --git a/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs index 02ce5a0488..226a584e1d 100644 --- a/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs +++ b/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs @@ -17,9 +17,9 @@ namespace Jellyfin.Api.Models.SyncPlayDtos } /// <summary> - /// Gets or sets the playlist identifiers ot the items. Ignored when clearing the playlist. + /// Gets or sets the playlist identifiers of the items. Ignored when clearing the playlist. /// </summary> - /// <value>The playlist identifiers ot the items.</value> + /// <value>The playlist identifiers of the items.</value> public IReadOnlyList<Guid> PlaylistItemIds { get; set; } /// <summary> diff --git a/Jellyfin.Api/Results/OkResultOfT.cs b/Jellyfin.Api/Results/OkResultOfT.cs new file mode 100644 index 0000000000..f60cbbceea --- /dev/null +++ b/Jellyfin.Api/Results/OkResultOfT.cs @@ -0,0 +1,21 @@ +#pragma warning disable SA1649 // File name should match type name. + +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Results; + +/// <summary> +/// Ok result with type specified. +/// </summary> +/// <typeparam name="T">The type to return.</typeparam> +public class OkResult<T> : OkObjectResult +{ + /// <summary> + /// Initializes a new instance of the <see cref="OkResult{T}"/> class. + /// </summary> + /// <param name="value">The value to return.</param> + public OkResult(T value) + : base(value) + { + } +} |
