diff options
| author | Orry Verducci <orry@orryverducci.co.uk> | 2021-12-01 22:13:52 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-12-01 22:13:52 +0000 |
| commit | e446e9fde935ad5744500e6efaab8fcacf89b600 (patch) | |
| tree | 9012e91423660bf4bc9992f06cf26f53e826fb65 /Jellyfin.Api | |
| parent | 9abe9e7e54cc454667ba2128b5d321631b5ece51 (diff) | |
| parent | f6d8c19a7ac41c6c7c217d9e9ccbf98f78122327 (diff) | |
Merge branch 'master' into mbaff-interlace-detection
Diffstat (limited to 'Jellyfin.Api')
34 files changed, 230 insertions, 200 deletions
diff --git a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs index 49b6689cde..58552d847d 100644 --- a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs +++ b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs @@ -1,4 +1,6 @@ -using System; +#pragma warning disable CA1813 // Avoid unsealed attributes + +using System; namespace Jellyfin.Api.Attributes { diff --git a/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs index 001f27409e..244a29da45 100644 --- a/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs +++ b/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs @@ -3,7 +3,7 @@ /// <summary> /// Produces file attribute of "image/*". /// </summary> - public class AcceptsImageFileAttribute : AcceptsFileAttribute + public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute { private const string ContentType = "image/*"; diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs index 2fdd1e4899..af8727552c 100644 --- a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs +++ b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs @@ -7,7 +7,7 @@ namespace Jellyfin.Api.Attributes /// <summary> /// Identifies an action that supports the HTTP GET method. /// </summary> - public class HttpSubscribeAttribute : HttpMethodAttribute + public sealed class HttpSubscribeAttribute : HttpMethodAttribute { private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" }; diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs index d6d7e4563d..1c0b70e719 100644 --- a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs +++ b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs @@ -7,7 +7,7 @@ namespace Jellyfin.Api.Attributes /// <summary> /// Identifies an action that supports the HTTP GET method. /// </summary> - public class HttpUnsubscribeAttribute : HttpMethodAttribute + public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute { private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" }; diff --git a/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs index 56c9772b6d..514e7ce974 100644 --- a/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs +++ b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs @@ -6,7 +6,7 @@ namespace Jellyfin.Api.Attributes /// Attribute to mark a parameter as obsolete. /// </summary> [AttributeUsage(AttributeTargets.Parameter)] - public class ParameterObsoleteAttribute : Attribute + public sealed class ParameterObsoleteAttribute : Attribute { } } diff --git a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs index 3adb700eb5..9fc25f192e 100644 --- a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs @@ -3,7 +3,7 @@ /// <summary> /// Produces file attribute of "image/*". /// </summary> - public class ProducesAudioFileAttribute : ProducesFileAttribute + public sealed class ProducesAudioFileAttribute : ProducesFileAttribute { private const string ContentType = "audio/*"; diff --git a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs index 62a576ede2..2bf77d729a 100644 --- a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs @@ -1,4 +1,6 @@ -using System; +#pragma warning disable CA1813 // Avoid unsealed attributes + +using System; namespace Jellyfin.Api.Attributes { diff --git a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs index e158136762..1e5b542e27 100644 --- a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs @@ -3,7 +3,7 @@ /// <summary> /// Produces file attribute of "image/*". /// </summary> - public class ProducesImageFileAttribute : ProducesFileAttribute + public sealed class ProducesImageFileAttribute : ProducesFileAttribute { private const string ContentType = "image/*"; diff --git a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs index 5d928ab914..5b15cb1a56 100644 --- a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs @@ -3,7 +3,7 @@ /// <summary> /// Produces file attribute of "image/*". /// </summary> - public class ProducesPlaylistFileAttribute : ProducesFileAttribute + public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute { private const string ContentType = "application/x-mpegURL"; diff --git a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs index d8b2856dca..6857d45ecc 100644 --- a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs @@ -3,7 +3,7 @@ /// <summary> /// Produces file attribute of "video/*". /// </summary> - public class ProducesVideoFileAttribute : ProducesFileAttribute + public sealed class ProducesVideoFileAttribute : ProducesFileAttribute { private const string ContentType = "video/*"; diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 369e846aef..bd3e7d9e3e 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -45,6 +45,11 @@ namespace Jellyfin.Api.Auth try { var authorizationInfo = await _authService.Authenticate(Request).ConfigureAwait(false); + if (!authorizationInfo.HasToken) + { + return AuthenticateResult.NoResult(); + } + var role = UserRoles.User; if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) { diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs new file mode 100644 index 0000000000..98fd224307 --- /dev/null +++ b/Jellyfin.Api/Controllers/ClientLogController.cs @@ -0,0 +1,80 @@ +using System.Net.Mime; +using System.Threading.Tasks; +using Jellyfin.Api.Attributes; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.ClientLogDtos; +using MediaBrowser.Controller.ClientEvent; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// Client log controller. + /// </summary> + [Authorize(Policy = Policies.DefaultAuthorization)] + public class ClientLogController : BaseJellyfinApiController + { + private const int MaxDocumentSize = 1_000_000; + private readonly IClientEventLogger _clientEventLogger; + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="ClientLogController"/> class. + /// </summary> + /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public ClientLogController( + IClientEventLogger clientEventLogger, + IServerConfigurationManager serverConfigurationManager) + { + _clientEventLogger = clientEventLogger; + _serverConfigurationManager = serverConfigurationManager; + } + + /// <summary> + /// Upload a document. + /// </summary> + /// <response code="200">Document saved.</response> + /// <response code="403">Event logging disabled.</response> + /// <response code="413">Upload size too large.</response> + /// <returns>Create response.</returns> + [HttpPost("Document")] + [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] + [AcceptsFile(MediaTypeNames.Text.Plain)] + [RequestSizeLimit(MaxDocumentSize)] + public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile() + { + if (!_serverConfigurationManager.Configuration.AllowClientLogUpload) + { + return Forbid(); + } + + if (Request.ContentLength > MaxDocumentSize) + { + // Manually validate to return proper status code. + return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes"); + } + + var (clientName, clientVersion) = GetRequestInformation(); + var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body) + .ConfigureAwait(false); + return Ok(new ClientLogDocumentResponseDto(fileName)); + } + + private (string ClientName, string ClientVersion) GetRequestInformation() + { + var clientName = ClaimHelpers.GetClient(HttpContext.User) ?? "unknown-client"; + var clientVersion = ClaimHelpers.GetIsApiKey(HttpContext.User) + ? "apikey" + : ClaimHelpers.GetVersion(HttpContext.User) ?? "unknown-version"; + + return (clientName, clientVersion); + } + } +} diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 2079476d0a..0b2604640b 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -8,7 +8,7 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; -using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Dto; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -143,21 +143,24 @@ namespace Jellyfin.Api.Controllers existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection; existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion) + && !string.IsNullOrEmpty(chromecastVersion) ? Enum.Parse<ChromecastVersion>(chromecastVersion, true) : ChromecastVersion.Stable; displayPreferences.CustomPrefs.Remove("chromecastVersion"); - existingDisplayPreferences.EnableNextVideoInfoOverlay = displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay) - ? bool.Parse(enableNextVideoInfoOverlay) - : true; + existingDisplayPreferences.EnableNextVideoInfoOverlay = !displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay) + || string.IsNullOrEmpty(enableNextVideoInfoOverlay) + || bool.Parse(enableNextVideoInfoOverlay); displayPreferences.CustomPrefs.Remove("enableNextVideoInfoOverlay"); existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength) + && !string.IsNullOrEmpty(skipBackLength) ? int.Parse(skipBackLength, CultureInfo.InvariantCulture) : 10000; displayPreferences.CustomPrefs.Remove("skipBackLength"); existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength) + && !string.IsNullOrEmpty(skipForwardLength) ? int.Parse(skipForwardLength, CultureInfo.InvariantCulture) : 30000; displayPreferences.CustomPrefs.Remove("skipForwardLength"); @@ -196,7 +199,7 @@ namespace Jellyfin.Api.Controllers } var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, itemId, existingDisplayPreferences.Client); - itemPrefs.SortBy = displayPreferences.SortBy; + itemPrefs.SortBy = displayPreferences.SortBy ?? "SortName"; itemPrefs.SortOrder = displayPreferences.SortOrder; itemPrefs.RememberIndexing = displayPreferences.RememberIndexing; itemPrefs.RememberSorting = displayPreferences.RememberSorting; diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs index 052a6aff2e..35c3a3d922 100644 --- a/Jellyfin.Api/Controllers/DlnaController.cs +++ b/Jellyfin.Api/Controllers/DlnaController.cs @@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - _dlnaManager.UpdateProfile(deviceProfile); + _dlnaManager.UpdateProfile(profileId, deviceProfile); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 42e82dd5b1..caa3d23681 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1391,7 +1391,7 @@ namespace Jellyfin.Api.Controllers } else { - _logger.LogError("Invalid HLS segment container: " + segmentFormat); + _logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat); } var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128 @@ -1794,7 +1794,7 @@ namespace Jellyfin.Api.Controllers return; } - _logger.LogDebug("Deleting partial HLS file {path}", path); + _logger.LogDebug("Deleting partial HLS file {Path}", path); try { @@ -1802,15 +1802,15 @@ namespace Jellyfin.Api.Controllers } catch (IOException ex) { - _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path); + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); var task = Task.Delay(100); - Task.WaitAll(task); + task.Wait(); DeleteFile(path, retryCount + 1); } catch (Exception ex) { - _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path); + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); } } diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 71caa0fe0d..7325dca0ae 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath)) + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid segment."); } @@ -90,7 +90,7 @@ namespace Jellyfin.Api.Controllers var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8") + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8") { return BadRequest("Invalid segment."); } @@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath)) + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture)) { return BadRequest("Invalid segment."); } diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 99ab7f232e..89bbf22c96 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - if (!path.StartsWith(_applicationPaths.GeneralPath)) + if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } @@ -177,7 +177,7 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { - if (!path.StartsWith(basePath)) + if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } @@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { - if (!path.StartsWith(basePath)) + if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index 448510c06a..8a6f9b8c75 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -5,8 +5,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; @@ -30,7 +28,6 @@ namespace Jellyfin.Api.Controllers public class ItemLookupController : BaseJellyfinApiController { private readonly IProviderManager _providerManager; - private readonly IServerApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; private readonly ILogger<ItemLookupController> _logger; @@ -39,19 +36,16 @@ namespace Jellyfin.Api.Controllers /// Initializes a new instance of the <see cref="ItemLookupController"/> class. /// </summary> /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param> public ItemLookupController( IProviderManager providerManager, - IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILogger<ItemLookupController> logger) { _providerManager = providerManager; - _appPaths = serverConfigurationManager.ApplicationPaths; _fileSystem = fileSystem; _libraryManager = libraryManager; _logger = logger; diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index f0d44e5cc8..45a36c8fe1 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -10,6 +10,7 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -33,6 +34,7 @@ namespace Jellyfin.Api.Controllers private readonly ILocalizationManager _localization; private readonly IDtoService _dtoService; private readonly ILogger<ItemsController> _logger; + private readonly ISessionManager _sessionManager; /// <summary> /// Initializes a new instance of the <see cref="ItemsController"/> class. @@ -42,18 +44,21 @@ namespace Jellyfin.Api.Controllers /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> + /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> public ItemsController( IUserManager userManager, ILibraryManager libraryManager, ILocalizationManager localization, IDtoService dtoService, - ILogger<ItemsController> logger) + ILogger<ItemsController> logger, + ISessionManager sessionManager) { _userManager = userManager; _libraryManager = libraryManager; _localization = localization; _dtoService = dtoService; _logger = logger; + _sessionManager = sessionManager; } /// <summary> @@ -763,6 +768,7 @@ namespace Jellyfin.Api.Controllers /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param> /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param> /// <param name="enableImages">Optional. Include image information in output.</param> + /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param> /// <response code="200">Items returned.</response> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns> [HttpGet("Users/{userId}/Items/Resume")] @@ -781,7 +787,8 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool? enableImages = true) + [FromQuery] bool? enableImages = true, + [FromQuery] bool excludeActiveSessions = false) { var user = _userManager.GetUserById(userId); var parentIdGuid = parentId ?? Guid.Empty; @@ -801,6 +808,15 @@ namespace Jellyfin.Api.Controllers .ToArray(); } + var excludeItemIds = Array.Empty<Guid>(); + if (excludeActiveSessions) + { + excludeItemIds = _sessionManager.Sessions + .Where(s => s.UserId == userId && s.NowPlayingItem != null) + .Select(s => s.NowPlayingItem.Id) + .ToArray(); + } + var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) }, @@ -817,7 +833,8 @@ namespace Jellyfin.Api.Controllers AncestorIds = ancestorIds, IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - SearchTerm = searchTerm + SearchTerm = searchTerm, + ExcludeItemIds = excludeItemIds }); var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index b98307f879..cb4894d771 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -26,7 +26,6 @@ namespace Jellyfin.Api.Controllers private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; private readonly IUserManager _userManager; - private readonly IUserDataManager _userDataManager; /// <summary> /// Initializes a new instance of the <see cref="PersonsController"/> class. @@ -34,17 +33,14 @@ namespace Jellyfin.Api.Controllers /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> - /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> public PersonsController( ILibraryManager libraryManager, IDtoService dtoService, - IUserManager userManager, - IUserDataManager userDataManager) + IUserManager userManager) { _libraryManager = libraryManager; _dtoService = dtoService; _userManager = userManager; - _userDataManager = userDataManager; } /// <summary> diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 0ae6109bcc..0778ea3fc5 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -28,7 +28,6 @@ namespace Jellyfin.Api.Controllers { private readonly IInstallationManager _installationManager; private readonly IPluginManager _pluginManager; - private readonly IConfigurationManager _config; private readonly JsonSerializerOptions _serializerOptions; /// <summary> @@ -36,16 +35,13 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param> - /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param> public PluginsController( IInstallationManager installationManager, - IPluginManager pluginManager, - IConfigurationManager config) + IPluginManager pluginManager) { _installationManager = installationManager; _pluginManager = pluginManager; _serializerOptions = JsonDefaults.Options; - _config = config; } /// <summary> diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 35921ede8f..773cff1ac6 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -30,7 +30,6 @@ namespace Jellyfin.Api.Controllers { private readonly IProviderManager _providerManager; private readonly IServerApplicationPaths _applicationPaths; - private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryManager _libraryManager; /// <summary> @@ -38,17 +37,14 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param> - /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> public RemoteImageController( IProviderManager providerManager, IServerApplicationPaths applicationPaths, - IHttpClientFactory httpClientFactory, ILibraryManager libraryManager) { _providerManager = providerManager; _applicationPaths = applicationPaths; - _httpClientFactory = httpClientFactory; _libraryManager = libraryManager; } diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index db8307f284..16acedcf35 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -528,7 +528,7 @@ namespace Jellyfin.Api.Controllers if (fontFile != null && fileSize != null && fileSize > 0) { - _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize); + _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize); return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName)); } else diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 904738bb45..2ff85fd2ae 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -212,10 +212,13 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns> [HttpGet("WakeOnLanInfo")] [Authorize(Policy = Policies.DefaultAuthorization)] + [Obsolete("This endpoint is obsolete.")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo() { - var result = _appHost.GetWakeOnLanInfo(); + var result = _network.GetMacAddresses() + .Select(i => new WakeOnLanInfo(i)) + .ToList(); return Ok(result); } } diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index e1cbc6f331..3c079a71dc 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -451,7 +451,7 @@ namespace Jellyfin.Api.Controllers if (@static.HasValue && @static.Value && state.DirectStreamProvider != null) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId); if (liveStreamInfo == null) @@ -467,7 +467,7 @@ namespace Jellyfin.Api.Controllers // Static remote stream if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false); @@ -484,7 +484,7 @@ namespace Jellyfin.Api.Controllers var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); var isTranscodeCached = outputPathExists && transcodingJob != null; - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager); // Static stream if (@static.HasValue && @static.Value) diff --git a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs index a911a33241..76fb27bcc3 100644 --- a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs +++ b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Api.Helpers // If any this null throw an exception. if (source == null || destination == null) { - throw new Exception("Source or/and Destination Objects are null"); + throw new ArgumentException("Source or/and Destination Objects are null"); } // Getting the Types of the objects. diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs index 61e18220a1..3fa07720ae 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs @@ -17,7 +17,6 @@ namespace Jellyfin.Api.Helpers private readonly TranscodingJobDto? _job; private readonly TranscodingJobHelper? _transcodingJobHelper; private readonly int _timeoutMs; - private int _bytesWritten; private bool _disposed; /// <summary> @@ -71,53 +70,58 @@ namespace Jellyfin.Api.Helpers /// <inheritdoc /> public override void Flush() { - _stream.Flush(); + // Not supported } /// <inheritdoc /> public override int Read(byte[] buffer, int offset, int count) + => Read(buffer.AsSpan(offset, count)); + + /// <inheritdoc /> + public override int Read(Span<byte> buffer) { - return _stream.Read(buffer, offset, count); + int totalBytesRead = 0; + var stopwatch = Stopwatch.StartNew(); + + while (KeepReading(stopwatch.ElapsedMilliseconds)) + { + totalBytesRead += _stream.Read(buffer); + if (totalBytesRead > 0) + { + break; + } + + Thread.Sleep(50); + } + + UpdateBytesWritten(totalBytesRead); + + return totalBytesRead; } /// <inheritdoc /> public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); + + /// <inheritdoc /> + public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) { int totalBytesRead = 0; - int remainingBytesToRead = count; var stopwatch = Stopwatch.StartNew(); - int newOffset = offset; - while (remainingBytesToRead > 0) + while (KeepReading(stopwatch.ElapsedMilliseconds)) { - cancellationToken.ThrowIfCancellationRequested(); - int bytesRead = await _stream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false); - - remainingBytesToRead -= bytesRead; - newOffset += bytesRead; - - if (bytesRead > 0) + totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + if (totalBytesRead > 0) { - _bytesWritten += bytesRead; - totalBytesRead += bytesRead; - - if (_job != null) - { - _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); - } + break; } - else - { - // If the job is null it's a live stream and will require user action to close, but don't keep it open indefinitely - if (_job?.HasExited ?? stopwatch.ElapsedMilliseconds > _timeoutMs) - { - break; - } - await Task.Delay(50, cancellationToken).ConfigureAwait(false); - } + await Task.Delay(50, cancellationToken).ConfigureAwait(false); } + UpdateBytesWritten(totalBytesRead); + return totalBytesRead; } @@ -159,5 +163,19 @@ namespace Jellyfin.Api.Helpers base.Dispose(disposing); } } + + private void UpdateBytesWritten(int totalBytesRead) + { + if (_job != null) + { + _job.BytesDownloaded += totalBytesRead; + } + } + + private bool KeepReading(long elapsed) + { + // If the job is null it's a live stream and will require user action to close, but don't keep it open indefinitely + return !_job?.HasExited ?? elapsed < _timeoutMs; + } } } diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 4fc791665e..1b8f24c27d 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -148,7 +148,7 @@ namespace Jellyfin.Api.Helpers mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId) ? mediaSources[0] - : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.InvariantCulture)); + : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal)); if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id) { diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 07d0b55433..9d80070ebf 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -283,6 +283,7 @@ namespace Jellyfin.Api.Helpers lock (job.ProcessLock!) { + #pragma warning disable CA1849 // Can't await in lock block job.TranscodingThrottler?.Stop().GetAwaiter().GetResult(); var process = job.Process; @@ -308,6 +309,7 @@ namespace Jellyfin.Api.Helpers { } } + #pragma warning restore CA1849 } if (delete(job.Path!)) @@ -541,8 +543,7 @@ namespace Jellyfin.Api.Helpers state, cancellationTokenSource); - var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; - _logger.LogInformation(commandLineLogMessage); + _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); var logFilePrefix = "FFmpeg.Transcode-"; if (state.VideoRequest != null @@ -560,8 +561,9 @@ namespace Jellyfin.Api.Helpers // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); - await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); + await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false); process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state); diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 57480b2f35..a3598edfa4 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -13,8 +13,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.3" /> </ItemGroup> diff --git a/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs new file mode 100644 index 0000000000..44509a9c04 --- /dev/null +++ b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs @@ -0,0 +1,22 @@ +namespace Jellyfin.Api.Models.ClientLogDtos +{ + /// <summary> + /// Client log document response dto. + /// </summary> + public class ClientLogDocumentResponseDto + { + /// <summary> + /// Initializes a new instance of the <see cref="ClientLogDocumentResponseDto"/> class. + /// </summary> + /// <param name="fileName">The file name.</param> + public ClientLogDocumentResponseDto(string fileName) + { + FileName = fileName; + } + + /// <summary> + /// Gets the resulting filename. + /// </summary> + public string FileName { get; } + } +} diff --git a/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs b/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs deleted file mode 100644 index 249d828d33..0000000000 --- a/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Collections.Generic; -using Jellyfin.Data.Enums; - -namespace Jellyfin.Api.Models.DisplayPreferencesDtos -{ - /// <summary> - /// Defines the display preferences for any item that supports them (usually Folders). - /// </summary> - public class DisplayPreferencesDto - { - /// <summary> - /// Initializes a new instance of the <see cref="DisplayPreferencesDto" /> class. - /// </summary> - public DisplayPreferencesDto() - { - RememberIndexing = false; - PrimaryImageHeight = 250; - PrimaryImageWidth = 250; - ShowBackdrop = true; - CustomPrefs = new Dictionary<string, string>(); - } - - /// <summary> - /// Gets or sets the user id. - /// </summary> - /// <value>The user id.</value> - public string? Id { get; set; } - - /// <summary> - /// Gets or sets the type of the view. - /// </summary> - /// <value>The type of the view.</value> - public string? ViewType { get; set; } - - /// <summary> - /// Gets or sets the sort by. - /// </summary> - /// <value>The sort by.</value> - public string? SortBy { get; set; } - - /// <summary> - /// Gets or sets the index by. - /// </summary> - /// <value>The index by.</value> - public string? IndexBy { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [remember indexing]. - /// </summary> - /// <value><c>true</c> if [remember indexing]; otherwise, <c>false</c>.</value> - public bool RememberIndexing { get; set; } - - /// <summary> - /// Gets or sets the height of the primary image. - /// </summary> - /// <value>The height of the primary image.</value> - public int PrimaryImageHeight { get; set; } - - /// <summary> - /// Gets or sets the width of the primary image. - /// </summary> - /// <value>The width of the primary image.</value> - public int PrimaryImageWidth { get; set; } - - /// <summary> - /// Gets the custom prefs. - /// </summary> - /// <value>The custom prefs.</value> - public Dictionary<string, string> CustomPrefs { get; } - - /// <summary> - /// Gets or sets the scroll direction. - /// </summary> - /// <value>The scroll direction.</value> - public ScrollDirection ScrollDirection { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether to show backdrops on this item. - /// </summary> - /// <value><c>true</c> if showing backdrops; otherwise, <c>false</c>.</value> - public bool ShowBackdrop { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [remember sorting]. - /// </summary> - /// <value><c>true</c> if [remember sorting]; otherwise, <c>false</c>.</value> - public bool RememberSorting { get; set; } - - /// <summary> - /// Gets or sets the sort order. - /// </summary> - /// <value>The sort order.</value> - public SortOrder SortOrder { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [show sidebar]. - /// </summary> - /// <value><c>true</c> if [show sidebar]; otherwise, <c>false</c>.</value> - public bool ShowSidebar { get; set; } - - /// <summary> - /// Gets or sets the client. - /// </summary> - public string? Client { get; set; } - } -} diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs index fed837b856..ab67c8732a 100644 --- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs +++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs @@ -134,7 +134,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos /// <summary> /// Gets or sets bytes downloaded. /// </summary> - public long? BytesDownloaded { get; set; } + public long BytesDownloaded { get; set; } /// <summary> /// Gets or sets bytes transcoded. diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs index 7b32d76ba7..7a1ca252cf 100644 --- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs +++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs @@ -141,7 +141,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos private bool IsThrottleAllowed(TranscodingJobDto job, int thresholdSeconds) { - var bytesDownloaded = job.BytesDownloaded ?? 0; + var bytesDownloaded = job.BytesDownloaded; var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0; var downloadPositionTicks = job.DownloadPositionTicks ?? 0; @@ -197,7 +197,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos } } - _logger.LogDebug("No throttle data for " + path); + _logger.LogDebug("No throttle data for {Path}", path); return false; } |
