mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-11 10:10:35 +01:00
Merge remote-tracking branch 'upstream/master' into client-logger
This commit is contained in:
@@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
|
||||
{
|
||||
StartIndex = startIndex,
|
||||
Skip = startIndex,
|
||||
Limit = limit,
|
||||
MinDate = minDate,
|
||||
HasUserId = hasUserId
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -18,24 +15,15 @@ namespace Jellyfin.Api.Controllers
|
||||
[Route("Auth")]
|
||||
public class ApiKeyController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
private readonly IAuthenticationManager _authenticationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApiKeyController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
||||
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
||||
/// <param name="authRepo">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
|
||||
public ApiKeyController(
|
||||
ISessionManager sessionManager,
|
||||
IServerApplicationHost appHost,
|
||||
IAuthenticationRepository authRepo)
|
||||
/// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param>
|
||||
public ApiKeyController(IAuthenticationManager authenticationManager)
|
||||
{
|
||||
_sessionManager = sessionManager;
|
||||
_appHost = appHost;
|
||||
_authRepo = authRepo;
|
||||
_authenticationManager = authenticationManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -46,14 +34,15 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Keys")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<AuthenticationInfo>> GetKeys()
|
||||
public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys()
|
||||
{
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
HasUser = false
|
||||
});
|
||||
var keys = await _authenticationManager.GetApiKeys();
|
||||
|
||||
return result;
|
||||
return new QueryResult<AuthenticationInfo>
|
||||
{
|
||||
Items = keys,
|
||||
TotalRecordCount = keys.Count
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,17 +54,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Keys")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult CreateKey([FromQuery, Required] string app)
|
||||
public async Task<ActionResult> CreateKey([FromQuery, Required] string app)
|
||||
{
|
||||
_authRepo.Create(new AuthenticationInfo
|
||||
{
|
||||
AppName = app,
|
||||
AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DeviceId = _appHost.SystemId,
|
||||
DeviceName = _appHost.FriendlyName,
|
||||
AppVersion = _appHost.ApplicationVersionString
|
||||
});
|
||||
await _authenticationManager.CreateApiKey(app).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -88,9 +70,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpDelete("Keys/{key}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult RevokeKey([FromRoute, Required] string key)
|
||||
public async Task<ActionResult> RevokeKey([FromRoute, Required] string key)
|
||||
{
|
||||
_sessionManager.RevokeToken(key);
|
||||
await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <param name="enableTotalRecordCount">Total record count.</param>
|
||||
/// <response code="200">Artists returned.</response>
|
||||
@@ -112,6 +114,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
@@ -150,7 +154,8 @@ namespace Jellyfin.Api.Controllers
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
EnableTotalRecordCount = enableTotalRecordCount
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
||||
};
|
||||
|
||||
if (parentId.HasValue)
|
||||
@@ -276,6 +281,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <param name="enableTotalRecordCount">Total record count.</param>
|
||||
/// <response code="200">Album artists returned.</response>
|
||||
@@ -311,6 +318,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
@@ -349,7 +358,8 @@ namespace Jellyfin.Api.Controllers
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
EnableTotalRecordCount = enableTotalRecordCount
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
||||
};
|
||||
|
||||
if (parentId.HasValue)
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
@@ -241,7 +241,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] Guid? parentId,
|
||||
[FromQuery] bool isLocked = false)
|
||||
{
|
||||
var userId = _authContext.GetAuthorizationInfo(Request).UserId;
|
||||
var userId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).UserId;
|
||||
|
||||
var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net.Mime;
|
||||
using System.Text.Json;
|
||||
@@ -5,7 +6,7 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.ConfigurationDtos;
|
||||
using MediaBrowser.Common.Json;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
@@ -94,6 +95,11 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
var configurationType = _configurationManager.GetConfigurationType(key);
|
||||
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false);
|
||||
if (configuration == null)
|
||||
{
|
||||
throw new ArgumentException("Body doesn't contain a valid configuration");
|
||||
}
|
||||
|
||||
_configurationManager.SaveConfiguration(key, configuration);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (enableInMainMenu.HasValue)
|
||||
{
|
||||
configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList();
|
||||
configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
|
||||
}
|
||||
|
||||
return configPages;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Data.Dtos;
|
||||
using Jellyfin.Data.Entities.Security;
|
||||
using Jellyfin.Data.Queries;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Devices;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@@ -19,22 +22,18 @@ namespace Jellyfin.Api.Controllers
|
||||
public class DevicesController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly IAuthenticationRepository _authenticationRepository;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DevicesController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
|
||||
/// <param name="authenticationRepository">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
|
||||
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
||||
public DevicesController(
|
||||
IDeviceManager deviceManager,
|
||||
IAuthenticationRepository authenticationRepository,
|
||||
ISessionManager sessionManager)
|
||||
{
|
||||
_deviceManager = deviceManager;
|
||||
_authenticationRepository = authenticationRepository;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
@@ -47,10 +46,9 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
|
||||
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
|
||||
{
|
||||
var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
|
||||
return _deviceManager.GetDevices(deviceQuery);
|
||||
return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,9 +61,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Info")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
|
||||
public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
|
||||
{
|
||||
var deviceInfo = _deviceManager.GetDevice(id);
|
||||
var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
||||
if (deviceInfo == null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -84,9 +82,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Options")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
|
||||
public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
|
||||
{
|
||||
var deviceInfo = _deviceManager.GetDeviceOptions(id);
|
||||
var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
|
||||
if (deviceInfo == null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -101,22 +99,14 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="id">Device Id.</param>
|
||||
/// <param name="deviceOptions">Device Options.</param>
|
||||
/// <response code="204">Device options updated.</response>
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Options")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UpdateDeviceOptions(
|
||||
public async Task<ActionResult> UpdateDeviceOptions(
|
||||
[FromQuery, Required] string id,
|
||||
[FromBody, Required] DeviceOptions deviceOptions)
|
||||
[FromBody, Required] DeviceOptionsDto deviceOptions)
|
||||
{
|
||||
var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
|
||||
if (existingDeviceOptions == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_deviceManager.UpdateDeviceOptions(id, deviceOptions);
|
||||
await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -130,19 +120,19 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpDelete]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult DeleteDevice([FromQuery, Required] string id)
|
||||
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
|
||||
{
|
||||
var existingDevice = _deviceManager.GetDevice(id);
|
||||
var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
||||
if (existingDevice == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items;
|
||||
var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
|
||||
|
||||
foreach (var session in sessions)
|
||||
foreach (var session in sessions.Items)
|
||||
{
|
||||
_sessionManager.Logout(session);
|
||||
await _sessionManager.Logout(session).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
|
||||
@@ -137,7 +137,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
|
||||
existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
|
||||
existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
|
||||
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
|
||||
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ using System.Threading.Tasks;
|
||||
using Emby.Dlna;
|
||||
using Emby.Dlna.Main;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Dlna Server Controller.
|
||||
/// </summary>
|
||||
[Route("Dlna")]
|
||||
[Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
|
||||
public class DlnaServerController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -28,7 +27,6 @@ using MediaBrowser.Model.Net;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
@@ -151,7 +149,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
@@ -317,7 +315,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
@@ -483,7 +481,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
@@ -545,7 +543,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] EncodingContext? context,
|
||||
[FromQuery] Dictionary<string, string> streamOptions)
|
||||
{
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
var streamingRequest = new VideoRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
@@ -710,7 +708,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] EncodingContext? context,
|
||||
[FromQuery] Dictionary<string, string> streamOptions)
|
||||
{
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
var streamingRequest = new StreamingRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
@@ -814,7 +812,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
@@ -1138,7 +1136,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase);
|
||||
var hlsVersion = isHlsInFmp4 ? "7" : "3";
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var builder = new StringBuilder(128);
|
||||
|
||||
builder.AppendLine("#EXTM3U")
|
||||
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
|
||||
@@ -1191,6 +1189,7 @@ namespace Jellyfin.Api.Controllers
|
||||
throw new ArgumentException("StartTimeTicks is not allowed.");
|
||||
}
|
||||
|
||||
// CTS lifecycle is managed internally.
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
@@ -1208,7 +1207,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_deviceManager,
|
||||
_transcodingJobHelper,
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource.Token)
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
|
||||
@@ -1227,7 +1226,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
|
||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
var released = false;
|
||||
var startTranscoding = false;
|
||||
|
||||
@@ -1323,24 +1322,28 @@ namespace Jellyfin.Api.Controllers
|
||||
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private double[] GetSegmentLengths(StreamState state)
|
||||
private static double[] GetSegmentLengths(StreamState state)
|
||||
=> GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength);
|
||||
|
||||
internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength)
|
||||
{
|
||||
var result = new List<double>();
|
||||
var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks;
|
||||
var wholeSegments = runtimeTicks / segmentLengthTicks;
|
||||
var remainingTicks = runtimeTicks % segmentLengthTicks;
|
||||
|
||||
var ticks = state.RunTimeTicks ?? 0;
|
||||
|
||||
var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks;
|
||||
|
||||
while (ticks > 0)
|
||||
var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1);
|
||||
var segments = new double[segmentsLen];
|
||||
for (int i = 0; i < wholeSegments; i++)
|
||||
{
|
||||
var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks;
|
||||
|
||||
result.Add(TimeSpan.FromTicks(length).TotalSeconds);
|
||||
|
||||
ticks -= length;
|
||||
segments[i] = segmentlength;
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
if (remainingTicks != 0)
|
||||
{
|
||||
segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds;
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber)
|
||||
@@ -1376,18 +1379,13 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var outputFmp4HeaderArg = string.Empty;
|
||||
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
if (isWindows)
|
||||
var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch
|
||||
{
|
||||
// on Windows, the path of fmp4 header file needs to be configured
|
||||
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"",
|
||||
// on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
|
||||
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
|
||||
}
|
||||
false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""
|
||||
};
|
||||
|
||||
segmentFormat = "fmp4" + outputFmp4HeaderArg;
|
||||
}
|
||||
@@ -1497,7 +1495,7 @@ namespace Jellyfin.Api.Controllers
|
||||
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
|
||||
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions);
|
||||
|
||||
return args;
|
||||
}
|
||||
@@ -1711,7 +1709,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext);
|
||||
return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath), false, HttpContext);
|
||||
}
|
||||
|
||||
private long GetEndPositionTicks(StreamState state, int requestedIndex)
|
||||
@@ -1762,9 +1760,9 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
|
||||
{
|
||||
var folder = Path.GetDirectoryName(playlist);
|
||||
var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist));
|
||||
|
||||
var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
|
||||
var filePrefix = Path.GetFileNameWithoutExtension(playlist);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -63,6 +63,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
|
||||
/// <response code="200">Genres returned.</response>
|
||||
@@ -84,6 +86,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
@@ -107,7 +111,8 @@ namespace Jellyfin.Api.Controllers
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
EnableTotalRecordCount = enableTotalRecordCount
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
||||
};
|
||||
|
||||
if (parentId.HasValue)
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return BadRequest("Invalid segment.");
|
||||
}
|
||||
|
||||
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
|
||||
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file), false, HttpContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -186,7 +186,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, HttpContext);
|
||||
return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path), false, HttpContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
var contentType = MimeTypes.GetMimeType(path);
|
||||
return File(System.IO.File.OpenRead(path), contentType);
|
||||
return File(AsyncFile.OpenRead(path), contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromQuery] int? index = null)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
|
||||
}
|
||||
@@ -106,7 +106,7 @@ namespace Jellyfin.Api.Controllers
|
||||
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
|
||||
if (user.ProfileImage != null)
|
||||
{
|
||||
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int index)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
|
||||
}
|
||||
@@ -153,7 +153,7 @@ namespace Jellyfin.Api.Controllers
|
||||
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
|
||||
if (user.ProfileImage != null)
|
||||
{
|
||||
@@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromQuery] int? index = null)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
|
||||
}
|
||||
@@ -234,7 +234,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int index)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
|
||||
}
|
||||
@@ -341,7 +341,7 @@ namespace Jellyfin.Api.Controllers
|
||||
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
@@ -377,7 +377,7 @@ namespace Jellyfin.Api.Controllers
|
||||
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
@@ -2007,7 +2007,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Response.Headers.Add(HeaderNames.CacheControl, "public");
|
||||
}
|
||||
|
||||
Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
|
||||
Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture));
|
||||
|
||||
// if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
|
||||
if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
|
||||
@@ -2026,7 +2026,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
return PhysicalFile(imagePath, imageContentType);
|
||||
return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace Jellyfin.Api.Controllers
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
@@ -116,7 +116,7 @@ namespace Jellyfin.Api.Controllers
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
@@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
@@ -187,7 +187,7 @@ namespace Jellyfin.Api.Controllers
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
@@ -223,43 +223,7 @@ namespace Jellyfin.Api.Controllers
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instant playlist based on a given genre.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("MusicGenres/{id}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
@@ -295,7 +259,7 @@ namespace Jellyfin.Api.Controllers
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
@@ -352,8 +316,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("MusicGenres/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Obsolete("Use GetInstantMixFromMusicGenres instead")]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById2(
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
|
||||
[FromQuery, Required] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
@@ -363,15 +326,15 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
return GetInstantMixFromMusicGenreById(
|
||||
id,
|
||||
userId,
|
||||
limit,
|
||||
fields,
|
||||
enableImages,
|
||||
enableUserData,
|
||||
imageTypeLimit,
|
||||
enableImageTypes);
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
|
||||
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -17,7 +14,6 @@ using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -237,48 +233,6 @@ namespace Jellyfin.Api.Controllers
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a remote image.
|
||||
/// </summary>
|
||||
/// <param name="imageUrl">The image url.</param>
|
||||
/// <param name="providerName">The provider name.</param>
|
||||
/// <response code="200">Remote image retrieved.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
|
||||
/// </returns>
|
||||
[HttpGet("Items/RemoteSearch/Image")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetRemoteSearchImage(
|
||||
[FromQuery, Required] string imageUrl,
|
||||
[FromQuery, Required] string providerName)
|
||||
{
|
||||
var urlHash = imageUrl.GetMD5();
|
||||
var pointerCachePath = GetFullCachePath(urlHash.ToString());
|
||||
|
||||
try
|
||||
{
|
||||
var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
if (System.IO.File.Exists(contentPath))
|
||||
{
|
||||
return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath));
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// Means the file isn't cached yet
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Means the file isn't cached yet
|
||||
}
|
||||
|
||||
await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
|
||||
var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies search criteria to an item and refreshes metadata.
|
||||
/// </summary>
|
||||
@@ -320,54 +274,5 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the image.
|
||||
/// </summary>
|
||||
/// <param name="providerName">Name of the provider.</param>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="urlHash">The URL hash.</param>
|
||||
/// <param name="pointerCachePath">The pointer cache path.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
|
||||
{
|
||||
using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
|
||||
if (result.Content.Headers.ContentType?.MediaType == null)
|
||||
{
|
||||
throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType));
|
||||
}
|
||||
|
||||
var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
|
||||
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
|
||||
|
||||
var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
||||
Directory.CreateDirectory(directory);
|
||||
using (var stream = result.Content)
|
||||
{
|
||||
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||
await using var fileStream = new FileStream(
|
||||
fullCachePath,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.None,
|
||||
IODefaults.FileStreamBufferSize,
|
||||
true);
|
||||
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
||||
|
||||
Directory.CreateDirectory(pointerCacheDirectory);
|
||||
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full cache path.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetFullCachePath(string filename)
|
||||
=> Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,11 +154,11 @@ namespace Jellyfin.Api.Controllers
|
||||
};
|
||||
|
||||
if (!item.IsVirtualItem
|
||||
&& !(item is ICollectionFolder)
|
||||
&& !(item is UserView)
|
||||
&& !(item is AggregateFolder)
|
||||
&& !(item is LiveTvChannel)
|
||||
&& !(item is IItemByName)
|
||||
&& item is not ICollectionFolder
|
||||
&& item is not UserView
|
||||
&& item is not AggregateFolder
|
||||
&& item is not LiveTvChannel
|
||||
&& item is not IItemByName
|
||||
&& item.SourceType == SourceType.Library)
|
||||
{
|
||||
var inheritedContentType = _libraryManager.GetInheritedContentType(item);
|
||||
@@ -263,8 +263,8 @@ namespace Jellyfin.Api.Controllers
|
||||
item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
|
||||
}
|
||||
|
||||
item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : (DateTime?)null;
|
||||
item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : (DateTime?)null;
|
||||
item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
|
||||
item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
|
||||
item.ProductionYear = request.ProductionYear;
|
||||
item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
|
||||
item.CustomRating = request.CustomRating;
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Items")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetItems(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] Guid userId,
|
||||
[FromQuery] string? maxOfficialRating,
|
||||
[FromQuery] bool? hasThemeSong,
|
||||
[FromQuery] bool? hasThemeVideo,
|
||||
@@ -224,8 +224,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
var user = !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
@@ -241,7 +241,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var item = _libraryManager.GetParentItem(parentId, userId);
|
||||
QueryResult<BaseItem> result;
|
||||
|
||||
if (!(item is Folder folder))
|
||||
if (item is not Folder folder)
|
||||
{
|
||||
folder = _libraryManager.GetUserRootFolder();
|
||||
}
|
||||
@@ -285,9 +285,9 @@ namespace Jellyfin.Api.Controllers
|
||||
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
|
||||
}
|
||||
|
||||
if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder))
|
||||
if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder)
|
||||
{
|
||||
var query = new InternalItemsQuery(user!)
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
IsPlayed = isPlayed,
|
||||
MediaTypes = mediaTypes,
|
||||
|
||||
@@ -331,10 +331,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public ActionResult DeleteItem(Guid itemId)
|
||||
public async Task<ActionResult> DeleteItem(Guid itemId)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
var auth = _authContext.GetAuthorizationInfo(Request);
|
||||
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
var user = auth.User;
|
||||
|
||||
if (!item.CanDelete(user))
|
||||
@@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
||||
public async Task<ActionResult> DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
||||
{
|
||||
if (ids.Length == 0)
|
||||
{
|
||||
@@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
|
||||
foreach (var i in ids)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(i);
|
||||
var auth = _authContext.GetAuthorizationInfo(Request);
|
||||
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
var user = auth.User;
|
||||
|
||||
if (!item.CanDelete(user))
|
||||
@@ -600,7 +600,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
foreach (var item in dto.Updates)
|
||||
{
|
||||
_libraryMonitor.ReportFileSystemChanged(item.Path);
|
||||
_libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null."));
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
@@ -627,7 +627,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var auth = _authContext.GetAuthorizationInfo(Request);
|
||||
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
|
||||
var user = auth.User;
|
||||
|
||||
@@ -700,7 +700,7 @@ namespace Jellyfin.Api.Controllers
|
||||
: _libraryManager.RootFolder)
|
||||
: _libraryManager.GetItemById(itemId);
|
||||
|
||||
if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
|
||||
if (item is Episode || (item is IItemByName && item is not MusicArtist))
|
||||
{
|
||||
return new QueryResult<BaseItemDto>();
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (paths != null && paths.Length > 0)
|
||||
{
|
||||
libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
|
||||
libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
|
||||
}
|
||||
|
||||
await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
|
||||
@@ -212,7 +212,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo { Path = mediaPathDto.Path };
|
||||
var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null."));
|
||||
|
||||
_libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
@@ -429,10 +428,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Tuners/{tunerId}/Reset")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public ActionResult ResetTuner([FromRoute, Required] string tunerId)
|
||||
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
|
||||
{
|
||||
AssertUserCanManageLiveTv();
|
||||
_liveTvManager.ResetTuner(tunerId, CancellationToken.None);
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -761,9 +760,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
|
||||
public async Task<ActionResult> DeleteRecording([FromRoute, Required] Guid recordingId)
|
||||
{
|
||||
AssertUserCanManageLiveTv();
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
|
||||
var item = _libraryManager.GetItemById(recordingId);
|
||||
if (item == null)
|
||||
@@ -790,7 +789,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
|
||||
{
|
||||
AssertUserCanManageLiveTv();
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -808,7 +807,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
|
||||
{
|
||||
AssertUserCanManageLiveTv();
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -824,7 +823,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
|
||||
{
|
||||
AssertUserCanManageLiveTv();
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -882,7 +881,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
|
||||
{
|
||||
AssertUserCanManageLiveTv();
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -900,7 +899,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
||||
{
|
||||
AssertUserCanManageLiveTv();
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -916,7 +915,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
||||
{
|
||||
AssertUserCanManageLiveTv();
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -1172,7 +1171,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetLiveRecordingFile([FromRoute, Required] string recordingId)
|
||||
public ActionResult GetLiveRecordingFile([FromRoute, Required] string recordingId)
|
||||
{
|
||||
var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
|
||||
|
||||
@@ -1181,11 +1180,8 @@ namespace Jellyfin.Api.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
await new ProgressiveFileCopier(path, null, _transcodingJobHelper, CancellationToken.None)
|
||||
.WriteToAsync(memoryStream, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return File(memoryStream, MimeTypes.GetMimeType(path));
|
||||
var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper);
|
||||
return new FileStreamResult(stream, MimeTypes.GetMimeType(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1203,21 +1199,21 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
|
||||
public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
|
||||
{
|
||||
var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false);
|
||||
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
|
||||
if (liveStreamInfo == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
|
||||
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
|
||||
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
|
||||
}
|
||||
|
||||
private void AssertUserCanManageLiveTv()
|
||||
private async Task AssertUserCanManageLiveTv()
|
||||
{
|
||||
var user = _sessionContext.GetUser(Request);
|
||||
var user = await _sessionContext.GetUser(Request).ConfigureAwait(false);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery, ParameterObsolete] bool? allowAudioStreamCopy,
|
||||
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
|
||||
{
|
||||
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
||||
var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
|
||||
var profile = playbackInfoDto?.DeviceProfile;
|
||||
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
|
||||
@@ -161,6 +161,11 @@ namespace Jellyfin.Api.Controllers
|
||||
liveStreamId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (info.ErrorCode != null)
|
||||
{
|
||||
return info;
|
||||
}
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
// set device specific data
|
||||
@@ -179,7 +184,7 @@ namespace Jellyfin.Api.Controllers
|
||||
audioStreamIndex,
|
||||
subtitleStreamIndex,
|
||||
maxAudioChannels,
|
||||
info!.PlaySessionId!,
|
||||
info.PlaySessionId!,
|
||||
userId ?? Guid.Empty,
|
||||
enableDirectPlay.Value,
|
||||
enableDirectStream.Value,
|
||||
@@ -302,31 +307,16 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </summary>
|
||||
/// <param name="size">The bitrate. Defaults to 102400.</param>
|
||||
/// <response code="200">Test buffer returned.</response>
|
||||
/// <response code="400">Size has to be a numer between 0 and 10,000,000.</response>
|
||||
/// <returns>A <see cref="FileResult"/> with specified bitrate.</returns>
|
||||
[HttpGet("Playback/BitrateTest")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesFile(MediaTypeNames.Application.Octet)]
|
||||
public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400)
|
||||
public ActionResult GetBitrateTestBytes([FromQuery][Range(1, 100_000_000, ErrorMessage = "The requested size must be greater than or equal to {1} and less than or equal to {2}")] int size = 102400)
|
||||
{
|
||||
const int MaxSize = 10_000_000;
|
||||
|
||||
if (size <= 0)
|
||||
{
|
||||
return BadRequest($"The requested size ({size}) is equal to or smaller than 0.");
|
||||
}
|
||||
|
||||
if (size > MaxSize)
|
||||
{
|
||||
return BadRequest($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).");
|
||||
}
|
||||
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
|
||||
try
|
||||
{
|
||||
new Random().NextBytes(buffer);
|
||||
Random.Shared.NextBytes(buffer);
|
||||
return File(buffer, MediaTypeNames.Application.Octet);
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -300,9 +300,8 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
|
||||
{
|
||||
var people = _libraryManager.GetPeople(new InternalPeopleQuery
|
||||
var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director })
|
||||
{
|
||||
ExcludePersonTypes = new[] { PersonType.Director },
|
||||
MaxListOrder = 3
|
||||
});
|
||||
|
||||
@@ -316,10 +315,9 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
|
||||
{
|
||||
var people = _libraryManager.GetPeople(new InternalPeopleQuery
|
||||
{
|
||||
PersonTypes = new[] { PersonType.Director }
|
||||
});
|
||||
var people = _libraryManager.GetPeople(new InternalPeopleQuery(
|
||||
new[] { PersonType.Director },
|
||||
Array.Empty<string>()));
|
||||
|
||||
var itemIds = items.Select(i => i.Id).ToList();
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
|
||||
/// <response code="200">Music genres returned.</response>
|
||||
@@ -84,6 +86,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
@@ -107,7 +111,8 @@ namespace Jellyfin.Api.Controllers
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
EnableTotalRecordCount = enableTotalRecordCount
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
||||
};
|
||||
|
||||
if (parentId.HasValue)
|
||||
|
||||
@@ -94,10 +94,10 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
|
||||
var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
|
||||
var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery(
|
||||
personTypes,
|
||||
excludePersonTypes)
|
||||
{
|
||||
PersonTypes = personTypes,
|
||||
ExcludePersonTypes = excludePersonTypes,
|
||||
NameContains = searchTerm,
|
||||
User = user,
|
||||
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
|
||||
|
||||
@@ -72,13 +72,13 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
|
||||
[HttpPost("Users/{userId}/PlayedItems/{itemId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<UserItemDataDto> MarkPlayedItem(
|
||||
public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
|
||||
var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
var dto = UpdatePlayedStatus(user, itemId, true, datePlayed);
|
||||
foreach (var additionalUserInfo in session.AdditionalUsers)
|
||||
{
|
||||
@@ -98,10 +98,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
|
||||
[HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
|
||||
public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
|
||||
var session = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
var dto = UpdatePlayedStatus(user, itemId, false, null);
|
||||
foreach (var additionalUserInfo in session.AdditionalUsers)
|
||||
{
|
||||
@@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
|
||||
public async Task<ActionResult> ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo)
|
||||
{
|
||||
playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
|
||||
playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers
|
||||
public async Task<ActionResult> ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo)
|
||||
{
|
||||
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
|
||||
playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -171,10 +171,11 @@ namespace Jellyfin.Api.Controllers
|
||||
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
|
||||
{
|
||||
await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
|
||||
var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -220,7 +221,7 @@ namespace Jellyfin.Api.Controllers
|
||||
};
|
||||
|
||||
playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
|
||||
playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -278,7 +279,7 @@ namespace Jellyfin.Api.Controllers
|
||||
};
|
||||
|
||||
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
|
||||
playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -320,10 +321,11 @@ namespace Jellyfin.Api.Controllers
|
||||
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
|
||||
{
|
||||
await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
|
||||
var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.PluginDtos;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Model.Net;
|
||||
@@ -207,12 +207,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId));
|
||||
|
||||
// Select the un-instanced one first.
|
||||
var plugin = plugins.FirstOrDefault(p => p.Instance == null);
|
||||
if (plugin == null)
|
||||
{
|
||||
// Then by the status.
|
||||
plugin = plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
|
||||
}
|
||||
var plugin = plugins.FirstOrDefault(p => p.Instance == null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
|
||||
|
||||
if (plugin != null)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.QuickConnect;
|
||||
using MediaBrowser.Model.QuickConnect;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -16,27 +19,29 @@ namespace Jellyfin.Api.Controllers
|
||||
public class QuickConnectController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IQuickConnect _quickConnect;
|
||||
private readonly IAuthorizationContext _authContext;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="QuickConnectController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
|
||||
public QuickConnectController(IQuickConnect quickConnect)
|
||||
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||
public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext)
|
||||
{
|
||||
_quickConnect = quickConnect;
|
||||
_authContext = authContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current quick connect state.
|
||||
/// </summary>
|
||||
/// <response code="200">Quick connect state returned.</response>
|
||||
/// <returns>The current <see cref="QuickConnectState"/>.</returns>
|
||||
[HttpGet("Status")]
|
||||
/// <returns>Whether Quick Connect is enabled on the server or not.</returns>
|
||||
[HttpGet("Enabled")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QuickConnectState> GetStatus()
|
||||
public ActionResult<bool> GetEnabled()
|
||||
{
|
||||
_quickConnect.ExpireRequests();
|
||||
return _quickConnect.State;
|
||||
return _quickConnect.IsEnabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -47,9 +52,17 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
|
||||
[HttpGet("Initiate")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QuickConnectResult> Initiate()
|
||||
public async Task<ActionResult<QuickConnectResult>> Initiate()
|
||||
{
|
||||
return _quickConnect.TryConnect();
|
||||
try
|
||||
{
|
||||
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
return _quickConnect.TryConnect(auth);
|
||||
}
|
||||
catch (AuthenticationException)
|
||||
{
|
||||
return Unauthorized("Quick connect is disabled");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -72,42 +85,10 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
return NotFound("Unknown secret");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily activates quick connect for five minutes.
|
||||
/// </summary>
|
||||
/// <response code="204">Quick connect has been temporarily activated.</response>
|
||||
/// <response code="403">Quick connect is unavailable on this server.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
|
||||
[HttpPost("Activate")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult Activate()
|
||||
{
|
||||
if (_quickConnect.State == QuickConnectState.Unavailable)
|
||||
catch (AuthenticationException)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Quick connect is unavailable");
|
||||
return Unauthorized("Quick connect is disabled");
|
||||
}
|
||||
|
||||
_quickConnect.Activate();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables quick connect.
|
||||
/// </summary>
|
||||
/// <param name="status">New <see cref="QuickConnectState"/>.</param>
|
||||
/// <response code="204">Quick connect state set successfully.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
|
||||
[HttpPost("Available")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available)
|
||||
{
|
||||
_quickConnect.SetState(status);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -121,7 +102,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult<bool> Authorize([FromQuery, Required] string code)
|
||||
public async Task<ActionResult<bool>> Authorize([FromQuery, Required] string code)
|
||||
{
|
||||
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
|
||||
if (!userId.HasValue)
|
||||
@@ -129,26 +110,14 @@ namespace Jellyfin.Api.Controllers
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id");
|
||||
}
|
||||
|
||||
return _quickConnect.AuthorizeRequest(userId.Value, code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deauthorize all quick connect devices for the current user.
|
||||
/// </summary>
|
||||
/// <response code="200">All quick connect devices were deleted.</response>
|
||||
/// <returns>The number of devices that were deleted.</returns>
|
||||
[HttpPost("Deauthorize")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<int> Deauthorize()
|
||||
{
|
||||
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
|
||||
if (!userId.HasValue)
|
||||
try
|
||||
{
|
||||
return 0;
|
||||
return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false);
|
||||
}
|
||||
catch (AuthenticationException)
|
||||
{
|
||||
return Unauthorized("Quick connect is disabled");
|
||||
}
|
||||
|
||||
return _quickConnect.DeleteAllDevices(userId.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
@@ -16,7 +15,6 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -145,58 +143,6 @@ namespace Jellyfin.Api.Controllers
|
||||
return Ok(_providerManager.GetRemoteImageProviderInfo(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a remote image.
|
||||
/// </summary>
|
||||
/// <param name="imageUrl">The image url.</param>
|
||||
/// <response code="200">Remote image returned.</response>
|
||||
/// <response code="404">Remote image not found.</response>
|
||||
/// <returns>Image Stream.</returns>
|
||||
[HttpGet("Images/Remote")]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl)
|
||||
{
|
||||
var urlHash = imageUrl.ToString().GetMD5();
|
||||
var pointerCachePath = GetFullCachePath(urlHash.ToString());
|
||||
|
||||
string? contentPath = null;
|
||||
var hasFile = false;
|
||||
|
||||
try
|
||||
{
|
||||
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
if (System.IO.File.Exists(contentPath))
|
||||
{
|
||||
hasFile = true;
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// The file isn't cached yet
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// The file isn't cached yet
|
||||
}
|
||||
|
||||
if (!hasFile)
|
||||
{
|
||||
await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
|
||||
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(contentPath))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var contentType = MimeTypes.GetMimeType(contentPath);
|
||||
return PhysicalFile(contentPath, contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a remote image for an item.
|
||||
/// </summary>
|
||||
@@ -237,36 +183,5 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the image.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="urlHash">The URL hash.</param>
|
||||
/// <param name="pointerCachePath">The pointer cache path.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task DownloadImage(Uri url, Guid urlHash, string pointerCachePath)
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
|
||||
if (response.Content.Headers.ContentType?.MediaType == null)
|
||||
{
|
||||
throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType));
|
||||
}
|
||||
|
||||
var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1];
|
||||
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
|
||||
|
||||
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
||||
Directory.CreateDirectory(fullCacheDirectory);
|
||||
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
|
||||
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
|
||||
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
||||
Directory.CreateDirectory(pointerCacheDirectory);
|
||||
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,10 +228,7 @@ namespace Jellyfin.Api.Controllers
|
||||
itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
|
||||
}
|
||||
|
||||
if (itemWithImage == null)
|
||||
{
|
||||
itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Thumb);
|
||||
}
|
||||
itemWithImage ??= GetParentWithImage<BaseItem>(item, ImageType.Thumb);
|
||||
|
||||
if (itemWithImage != null)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
@@ -124,7 +125,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Sessions/{sessionId}/Viewing")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DisplayContent(
|
||||
public async Task<ActionResult> DisplayContent(
|
||||
[FromRoute, Required] string sessionId,
|
||||
[FromQuery, Required] string itemType,
|
||||
[FromQuery, Required] string itemId,
|
||||
@@ -137,11 +138,12 @@ namespace Jellyfin.Api.Controllers
|
||||
ItemType = itemType
|
||||
};
|
||||
|
||||
_sessionManager.SendBrowseCommand(
|
||||
RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
|
||||
await _sessionManager.SendBrowseCommand(
|
||||
await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
|
||||
sessionId,
|
||||
command,
|
||||
CancellationToken.None);
|
||||
CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -162,7 +164,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Sessions/{sessionId}/Playing")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult Play(
|
||||
public async Task<ActionResult> Play(
|
||||
[FromRoute, Required] string sessionId,
|
||||
[FromQuery, Required] PlayCommand playCommand,
|
||||
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
|
||||
@@ -183,11 +185,12 @@ namespace Jellyfin.Api.Controllers
|
||||
StartIndex = startIndex
|
||||
};
|
||||
|
||||
_sessionManager.SendPlayCommand(
|
||||
RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
|
||||
await _sessionManager.SendPlayCommand(
|
||||
await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
|
||||
sessionId,
|
||||
playRequest,
|
||||
CancellationToken.None);
|
||||
CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -204,14 +207,14 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Sessions/{sessionId}/Playing/{command}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SendPlaystateCommand(
|
||||
public async Task<ActionResult> SendPlaystateCommand(
|
||||
[FromRoute, Required] string sessionId,
|
||||
[FromRoute, Required] PlaystateCommand command,
|
||||
[FromQuery] long? seekPositionTicks,
|
||||
[FromQuery] string? controllingUserId)
|
||||
{
|
||||
_sessionManager.SendPlaystateCommand(
|
||||
RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
|
||||
await _sessionManager.SendPlaystateCommand(
|
||||
await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
|
||||
sessionId,
|
||||
new PlaystateRequest()
|
||||
{
|
||||
@@ -219,7 +222,8 @@ namespace Jellyfin.Api.Controllers
|
||||
ControllingUserId = controllingUserId,
|
||||
SeekPositionTicks = seekPositionTicks,
|
||||
},
|
||||
CancellationToken.None);
|
||||
CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -234,18 +238,18 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Sessions/{sessionId}/System/{command}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SendSystemCommand(
|
||||
public async Task<ActionResult> SendSystemCommand(
|
||||
[FromRoute, Required] string sessionId,
|
||||
[FromRoute, Required] GeneralCommandType command)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
var generalCommand = new GeneralCommand
|
||||
{
|
||||
Name = command,
|
||||
ControllingUserId = currentSession.UserId
|
||||
};
|
||||
|
||||
_sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
|
||||
await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -260,11 +264,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Sessions/{sessionId}/Command/{command}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SendGeneralCommand(
|
||||
public async Task<ActionResult> SendGeneralCommand(
|
||||
[FromRoute, Required] string sessionId,
|
||||
[FromRoute, Required] GeneralCommandType command)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
|
||||
var generalCommand = new GeneralCommand
|
||||
{
|
||||
@@ -272,7 +276,8 @@ namespace Jellyfin.Api.Controllers
|
||||
ControllingUserId = currentSession.UserId
|
||||
};
|
||||
|
||||
_sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None);
|
||||
await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -287,11 +292,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Sessions/{sessionId}/Command")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SendFullGeneralCommand(
|
||||
public async Task<ActionResult> SendFullGeneralCommand(
|
||||
[FromRoute, Required] string sessionId,
|
||||
[FromBody, Required] GeneralCommand command)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authContext, Request)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (command == null)
|
||||
{
|
||||
@@ -300,11 +306,12 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
command.ControllingUserId = currentSession.UserId;
|
||||
|
||||
_sessionManager.SendGeneralCommand(
|
||||
await _sessionManager.SendGeneralCommand(
|
||||
currentSession.Id,
|
||||
sessionId,
|
||||
command,
|
||||
CancellationToken.None);
|
||||
CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -319,7 +326,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Sessions/{sessionId}/Message")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SendMessageCommand(
|
||||
public async Task<ActionResult> SendMessageCommand(
|
||||
[FromRoute, Required] string sessionId,
|
||||
[FromBody, Required] MessageCommand command)
|
||||
{
|
||||
@@ -328,7 +335,12 @@ namespace Jellyfin.Api.Controllers
|
||||
command.Header = "Message from Server";
|
||||
}
|
||||
|
||||
_sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None);
|
||||
await _sessionManager.SendMessageCommand(
|
||||
await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false),
|
||||
sessionId,
|
||||
command,
|
||||
CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -383,7 +395,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Sessions/Capabilities")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult PostCapabilities(
|
||||
public async Task<ActionResult> PostCapabilities(
|
||||
[FromQuery] string? id,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
|
||||
@@ -393,7 +405,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_sessionManager.ReportCapabilities(id, new ClientCapabilities
|
||||
@@ -417,13 +429,13 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Sessions/Capabilities/Full")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult PostFullCapabilities(
|
||||
public async Task<ActionResult> PostFullCapabilities(
|
||||
[FromQuery] string? id,
|
||||
[FromBody, Required] ClientCapabilitiesDto capabilities)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
id = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
|
||||
@@ -441,11 +453,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Sessions/Viewing")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult ReportViewing(
|
||||
public async Task<ActionResult> ReportViewing(
|
||||
[FromQuery] string? sessionId,
|
||||
[FromQuery, Required] string? itemId)
|
||||
{
|
||||
string session = sessionId ?? RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
|
||||
|
||||
_sessionManager.ReportNowViewingItem(session, itemId);
|
||||
return NoContent();
|
||||
@@ -459,11 +471,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Sessions/Logout")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult ReportSessionEnded()
|
||||
public async Task<ActionResult> ReportSessionEnded()
|
||||
{
|
||||
AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request);
|
||||
AuthorizationInfo auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
|
||||
_sessionManager.Logout(auth.Token);
|
||||
await _sessionManager.Logout(auth.Token).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||
|
||||
return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false);
|
||||
return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="startPositionTicks">The start position of the subtitle in ticks.</param>
|
||||
/// <response code="200">File returned.</response>
|
||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||
[HttpGet("Videos/{routeItemId}/routeMediaSourceId/Subtitles/{routeIndex}/Stream.{routeFormat}")]
|
||||
[HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile("text/*")]
|
||||
public async Task<ActionResult> GetSubtitle(
|
||||
@@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
long positionTicks = 0;
|
||||
|
||||
var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
|
||||
var accessToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
|
||||
|
||||
while (positionTicks < runtime)
|
||||
{
|
||||
@@ -376,7 +376,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
|
||||
|
||||
var url = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
CultureInfo.InvariantCulture,
|
||||
"stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
|
||||
positionTicks.ToString(CultureInfo.InvariantCulture),
|
||||
endPositionTicks.ToString(CultureInfo.InvariantCulture),
|
||||
@@ -417,6 +417,8 @@ namespace Jellyfin.Api.Controllers
|
||||
IsForced = body.IsForced,
|
||||
Stream = memoryStream
|
||||
}).ConfigureAwait(false);
|
||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.SyncPlayDtos;
|
||||
@@ -51,10 +52,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("New")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayCreateGroup)]
|
||||
public ActionResult SyncPlayCreateGroup(
|
||||
public async Task<ActionResult> SyncPlayCreateGroup(
|
||||
[FromBody, Required] NewGroupRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
|
||||
_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -69,10 +70,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Join")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
||||
public ActionResult SyncPlayJoinGroup(
|
||||
public async Task<ActionResult> SyncPlayJoinGroup(
|
||||
[FromBody, Required] JoinGroupRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new JoinGroupRequest(requestData.GroupId);
|
||||
_syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -86,9 +87,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Leave")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayLeaveGroup()
|
||||
public async Task<ActionResult> SyncPlayLeaveGroup()
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new LeaveGroupRequest();
|
||||
_syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -102,9 +103,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("List")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
||||
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
|
||||
public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups()
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new ListGroupsRequest();
|
||||
return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest));
|
||||
}
|
||||
@@ -118,10 +119,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("SetNewQueue")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySetNewQueue(
|
||||
public async Task<ActionResult> SyncPlaySetNewQueue(
|
||||
[FromBody, Required] PlayRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new PlayGroupRequest(
|
||||
requestData.PlayingQueue,
|
||||
requestData.PlayingItemPosition,
|
||||
@@ -139,10 +140,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("SetPlaylistItem")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySetPlaylistItem(
|
||||
public async Task<ActionResult> SyncPlaySetPlaylistItem(
|
||||
[FromBody, Required] SetPlaylistItemRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId);
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -157,11 +158,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("RemoveFromPlaylist")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayRemoveFromPlaylist(
|
||||
public async Task<ActionResult> SyncPlayRemoveFromPlaylist(
|
||||
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem);
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -175,10 +176,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("MovePlaylistItem")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayMovePlaylistItem(
|
||||
public async Task<ActionResult> SyncPlayMovePlaylistItem(
|
||||
[FromBody, Required] MovePlaylistItemRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex);
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -193,10 +194,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Queue")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayQueue(
|
||||
public async Task<ActionResult> SyncPlayQueue(
|
||||
[FromBody, Required] QueueRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode);
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -210,9 +211,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Unpause")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayUnpause()
|
||||
public async Task<ActionResult> SyncPlayUnpause()
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new UnpauseGroupRequest();
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -226,9 +227,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Pause")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayPause()
|
||||
public async Task<ActionResult> SyncPlayPause()
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new PauseGroupRequest();
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -242,9 +243,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Stop")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayStop()
|
||||
public async Task<ActionResult> SyncPlayStop()
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new StopGroupRequest();
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -259,10 +260,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Seek")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySeek(
|
||||
public async Task<ActionResult> SyncPlaySeek(
|
||||
[FromBody, Required] SeekRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks);
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -277,10 +278,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Buffering")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayBuffering(
|
||||
public async Task<ActionResult> SyncPlayBuffering(
|
||||
[FromBody, Required] BufferRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new BufferGroupRequest(
|
||||
requestData.When,
|
||||
requestData.PositionTicks,
|
||||
@@ -299,10 +300,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("Ready")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayReady(
|
||||
public async Task<ActionResult> SyncPlayReady(
|
||||
[FromBody, Required] ReadyRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new ReadyGroupRequest(
|
||||
requestData.When,
|
||||
requestData.PositionTicks,
|
||||
@@ -321,10 +322,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("SetIgnoreWait")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySetIgnoreWait(
|
||||
public async Task<ActionResult> SyncPlaySetIgnoreWait(
|
||||
[FromBody, Required] IgnoreWaitRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait);
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -339,10 +340,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("NextItem")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayNextItem(
|
||||
public async Task<ActionResult> SyncPlayNextItem(
|
||||
[FromBody, Required] NextItemRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId);
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -357,10 +358,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("PreviousItem")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlayPreviousItem(
|
||||
public async Task<ActionResult> SyncPlayPreviousItem(
|
||||
[FromBody, Required] PreviousItemRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId);
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -375,10 +376,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("SetRepeatMode")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySetRepeatMode(
|
||||
public async Task<ActionResult> SyncPlaySetRepeatMode(
|
||||
[FromBody, Required] SetRepeatModeRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode);
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -393,10 +394,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpPost("SetShuffleMode")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||
public ActionResult SyncPlaySetShuffleMode(
|
||||
public async Task<ActionResult> SyncPlaySetShuffleMode(
|
||||
[FromBody, Required] SetShuffleModeRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode);
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
@@ -410,10 +411,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("Ping")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SyncPlayPing(
|
||||
public async Task<ActionResult> SyncPlayPing(
|
||||
[FromBody, Required] PingRequestDto requestData)
|
||||
{
|
||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request).ConfigureAwait(false);
|
||||
var syncPlayRequest = new PingGroupRequest(requestData.Ping);
|
||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||
return NoContent();
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<SystemInfo> GetSystemInfo()
|
||||
{
|
||||
return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
|
||||
return _appHost.GetSystemInfo(Request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
||||
{
|
||||
return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
|
||||
return _appHost.GetPublicSystemInfo(Request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
// For older files, assume fully static
|
||||
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
|
||||
FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare);
|
||||
FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
return File(stream, "text/plain; charset=utf-8");
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<UtcTimeResponse> GetUtcTime()
|
||||
{
|
||||
// Important to keep the following line at the beginning
|
||||
var requestReceptionTime = DateTime.UtcNow.ToUniversalTime();
|
||||
var requestReceptionTime = DateTime.UtcNow;
|
||||
|
||||
// Important to keep the following line at the end
|
||||
var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime();
|
||||
var responseTransmissionTime = DateTime.UtcNow;
|
||||
|
||||
// Implementing NTP on such a high level results in this useless
|
||||
// information being sent. On the other hand it enables future additions.
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetTrailers(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] Guid userId,
|
||||
[FromQuery] string? maxOfficialRating,
|
||||
[FromQuery] bool? hasThemeSong,
|
||||
[FromQuery] bool? hasThemeVideo,
|
||||
|
||||
@@ -6,7 +6,7 @@ using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
@@ -65,6 +65,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
|
||||
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
|
||||
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
|
||||
@@ -81,12 +82,13 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] DateTime? nextUpDateCutoff,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool disableFirstEpisode = false)
|
||||
{
|
||||
var options = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
.AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var result = _tvSeriesManager.GetNextUp(
|
||||
new NextUpQuery
|
||||
@@ -97,7 +99,8 @@ namespace Jellyfin.Api.Controllers
|
||||
StartIndex = startIndex,
|
||||
UserId = userId ?? Guid.Empty,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
DisableFirstEpisode = disableFirstEpisode
|
||||
DisableFirstEpisode = disableFirstEpisode,
|
||||
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue
|
||||
},
|
||||
options);
|
||||
|
||||
@@ -144,13 +147,13 @@ namespace Jellyfin.Api.Controllers
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
|
||||
var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
|
||||
var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1);
|
||||
|
||||
var parentIdGuid = parentId ?? Guid.Empty;
|
||||
|
||||
var options = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
.AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
{
|
||||
@@ -220,12 +223,12 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
|
||||
{
|
||||
var item = _libraryManager.GetItemById(seasonId.Value);
|
||||
if (!(item is Season seasonItem))
|
||||
if (item is not Season seasonItem)
|
||||
{
|
||||
return NotFound("No season exists with Id " + seasonId);
|
||||
}
|
||||
@@ -234,7 +237,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
else if (season.HasValue) // Season number was supplied. Get episodes by season number
|
||||
{
|
||||
if (!(_libraryManager.GetItemById(seriesId) is Series series))
|
||||
if (_libraryManager.GetItemById(seriesId) is not Series series)
|
||||
{
|
||||
return NotFound("Series not found");
|
||||
}
|
||||
@@ -249,7 +252,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
else // No season number or season id was supplied. Returning all episodes.
|
||||
{
|
||||
if (!(_libraryManager.GetItemById(seriesId) is Series series))
|
||||
if (_libraryManager.GetItemById(seriesId) is not Series series)
|
||||
{
|
||||
return NotFound("Series not found");
|
||||
}
|
||||
@@ -333,7 +336,7 @@ namespace Jellyfin.Api.Controllers
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
|
||||
if (!(_libraryManager.GetItemById(seriesId) is Series series))
|
||||
if (_libraryManager.GetItemById(seriesId) is not Series series)
|
||||
{
|
||||
return NotFound("Series not found");
|
||||
}
|
||||
@@ -347,7 +350,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
|
||||
|
||||
|
||||
@@ -116,9 +116,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool enableRedirection = true)
|
||||
{
|
||||
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
||||
_authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
|
||||
(await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId;
|
||||
|
||||
var authInfo = _authorizationContext.GetAuthorizationInfo(Request);
|
||||
var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers
|
||||
null,
|
||||
null,
|
||||
maxAudioChannels,
|
||||
info!.PlaySessionId!,
|
||||
info.PlaySessionId!,
|
||||
userId ?? Guid.Empty,
|
||||
true,
|
||||
true,
|
||||
@@ -298,9 +298,9 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
Type = DlnaProfileType.Audio,
|
||||
Context = EncodingContext.Streaming,
|
||||
Container = transcodingContainer,
|
||||
AudioCodec = audioCodec,
|
||||
Protocol = transcodingProtocol,
|
||||
Container = transcodingContainer ?? "mp3",
|
||||
AudioCodec = audioCodec ?? "mp3",
|
||||
Protocol = transcodingProtocol ?? "http",
|
||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||
MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.QuickConnect;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
@@ -38,6 +39,7 @@ namespace Jellyfin.Api.Controllers
|
||||
private readonly IAuthorizationContext _authContext;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IQuickConnect _quickConnectManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserController"/> class.
|
||||
@@ -49,6 +51,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
/// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param>
|
||||
public UserController(
|
||||
IUserManager userManager,
|
||||
ISessionManager sessionManager,
|
||||
@@ -56,7 +59,8 @@ namespace Jellyfin.Api.Controllers
|
||||
IDeviceManager deviceManager,
|
||||
IAuthorizationContext authContext,
|
||||
IServerConfigurationManager config,
|
||||
ILogger<UserController> logger)
|
||||
ILogger<UserController> logger,
|
||||
IQuickConnect quickConnectManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_sessionManager = sessionManager;
|
||||
@@ -65,6 +69,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_authContext = authContext;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
_quickConnectManager = quickConnectManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,11 +82,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<UserDto>> GetUsers(
|
||||
public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers(
|
||||
[FromQuery] bool? isHidden,
|
||||
[FromQuery] bool? isDisabled)
|
||||
{
|
||||
var users = Get(isHidden, isDisabled, false, false);
|
||||
var users = await Get(isHidden, isDisabled, false, false).ConfigureAwait(false);
|
||||
return Ok(users);
|
||||
}
|
||||
|
||||
@@ -92,15 +97,15 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns>
|
||||
[HttpGet("Public")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<UserDto>> GetPublicUsers()
|
||||
public async Task<ActionResult<IEnumerable<UserDto>>> GetPublicUsers()
|
||||
{
|
||||
// If the startup wizard hasn't been completed then just return all users
|
||||
if (!_config.Configuration.IsStartupWizardCompleted)
|
||||
{
|
||||
return Ok(Get(false, false, false, false));
|
||||
return Ok(await Get(false, false, false, false).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
return Ok(Get(false, false, true, true));
|
||||
return Ok(await Get(false, false, true, true).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -141,7 +146,7 @@ namespace Jellyfin.Api.Controllers
|
||||
public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
_sessionManager.RevokeUserTokens(user.Id, null);
|
||||
await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
|
||||
await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -195,7 +200,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
|
||||
{
|
||||
var auth = _authContext.GetAuthorizationInfo(Request);
|
||||
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -228,23 +233,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
|
||||
[HttpPost("AuthenticateWithQuickConnect")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<AuthenticationResult>> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
|
||||
public ActionResult<AuthenticationResult> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
|
||||
{
|
||||
var auth = _authContext.GetAuthorizationInfo(Request);
|
||||
|
||||
try
|
||||
{
|
||||
var authRequest = new AuthenticationRequest
|
||||
{
|
||||
App = auth.Client,
|
||||
AppVersion = auth.Version,
|
||||
DeviceId = auth.DeviceId,
|
||||
DeviceName = auth.Device,
|
||||
};
|
||||
|
||||
return await _sessionManager.AuthenticateQuickConnect(
|
||||
authRequest,
|
||||
request.Token).ConfigureAwait(false);
|
||||
return _quickConnectManager.GetAuthorizedRequest(request.Secret);
|
||||
}
|
||||
catch (SecurityException e)
|
||||
{
|
||||
@@ -271,7 +264,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromBody, Required] UpdateUserPassword request)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
|
||||
}
|
||||
@@ -303,9 +296,9 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
|
||||
|
||||
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
|
||||
var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
|
||||
|
||||
_sessionManager.RevokeUserTokens(user.Id, currentToken);
|
||||
await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
@@ -325,11 +318,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UpdateUserEasyPassword(
|
||||
public async Task<ActionResult> UpdateUserEasyPassword(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromBody, Required] UpdateUserEasyPassword request)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
|
||||
}
|
||||
@@ -343,11 +336,11 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (request.ResetPassword)
|
||||
{
|
||||
_userManager.ResetEasyPassword(user);
|
||||
await _userManager.ResetEasyPassword(user).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword);
|
||||
await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
@@ -371,7 +364,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromBody, Required] UserDto updateUser)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
|
||||
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
|
||||
}
|
||||
@@ -431,8 +424,8 @@ namespace Jellyfin.Api.Controllers
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
|
||||
}
|
||||
|
||||
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
|
||||
_sessionManager.RevokeUserTokens(user.Id, currentToken);
|
||||
var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
|
||||
await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
|
||||
@@ -456,7 +449,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromBody, Required] UserConfiguration userConfig)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
|
||||
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
|
||||
}
|
||||
@@ -555,7 +548,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return _userManager.GetUserDto(user);
|
||||
}
|
||||
|
||||
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
|
||||
private async Task<IEnumerable<UserDto>> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
|
||||
{
|
||||
var users = _userManager.Users;
|
||||
|
||||
@@ -571,7 +564,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (filterByDevice)
|
||||
{
|
||||
var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId;
|
||||
var deviceId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.UserViewDtos;
|
||||
@@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the user views.</returns>
|
||||
[HttpGet("Users/{userId}/Views")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetUserViews(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromQuery] bool? includeExternalContent,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
|
||||
@@ -86,7 +87,7 @@ namespace Jellyfin.Api.Controllers
|
||||
query.PresetViews = presetViews;
|
||||
}
|
||||
|
||||
var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
|
||||
var app = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Client ?? string.Empty;
|
||||
if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows };
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
@@ -20,12 +19,10 @@ using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
@@ -140,7 +137,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
@@ -265,7 +262,11 @@ namespace Jellyfin.Api.Controllers
|
||||
EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
|
||||
};
|
||||
|
||||
// CTS lifecycle is managed internally.
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
// Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
|
||||
// since it gets disposed when ffmpeg exits
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
using var state = await StreamingHelpers.GetStreamingState(
|
||||
streamingRequest,
|
||||
Request,
|
||||
@@ -280,7 +281,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_deviceManager,
|
||||
_transcodingJobHelper,
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource.Token)
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
TranscodingJobDto? job = null;
|
||||
@@ -289,7 +290,7 @@ namespace Jellyfin.Api.Controllers
|
||||
if (!System.IO.File.Exists(playlistPath))
|
||||
{
|
||||
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
|
||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!System.IO.File.Exists(playlistPath))
|
||||
@@ -316,7 +317,7 @@ namespace Jellyfin.Api.Controllers
|
||||
minSegments = state.MinSegments;
|
||||
if (minSegments > 0)
|
||||
{
|
||||
await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,8 +366,7 @@ namespace Jellyfin.Api.Controllers
|
||||
else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var outputFmp4HeaderArg = string.Empty;
|
||||
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
if (isWindows)
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// on Windows, the path of fmp4 header file needs to be configured
|
||||
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
|
||||
@@ -484,7 +484,7 @@ namespace Jellyfin.Api.Controllers
|
||||
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
|
||||
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -25,14 +25,12 @@ using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
@@ -296,6 +294,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
||||
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
||||
/// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
|
||||
/// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
|
||||
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
||||
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
||||
@@ -308,7 +308,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
@@ -352,6 +352,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] int? videoBitRate,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||
@@ -373,6 +375,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] Dictionary<string, string> streamOptions)
|
||||
{
|
||||
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
||||
// CTS lifecycle is managed internally.
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var streamingRequest = new VideoRequestDto
|
||||
{
|
||||
@@ -406,6 +409,8 @@ namespace Jellyfin.Api.Controllers
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
MaxWidth = maxWidth,
|
||||
MaxHeight = maxHeight,
|
||||
VideoBitRate = videoBitRate,
|
||||
SubtitleStreamIndex = subtitleStreamIndex,
|
||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||
@@ -448,14 +453,15 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
||||
|
||||
await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
|
||||
{
|
||||
AllowEndOfFile = false
|
||||
}.WriteToAsync(Response.Body, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
|
||||
if (liveStreamInfo == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
|
||||
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
|
||||
return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
|
||||
return File(liveStream, MimeTypes.GetMimeType("file.ts"));
|
||||
}
|
||||
|
||||
// Static remote stream
|
||||
@@ -487,13 +493,8 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (state.MediaSource.IsInfiniteStream)
|
||||
{
|
||||
await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
|
||||
{
|
||||
AllowEndOfFile = false
|
||||
}.WriteToAsync(Response.Body, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return File(Response.Body, contentType);
|
||||
var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
|
||||
return File(liveStream, contentType);
|
||||
}
|
||||
|
||||
return FileStreamResponseHelpers.GetStaticFileResult(
|
||||
@@ -527,7 +528,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="segmentLength">The segment length.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
@@ -549,6 +550,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
||||
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
||||
/// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
|
||||
/// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
|
||||
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
||||
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
||||
@@ -556,12 +559,12 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
@@ -570,8 +573,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Video stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/{stream=stream}.{container}")]
|
||||
[HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")]
|
||||
[HttpGet("{itemId}/stream.{container}")]
|
||||
[HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesVideoFile]
|
||||
public Task<ActionResult> GetVideoStreamByContainer(
|
||||
@@ -605,6 +608,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] int? videoBitRate,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||
@@ -656,6 +661,8 @@ namespace Jellyfin.Api.Controllers
|
||||
startTimeTicks,
|
||||
width,
|
||||
height,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
videoBitRate,
|
||||
subtitleStreamIndex,
|
||||
subtitleMethod,
|
||||
|
||||
Reference in New Issue
Block a user