From f5f890e68562e55d4bed16c454c4b4305152b296 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 31 Jan 2023 12:18:10 +0100 Subject: [PATCH] Migrate to file-scoped namespaces --- .../Attributes/AcceptsFileAttribute.cs | 39 +- .../Attributes/AcceptsImageFileAttribute.cs | 29 +- .../Attributes/HttpSubscribeAttribute.cs | 39 +- .../Attributes/HttpUnsubscribeAttribute.cs | 39 +- .../Attributes/ParameterObsoleteAttribute.cs | 15 +- .../Attributes/ProducesAudioFileAttribute.cs | 29 +- .../Attributes/ProducesFileAttribute.cs | 39 +- .../Attributes/ProducesImageFileAttribute.cs | 29 +- .../ProducesPlaylistFileAttribute.cs | 29 +- .../Attributes/ProducesVideoFileAttribute.cs | 29 +- Jellyfin.Api/BaseJellyfinApiController.cs | 53 +- .../Constants/AuthenticationSchemes.cs | 17 +- Jellyfin.Api/Constants/InternalClaimTypes.cs | 65 +- Jellyfin.Api/Constants/Policies.cs | 121 +- Jellyfin.Api/Constants/UserRoles.cs | 33 +- .../Controllers/ActivityLogController.cs | 77 +- Jellyfin.Api/Controllers/ApiKeyController.cs | 109 +- Jellyfin.Api/Controllers/ArtistsController.cs | 857 ++-- Jellyfin.Api/Controllers/AudioController.cs | 681 ++- .../Controllers/BrandingController.cs | 85 +- .../Controllers/ChannelsController.cs | 423 +- .../Controllers/ClientLogController.cs | 107 +- .../Controllers/CollectionController.cs | 171 +- .../Controllers/ConfigurationController.cs | 215 +- .../Controllers/DashboardController.cs | 159 +- Jellyfin.Api/Controllers/DevicesController.cs | 213 +- .../DisplayPreferencesController.cs | 349 +- Jellyfin.Api/Controllers/DlnaController.cs | 215 +- .../Controllers/DlnaServerController.cs | 573 ++- .../Controllers/DynamicHlsController.cs | 3793 ++++++++-------- .../Controllers/EnvironmentController.cs | 295 +- Jellyfin.Api/Controllers/FilterController.cs | 357 +- Jellyfin.Api/Controllers/GenresController.cs | 339 +- .../Controllers/HlsSegmentController.cs | 319 +- Jellyfin.Api/Controllers/ImageController.cs | 3851 ++++++++--------- .../Controllers/InstantMixController.cs | 661 ++- .../Controllers/ItemLookupController.cs | 483 ++- .../Controllers/ItemRefreshController.cs | 125 +- .../Controllers/ItemUpdateController.cs | 753 ++-- Jellyfin.Api/Controllers/ItemsController.cs | 1647 ++++--- Jellyfin.Api/Controllers/LibraryController.cs | 1723 ++++---- .../Controllers/LibraryStructureController.cs | 533 ++- Jellyfin.Api/Controllers/LiveTvController.cs | 2271 +++++----- .../Controllers/LocalizationController.cs | 115 +- .../Controllers/MediaInfoController.cs | 537 ++- Jellyfin.Api/Controllers/MoviesController.cs | 509 ++- .../Controllers/MusicGenresController.cs | 317 +- Jellyfin.Api/Controllers/PackageController.cs | 267 +- Jellyfin.Api/Controllers/PersonsController.cs | 217 +- .../Controllers/PlaylistsController.cs | 347 +- .../Controllers/PlaystateController.cs | 693 ++- Jellyfin.Api/Controllers/PluginsController.cs | 425 +- .../Controllers/QuickConnectController.cs | 217 +- .../Controllers/RemoteImageController.cs | 279 +- .../Controllers/ScheduledTasksController.cs | 271 +- Jellyfin.Api/Controllers/SearchController.cs | 427 +- Jellyfin.Api/Controllers/SessionController.cs | 873 ++-- Jellyfin.Api/Controllers/StartupController.cs | 249 +- Jellyfin.Api/Controllers/StudiosController.cs | 245 +- .../Controllers/SubtitleController.cs | 955 ++-- .../Controllers/SuggestionsController.cs | 133 +- .../Controllers/SyncPlayController.cs | 757 ++-- Jellyfin.Api/Controllers/SystemController.cs | 369 +- .../Controllers/TimeSyncController.cs | 41 +- .../Controllers/TrailersController.cs | 557 ++- Jellyfin.Api/Controllers/TvShowsController.cs | 657 ++- .../Controllers/UniversalAudioController.cs | 561 ++- Jellyfin.Api/Controllers/UserController.cs | 1069 +++-- .../Controllers/UserLibraryController.cs | 763 ++-- .../Controllers/UserViewsController.cs | 207 +- .../Controllers/VideoAttachmentsController.cs | 117 +- Jellyfin.Api/Controllers/VideosController.cs | 1187 +++-- Jellyfin.Api/Controllers/YearsController.cs | 349 +- Jellyfin.Api/Extensions/DtoExtensions.cs | 185 +- .../CamelCaseJsonProfileFormatter.cs | 21 +- Jellyfin.Api/Formatters/CssOutputFormatter.cs | 45 +- .../PascalCaseJsonProfileFormatter.cs | 25 +- Jellyfin.Api/Formatters/XmlOutputFormatter.cs | 37 +- Jellyfin.Api/Helpers/AudioHelper.cs | 291 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 1325 +++--- .../Helpers/FileStreamResponseHelpers.cs | 179 +- Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 321 +- Jellyfin.Api/Helpers/HlsHelpers.cs | 201 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 857 ++-- Jellyfin.Api/Helpers/ProgressiveFileStream.cs | 285 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 229 +- Jellyfin.Api/Helpers/StreamingHelpers.cs | 1385 +++--- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 1649 ++++--- .../BaseUrlRedirectionMiddleware.cs | 117 +- .../Middleware/ExceptionMiddleware.cs | 237 +- .../IpBasedAccessValidationMiddleware.cs | 67 +- .../Middleware/LanFilteringMiddleware.cs | 55 +- .../LegacyEmbyRouteRewriteMiddleware.cs | 77 +- .../QueryStringDecodingMiddleware.cs | 49 +- .../Middleware/ResponseTimeMiddleware.cs | 95 +- .../Middleware/RobotsRedirectionMiddleware.cs | 63 +- .../ServerStartupMessageMiddleware.cs | 67 +- .../Middleware/UrlDecodeQueryFeature.cs | 125 +- .../Middleware/WebSocketHandlerMiddleware.cs | 51 +- .../CommaDelimitedArrayModelBinder.cs | 115 +- .../ModelBinders/LegacyDateTimeModelBinder.cs | 61 +- .../ModelBinders/NullableEnumModelBinder.cs | 61 +- .../NullableEnumModelBinderProvider.cs | 35 +- .../PipeDelimitedArrayModelBinder.cs | 115 +- .../ClientLogDocumentResponseDto.cs | 31 +- .../ConfigurationDtos/MediaEncoderPathDto.cs | 25 +- Jellyfin.Api/Models/ConfigurationPageInfo.cs | 113 +- .../DefaultDirectoryBrowserInfoDto.cs | 17 +- .../Models/EnvironmentDtos/ValidatePathDto.cs | 33 +- .../LibraryDtos/LibraryOptionInfoDto.cs | 25 +- .../LibraryDtos/LibraryOptionsResultDto.cs | 41 +- .../LibraryDtos/LibraryTypeOptionsDto.cs | 49 +- .../Models/LibraryDtos/MediaUpdateInfoDto.cs | 17 +- .../LibraryDtos/MediaUpdateInfoPathDto.cs | 27 +- .../AddVirtualFolderDto.cs | 17 +- .../LibraryStructureDto/MediaPathDto.cs | 35 +- .../UpdateLibraryOptionsDto.cs | 25 +- .../UpdateMediaPathRequestDto.cs | 29 +- .../LiveTvDtos/ChannelMappingOptionsDto.cs | 41 +- .../Models/LiveTvDtos/GetProgramsDto.cs | 281 +- .../Models/LiveTvDtos/SetChannelMappingDto.cs | 39 +- .../Models/MediaInfoDtos/OpenLiveStreamDto.cs | 113 +- .../Models/MediaInfoDtos/PlaybackInfoDto.cs | 129 +- .../Models/PlaybackDtos/TranscodingJobDto.cs | 519 ++- .../PlaybackDtos/TranscodingThrottler.cs | 391 +- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 43 +- .../SessionDtos/ClientCapabilitiesDto.cs | 147 +- .../StartupDtos/StartupConfigurationDto.cs | 33 +- .../StartupDtos/StartupRemoteAccessDto.cs | 29 +- .../Models/StartupDtos/StartupUserDto.cs | 25 +- .../StreamingDtos/HlsAudioRequestDto.cs | 17 +- .../StreamingDtos/HlsVideoRequestDto.cs | 17 +- .../Models/StreamingDtos/StreamState.cs | 325 +- .../StreamingDtos/StreamingRequestDto.cs | 81 +- .../Models/StreamingDtos/VideoRequestDto.cs | 27 +- .../Models/SubtitleDtos/UploadSubtitleDto.cs | 49 +- .../Models/SyncPlayDtos/BufferRequestDto.cs | 67 +- .../SyncPlayDtos/IgnoreWaitRequestDto.cs | 19 +- .../SyncPlayDtos/JoinGroupRequestDto.cs | 19 +- .../MovePlaylistItemRequestDto.cs | 43 +- .../Models/SyncPlayDtos/NewGroupRequestDto.cs | 31 +- .../Models/SyncPlayDtos/NextItemRequestDto.cs | 31 +- .../Models/SyncPlayDtos/PingRequestDto.cs | 19 +- .../Models/SyncPlayDtos/PlayRequestDto.cs | 55 +- .../SyncPlayDtos/PreviousItemRequestDto.cs | 31 +- .../Models/SyncPlayDtos/QueueRequestDto.cs | 43 +- .../Models/SyncPlayDtos/ReadyRequestDto.cs | 67 +- .../RemoveFromPlaylistRequestDto.cs | 55 +- .../Models/SyncPlayDtos/SeekRequestDto.cs | 19 +- .../SyncPlayDtos/SetPlaylistItemRequestDto.cs | 31 +- .../SyncPlayDtos/SetRepeatModeRequestDto.cs | 19 +- .../SyncPlayDtos/SetShuffleModeRequestDto.cs | 19 +- .../Models/UserDtos/AuthenticateUserByName.cs | 25 +- .../Models/UserDtos/CreateUserByName.cs | 25 +- .../Models/UserDtos/ForgotPasswordDto.cs | 19 +- .../Models/UserDtos/ForgotPasswordPinDto.cs | 19 +- .../Models/UserDtos/QuickConnectDto.cs | 19 +- .../Models/UserDtos/UpdateUserEasyPassword.cs | 33 +- .../Models/UserDtos/UpdateUserPassword.cs | 41 +- .../UserViewDtos/SpecialViewOptionDto.cs | 25 +- .../ActivityLogWebSocketListener.cs | 85 +- .../ScheduledTasksWebSocketListener.cs | 117 +- .../SessionInfoWebSocketListener.cs | 151 +- 163 files changed, 23939 insertions(+), 24102 deletions(-) diff --git a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs index fbe68b6b97..a6c89bab88 100644 --- a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs +++ b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs @@ -2,29 +2,28 @@ using System; -namespace Jellyfin.Api.Attributes +namespace Jellyfin.Api.Attributes; + +/// +/// Internal produces image attribute. +/// +[AttributeUsage(AttributeTargets.Method)] +public class AcceptsFileAttribute : Attribute { + private readonly string[] _contentTypes; + /// - /// Internal produces image attribute. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.Method)] - public class AcceptsFileAttribute : Attribute + /// Content types this endpoint produces. + public AcceptsFileAttribute(params string[] contentTypes) { - private readonly string[] _contentTypes; - - /// - /// Initializes a new instance of the class. - /// - /// Content types this endpoint produces. - public AcceptsFileAttribute(params string[] contentTypes) - { - _contentTypes = contentTypes; - } - - /// - /// Gets the configured content types. - /// - /// the configured content types. - public string[] ContentTypes => _contentTypes; + _contentTypes = contentTypes; } + + /// + /// Gets the configured content types. + /// + /// the configured content types. + public string[] ContentTypes => _contentTypes; } diff --git a/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs index 244a29da45..57433202e1 100644 --- a/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs +++ b/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs @@ -1,18 +1,17 @@ -namespace Jellyfin.Api.Attributes -{ - /// - /// Produces file attribute of "image/*". - /// - public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute - { - private const string ContentType = "image/*"; +namespace Jellyfin.Api.Attributes; - /// - /// Initializes a new instance of the class. - /// - public AcceptsImageFileAttribute() - : base(ContentType) - { - } +/// +/// Produces file attribute of "image/*". +/// +public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute +{ + private const string ContentType = "image/*"; + + /// + /// Initializes a new instance of the class. + /// + public AcceptsImageFileAttribute() + : base(ContentType) + { } } diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs index 4dcf5976a2..cbd32ed822 100644 --- a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs +++ b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs @@ -2,29 +2,28 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Routing; -namespace Jellyfin.Api.Attributes +namespace Jellyfin.Api.Attributes; + +/// +/// Identifies an action that supports the HTTP GET method. +/// +public sealed class HttpSubscribeAttribute : HttpMethodAttribute { + private static readonly IEnumerable _supportedMethods = new[] { "SUBSCRIBE" }; + /// - /// Identifies an action that supports the HTTP GET method. + /// Initializes a new instance of the class. /// - public sealed class HttpSubscribeAttribute : HttpMethodAttribute + public HttpSubscribeAttribute() + : base(_supportedMethods) { - private static readonly IEnumerable _supportedMethods = new[] { "SUBSCRIBE" }; - - /// - /// Initializes a new instance of the class. - /// - public HttpSubscribeAttribute() - : base(_supportedMethods) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The route template. May not be null. - public HttpSubscribeAttribute(string template) - : base(_supportedMethods, template) - => ArgumentNullException.ThrowIfNull(template); } + + /// + /// Initializes a new instance of the class. + /// + /// The route template. May not be null. + public HttpSubscribeAttribute(string template) + : base(_supportedMethods, template) + => ArgumentNullException.ThrowIfNull(template); } diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs index d0238424a7..f4a6dcdaf9 100644 --- a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs +++ b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs @@ -2,29 +2,28 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Routing; -namespace Jellyfin.Api.Attributes +namespace Jellyfin.Api.Attributes; + +/// +/// Identifies an action that supports the HTTP GET method. +/// +public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute { + private static readonly IEnumerable _supportedMethods = new[] { "UNSUBSCRIBE" }; + /// - /// Identifies an action that supports the HTTP GET method. + /// Initializes a new instance of the class. /// - public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute + public HttpUnsubscribeAttribute() + : base(_supportedMethods) { - private static readonly IEnumerable _supportedMethods = new[] { "UNSUBSCRIBE" }; - - /// - /// Initializes a new instance of the class. - /// - public HttpUnsubscribeAttribute() - : base(_supportedMethods) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The route template. May not be null. - public HttpUnsubscribeAttribute(string template) - : base(_supportedMethods, template) - => ArgumentNullException.ThrowIfNull(template); } + + /// + /// Initializes a new instance of the class. + /// + /// The route template. May not be null. + public HttpUnsubscribeAttribute(string template) + : base(_supportedMethods, template) + => ArgumentNullException.ThrowIfNull(template); } diff --git a/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs index 514e7ce974..bf64fef5d7 100644 --- a/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs +++ b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs @@ -1,12 +1,11 @@ using System; -namespace Jellyfin.Api.Attributes +namespace Jellyfin.Api.Attributes; + +/// +/// Attribute to mark a parameter as obsolete. +/// +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class ParameterObsoleteAttribute : Attribute { - /// - /// Attribute to mark a parameter as obsolete. - /// - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class ParameterObsoleteAttribute : Attribute - { - } } diff --git a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs index 9fc25f192e..7ce09c299d 100644 --- a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs @@ -1,18 +1,17 @@ -namespace Jellyfin.Api.Attributes -{ - /// - /// Produces file attribute of "image/*". - /// - public sealed class ProducesAudioFileAttribute : ProducesFileAttribute - { - private const string ContentType = "audio/*"; +namespace Jellyfin.Api.Attributes; - /// - /// Initializes a new instance of the class. - /// - public ProducesAudioFileAttribute() - : base(ContentType) - { - } +/// +/// Produces file attribute of "image/*". +/// +public sealed class ProducesAudioFileAttribute : ProducesFileAttribute +{ + private const string ContentType = "audio/*"; + + /// + /// Initializes a new instance of the class. + /// + public ProducesAudioFileAttribute() + : base(ContentType) + { } } diff --git a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs index d8e4141acb..c728f68e07 100644 --- a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs @@ -2,29 +2,28 @@ using System; -namespace Jellyfin.Api.Attributes +namespace Jellyfin.Api.Attributes; + +/// +/// Internal produces image attribute. +/// +[AttributeUsage(AttributeTargets.Method)] +public class ProducesFileAttribute : Attribute { + private readonly string[] _contentTypes; + /// - /// Internal produces image attribute. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.Method)] - public class ProducesFileAttribute : Attribute + /// Content types this endpoint produces. + public ProducesFileAttribute(params string[] contentTypes) { - private readonly string[] _contentTypes; - - /// - /// Initializes a new instance of the class. - /// - /// Content types this endpoint produces. - public ProducesFileAttribute(params string[] contentTypes) - { - _contentTypes = contentTypes; - } - - /// - /// Gets the configured content types. - /// - /// the configured content types. - public string[] ContentTypes => _contentTypes; + _contentTypes = contentTypes; } + + /// + /// Gets the configured content types. + /// + /// the configured content types. + public string[] ContentTypes => _contentTypes; } diff --git a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs index 1e5b542e27..f145a061ee 100644 --- a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs @@ -1,18 +1,17 @@ -namespace Jellyfin.Api.Attributes -{ - /// - /// Produces file attribute of "image/*". - /// - public sealed class ProducesImageFileAttribute : ProducesFileAttribute - { - private const string ContentType = "image/*"; +namespace Jellyfin.Api.Attributes; - /// - /// Initializes a new instance of the class. - /// - public ProducesImageFileAttribute() - : base(ContentType) - { - } +/// +/// Produces file attribute of "image/*". +/// +public sealed class ProducesImageFileAttribute : ProducesFileAttribute +{ + private const string ContentType = "image/*"; + + /// + /// Initializes a new instance of the class. + /// + public ProducesImageFileAttribute() + : base(ContentType) + { } } diff --git a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs index 5b15cb1a56..c03ed740c0 100644 --- a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs @@ -1,18 +1,17 @@ -namespace Jellyfin.Api.Attributes -{ - /// - /// Produces file attribute of "image/*". - /// - public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute - { - private const string ContentType = "application/x-mpegURL"; +namespace Jellyfin.Api.Attributes; - /// - /// Initializes a new instance of the class. - /// - public ProducesPlaylistFileAttribute() - : base(ContentType) - { - } +/// +/// Produces file attribute of "image/*". +/// +public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute +{ + private const string ContentType = "application/x-mpegURL"; + + /// + /// Initializes a new instance of the class. + /// + public ProducesPlaylistFileAttribute() + : base(ContentType) + { } } diff --git a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs index 6857d45ecc..10dec0c00e 100644 --- a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs @@ -1,18 +1,17 @@ -namespace Jellyfin.Api.Attributes -{ - /// - /// Produces file attribute of "video/*". - /// - public sealed class ProducesVideoFileAttribute : ProducesFileAttribute - { - private const string ContentType = "video/*"; +namespace Jellyfin.Api.Attributes; - /// - /// Initializes a new instance of the class. - /// - public ProducesVideoFileAttribute() - : base(ContentType) - { - } +/// +/// Produces file attribute of "video/*". +/// +public sealed class ProducesVideoFileAttribute : ProducesFileAttribute +{ + private const string ContentType = "video/*"; + + /// + /// Initializes a new instance of the class. + /// + public ProducesVideoFileAttribute() + : base(ContentType) + { } } diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index e327831fe7..5b4bd0adb0 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -4,35 +4,34 @@ using Jellyfin.Api.Results; using Jellyfin.Extensions.Json; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api +namespace Jellyfin.Api; + +/// +/// Base api controller for the API setting a default route. +/// +[ApiController] +[Route("[controller]")] +[Produces( + MediaTypeNames.Application.Json, + JsonDefaults.CamelCaseMediaType, + JsonDefaults.PascalCaseMediaType)] +public class BaseJellyfinApiController : ControllerBase { /// - /// Base api controller for the API setting a default route. + /// Create a new . /// - [ApiController] - [Route("[controller]")] - [Produces( - MediaTypeNames.Application.Json, - JsonDefaults.CamelCaseMediaType, - JsonDefaults.PascalCaseMediaType)] - public class BaseJellyfinApiController : ControllerBase - { - /// - /// Create a new . - /// - /// The value to return. - /// The type to return. - /// The . - protected ActionResult> Ok(IEnumerable? value) - => new OkResult?>(value); + /// The value to return. + /// The type to return. + /// The . + protected ActionResult> Ok(IEnumerable? value) + => new OkResult?>(value); - /// - /// Create a new . - /// - /// The value to return. - /// The type to return. - /// The . - protected ActionResult Ok(T value) - => new OkResult(value); - } + /// + /// Create a new . + /// + /// The value to return. + /// The type to return. + /// The . + protected ActionResult Ok(T value) + => new OkResult(value); } diff --git a/Jellyfin.Api/Constants/AuthenticationSchemes.cs b/Jellyfin.Api/Constants/AuthenticationSchemes.cs index bac3379e71..d5c2253e4a 100644 --- a/Jellyfin.Api/Constants/AuthenticationSchemes.cs +++ b/Jellyfin.Api/Constants/AuthenticationSchemes.cs @@ -1,13 +1,12 @@ -namespace Jellyfin.Api.Constants +namespace Jellyfin.Api.Constants; + +/// +/// Authentication schemes for user authentication in the API. +/// +public static class AuthenticationSchemes { /// - /// Authentication schemes for user authentication in the API. + /// Scheme name for the custom legacy authentication. /// - public static class AuthenticationSchemes - { - /// - /// Scheme name for the custom legacy authentication. - /// - public const string CustomAuthentication = "CustomAuthentication"; - } + public const string CustomAuthentication = "CustomAuthentication"; } diff --git a/Jellyfin.Api/Constants/InternalClaimTypes.cs b/Jellyfin.Api/Constants/InternalClaimTypes.cs index 8323312e51..73c4acb882 100644 --- a/Jellyfin.Api/Constants/InternalClaimTypes.cs +++ b/Jellyfin.Api/Constants/InternalClaimTypes.cs @@ -1,43 +1,42 @@ -namespace Jellyfin.Api.Constants +namespace Jellyfin.Api.Constants; + +/// +/// Internal claim types for authorization. +/// +public static class InternalClaimTypes { /// - /// Internal claim types for authorization. + /// User Id. /// - public static class InternalClaimTypes - { - /// - /// User Id. - /// - public const string UserId = "Jellyfin-UserId"; + public const string UserId = "Jellyfin-UserId"; - /// - /// Device Id. - /// - public const string DeviceId = "Jellyfin-DeviceId"; + /// + /// Device Id. + /// + public const string DeviceId = "Jellyfin-DeviceId"; - /// - /// Device. - /// - public const string Device = "Jellyfin-Device"; + /// + /// Device. + /// + public const string Device = "Jellyfin-Device"; - /// - /// Client. - /// - public const string Client = "Jellyfin-Client"; + /// + /// Client. + /// + public const string Client = "Jellyfin-Client"; - /// - /// Version. - /// - public const string Version = "Jellyfin-Version"; + /// + /// Version. + /// + public const string Version = "Jellyfin-Version"; - /// - /// Token. - /// - public const string Token = "Jellyfin-Token"; + /// + /// Token. + /// + public const string Token = "Jellyfin-Token"; - /// - /// Is Api Key. - /// - public const string IsApiKey = "Jellyfin-IsApiKey"; - } + /// + /// Is Api Key. + /// + public const string IsApiKey = "Jellyfin-IsApiKey"; } diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index a72eeea284..5a5a2bf466 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -1,78 +1,77 @@ -namespace Jellyfin.Api.Constants +namespace Jellyfin.Api.Constants; + +/// +/// Policies for the API authorization. +/// +public static class Policies { /// - /// Policies for the API authorization. + /// Policy name for default authorization. /// - public static class Policies - { - /// - /// Policy name for default authorization. - /// - public const string DefaultAuthorization = "DefaultAuthorization"; + public const string DefaultAuthorization = "DefaultAuthorization"; - /// - /// Policy name for requiring first time setup or elevated privileges. - /// - public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated"; + /// + /// Policy name for requiring first time setup or elevated privileges. + /// + public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated"; - /// - /// Policy name for requiring elevated privileges. - /// - public const string RequiresElevation = "RequiresElevation"; + /// + /// Policy name for requiring elevated privileges. + /// + public const string RequiresElevation = "RequiresElevation"; - /// - /// Policy name for allowing local access only. - /// - public const string LocalAccessOnly = "LocalAccessOnly"; + /// + /// Policy name for allowing local access only. + /// + public const string LocalAccessOnly = "LocalAccessOnly"; - /// - /// Policy name for escaping schedule controls. - /// - public const string IgnoreParentalControl = "IgnoreParentalControl"; + /// + /// Policy name for escaping schedule controls. + /// + public const string IgnoreParentalControl = "IgnoreParentalControl"; - /// - /// Policy name for requiring download permission. - /// - public const string Download = "Download"; + /// + /// Policy name for requiring download permission. + /// + public const string Download = "Download"; - /// - /// Policy name for requiring first time setup or default permissions. - /// - public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault"; + /// + /// Policy name for requiring first time setup or default permissions. + /// + public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault"; - /// - /// Policy name for requiring local access or elevated privileges. - /// - public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation"; + /// + /// Policy name for requiring local access or elevated privileges. + /// + public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation"; - /// - /// Policy name for requiring (anonymous) LAN access. - /// - public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy"; + /// + /// Policy name for requiring (anonymous) LAN access. + /// + public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy"; - /// - /// Policy name for escaping schedule controls or requiring first time setup. - /// - public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl"; + /// + /// Policy name for escaping schedule controls or requiring first time setup. + /// + public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl"; - /// - /// Policy name for accessing SyncPlay. - /// - public const string SyncPlayHasAccess = "SyncPlayHasAccess"; + /// + /// Policy name for accessing SyncPlay. + /// + public const string SyncPlayHasAccess = "SyncPlayHasAccess"; - /// - /// Policy name for creating a SyncPlay group. - /// - public const string SyncPlayCreateGroup = "SyncPlayCreateGroup"; + /// + /// Policy name for creating a SyncPlay group. + /// + public const string SyncPlayCreateGroup = "SyncPlayCreateGroup"; - /// - /// Policy name for joining a SyncPlay group. - /// - public const string SyncPlayJoinGroup = "SyncPlayJoinGroup"; + /// + /// Policy name for joining a SyncPlay group. + /// + public const string SyncPlayJoinGroup = "SyncPlayJoinGroup"; - /// - /// Policy name for accessing a SyncPlay group. - /// - public const string SyncPlayIsInGroup = "SyncPlayIsInGroup"; - } + /// + /// Policy name for accessing a SyncPlay group. + /// + public const string SyncPlayIsInGroup = "SyncPlayIsInGroup"; } diff --git a/Jellyfin.Api/Constants/UserRoles.cs b/Jellyfin.Api/Constants/UserRoles.cs index d9a536e7d7..41c7b7cd0f 100644 --- a/Jellyfin.Api/Constants/UserRoles.cs +++ b/Jellyfin.Api/Constants/UserRoles.cs @@ -1,23 +1,22 @@ -namespace Jellyfin.Api.Constants +namespace Jellyfin.Api.Constants; + +/// +/// Constants for user roles used in the authentication and authorization for the API. +/// +public static class UserRoles { /// - /// Constants for user roles used in the authentication and authorization for the API. + /// Guest user. /// - public static class UserRoles - { - /// - /// Guest user. - /// - public const string Guest = "Guest"; + public const string Guest = "Guest"; - /// - /// Regular user with no special privileges. - /// - public const string User = "User"; + /// + /// Regular user with no special privileges. + /// + public const string User = "User"; - /// - /// Administrator user with elevated privileges. - /// - public const string Administrator = "Administrator"; - } + /// + /// Administrator user with elevated privileges. + /// + public const string Administrator = "Administrator"; } diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index ae45f647f7..c3d02976eb 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -8,50 +8,49 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Activity log controller. +/// +[Route("System/ActivityLog")] +[Authorize(Policy = Policies.RequiresElevation)] +public class ActivityLogController : BaseJellyfinApiController { + private readonly IActivityManager _activityManager; + /// - /// Activity log controller. + /// Initializes a new instance of the class. /// - [Route("System/ActivityLog")] - [Authorize(Policy = Policies.RequiresElevation)] - public class ActivityLogController : BaseJellyfinApiController + /// Instance of interface. + public ActivityLogController(IActivityManager activityManager) { - private readonly IActivityManager _activityManager; + _activityManager = activityManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - public ActivityLogController(IActivityManager activityManager) + /// + /// Gets activity log entries. + /// + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. The minimum date. Format = ISO. + /// Optional. Filter log entries if it has user id, or not. + /// Activity log returned. + /// A containing the log entries. + [HttpGet("Entries")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetLogEntries( + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] DateTime? minDate, + [FromQuery] bool? hasUserId) + { + return await _activityManager.GetPagedResultAsync(new ActivityLogQuery { - _activityManager = activityManager; - } - - /// - /// Gets activity log entries. - /// - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. The minimum date. Format = ISO. - /// Optional. Filter log entries if it has user id, or not. - /// Activity log returned. - /// A containing the log entries. - [HttpGet("Entries")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> GetLogEntries( - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] DateTime? minDate, - [FromQuery] bool? hasUserId) - { - return await _activityManager.GetPagedResultAsync(new ActivityLogQuery - { - Skip = startIndex, - Limit = limit, - MinDate = minDate, - HasUserId = hasUserId - }).ConfigureAwait(false); - } + Skip = startIndex, + Limit = limit, + MinDate = minDate, + HasUserId = hasUserId + }).ConfigureAwait(false); } } diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index 024a15349e..991f8cbf20 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -7,70 +7,69 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Authentication controller. +/// +[Route("Auth")] +public class ApiKeyController : BaseJellyfinApiController { + private readonly IAuthenticationManager _authenticationManager; + /// - /// Authentication controller. + /// Initializes a new instance of the class. /// - [Route("Auth")] - public class ApiKeyController : BaseJellyfinApiController + /// Instance of interface. + public ApiKeyController(IAuthenticationManager authenticationManager) { - private readonly IAuthenticationManager _authenticationManager; + _authenticationManager = authenticationManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - public ApiKeyController(IAuthenticationManager authenticationManager) - { - _authenticationManager = authenticationManager; - } + /// + /// Get all keys. + /// + /// Api keys retrieved. + /// A with all keys. + [HttpGet("Keys")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetKeys() + { + var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false); - /// - /// Get all keys. - /// - /// Api keys retrieved. - /// A with all keys. - [HttpGet("Keys")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> GetKeys() - { - var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false); + return new QueryResult(keys); + } - return new QueryResult(keys); - } + /// + /// Create a new api key. + /// + /// Name of the app using the authentication key. + /// Api key created. + /// A . + [HttpPost("Keys")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task CreateKey([FromQuery, Required] string app) + { + await _authenticationManager.CreateApiKey(app).ConfigureAwait(false); - /// - /// Create a new api key. - /// - /// Name of the app using the authentication key. - /// Api key created. - /// A . - [HttpPost("Keys")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task CreateKey([FromQuery, Required] string app) - { - await _authenticationManager.CreateApiKey(app).ConfigureAwait(false); + return NoContent(); + } - return NoContent(); - } + /// + /// Remove an api key. + /// + /// The access token to delete. + /// Api key deleted. + /// A . + [HttpDelete("Keys/{key}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task RevokeKey([FromRoute, Required] string key) + { + await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false); - /// - /// Remove an api key. - /// - /// The access token to delete. - /// Api key deleted. - /// A . - [HttpDelete("Keys/{key}")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task RevokeKey([FromRoute, Required] string key) - { - await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false); - - return NoContent(); - } + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index c8ac2ed526..069e7311b8 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -17,464 +17,463 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The artists controller. +/// +[Route("Artists")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class ArtistsController : BaseJellyfinApiController { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; + /// - /// The artists controller. + /// Initializes a new instance of the class. /// - [Route("Artists")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class ArtistsController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ArtistsController( + ILibraryManager libraryManager, + IUserManager userManager, + IDtoService dtoService) { - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDtoService _dtoService; + _libraryManager = libraryManager; + _userManager = userManager; + _dtoService = dtoService; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public ArtistsController( - ILibraryManager libraryManager, - IUserManager userManager, - IDtoService dtoService) + /// + /// Gets all artists from a given item, folder, or the entire library. + /// + /// Optional filter by minimum community rating. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Search term. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. Specify additional filters to apply. + /// Optional filter by items that are marked as favorite, or not. + /// Optional filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person ids. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. + /// User id. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional. Specify one or more sort orders, comma delimited. + /// Sort Order - Ascending,Descending. + /// Optional, include image information in output. + /// Total record count. + /// Artists returned. + /// An containing the artists. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetArtists( + [FromQuery] double? minCommunityRating, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string? searchTerm, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery] bool? isFavorite, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] string? person, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, + [FromQuery] Guid? userId, + [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) + { + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + User? user = null; + BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); + + if (userId.HasValue && !userId.Equals(default)) { - _libraryManager = libraryManager; - _userManager = userManager; - _dtoService = dtoService; + user = _userManager.GetUserById(userId.Value); } - /// - /// Gets all artists from a given item, folder, or the entire library. - /// - /// Optional filter by minimum community rating. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Search term. - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional. Specify additional filters to apply. - /// Optional filter by items that are marked as favorite, or not. - /// Optional filter by MediaType. Allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. - /// Optional, include user data. - /// Optional, the max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. If specified, results will be filtered to include only those containing the specified person. - /// Optional. If specified, results will be filtered to include only those containing the specified person ids. - /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. - /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. - /// User id. - /// Optional filter by items whose name is sorted equally or greater than a given input string. - /// Optional filter by items whose name is sorted equally than a given input string. - /// Optional filter by items whose name is equally or lesser than a given input string. - /// Optional. Specify one or more sort orders, comma delimited. - /// Sort Order - Ascending,Descending. - /// Optional, include image information in output. - /// Total record count. - /// Artists returned. - /// An containing the artists. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetArtists( - [FromQuery] double? minCommunityRating, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] string? searchTerm, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, - [FromQuery] bool? enableUserData, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? person, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, - [FromQuery] Guid? userId, - [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) + var query = new InternalItemsQuery(user) { - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + MediaTypes = mediaTypes, + StartIndex = startIndex, + Limit = limit, + IsFavorite = isFavorite, + NameLessThan = nameLessThan, + NameStartsWith = nameStartsWith, + NameStartsWithOrGreater = nameStartsWithOrGreater, + Tags = tags, + OfficialRatings = officialRatings, + Genres = genres, + GenreIds = genreIds, + StudioIds = studioIds, + Person = person, + PersonIds = personIds, + PersonTypes = personTypes, + Years = years, + MinCommunityRating = minCommunityRating, + DtoOptions = dtoOptions, + SearchTerm = searchTerm, + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) + }; - User? user = null; - BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - - if (userId.HasValue && !userId.Equals(default)) + if (parentId.HasValue) + { + if (parentItem is Folder) { - user = _userManager.GetUserById(userId.Value); + query.AncestorIds = new[] { parentId.Value }; } - - var query = new InternalItemsQuery(user) + else { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, - MediaTypes = mediaTypes, - StartIndex = startIndex, - Limit = limit, - IsFavorite = isFavorite, - NameLessThan = nameLessThan, - NameStartsWith = nameStartsWith, - NameStartsWithOrGreater = nameStartsWithOrGreater, - Tags = tags, - OfficialRatings = officialRatings, - Genres = genres, - GenreIds = genreIds, - StudioIds = studioIds, - Person = person, - PersonIds = personIds, - PersonTypes = personTypes, - Years = years, - MinCommunityRating = minCommunityRating, - DtoOptions = dtoOptions, - SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount, - OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) - }; - - if (parentId.HasValue) - { - if (parentItem is Folder) - { - query.AncestorIds = new[] { parentId.Value }; - } - else - { - query.ItemIds = new[] { parentId.Value }; - } + query.ItemIds = new[] { parentId.Value }; } - - // Studios - if (studios.Length != 0) - { - query.StudioIds = studios.Select(i => - { - try - { - return _libraryManager.GetStudio(i); - } - catch - { - return null; - } - }).Where(i => i is not null).Select(i => i!.Id).ToArray(); - } - - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - } - } - - var result = _libraryManager.GetArtists(query); - - var dtos = result.Items.Select(i => - { - var (baseItem, itemCounts) = i; - var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); - - if (includeItemTypes.Length != 0) - { - dto.ChildCount = itemCounts.ItemCount; - dto.ProgramCount = itemCounts.ProgramCount; - dto.SeriesCount = itemCounts.SeriesCount; - dto.EpisodeCount = itemCounts.EpisodeCount; - dto.MovieCount = itemCounts.MovieCount; - dto.TrailerCount = itemCounts.TrailerCount; - dto.AlbumCount = itemCounts.AlbumCount; - dto.SongCount = itemCounts.SongCount; - dto.ArtistCount = itemCounts.ArtistCount; - } - - return dto; - }); - - return new QueryResult( - query.StartIndex, - result.TotalRecordCount, - dtos.ToArray()); } - /// - /// Gets all album artists from a given item, folder, or the entire library. - /// - /// Optional filter by minimum community rating. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Search term. - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional. Specify additional filters to apply. - /// Optional filter by items that are marked as favorite, or not. - /// Optional filter by MediaType. Allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. - /// Optional, include user data. - /// Optional, the max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. If specified, results will be filtered to include only those containing the specified person. - /// Optional. If specified, results will be filtered to include only those containing the specified person ids. - /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. - /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. - /// User id. - /// Optional filter by items whose name is sorted equally or greater than a given input string. - /// Optional filter by items whose name is sorted equally than a given input string. - /// Optional filter by items whose name is equally or lesser than a given input string. - /// Optional. Specify one or more sort orders, comma delimited. - /// Sort Order - Ascending,Descending. - /// Optional, include image information in output. - /// Total record count. - /// Album artists returned. - /// An containing the album artists. - [HttpGet("AlbumArtists")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetAlbumArtists( - [FromQuery] double? minCommunityRating, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] string? searchTerm, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, - [FromQuery] bool? enableUserData, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? person, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, - [FromQuery] Guid? userId, - [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) + // Studios + if (studios.Length != 0) { - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - - User? user = null; - BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - - if (userId.HasValue && !userId.Equals(default)) + query.StudioIds = studios.Select(i => { - user = _userManager.GetUserById(userId.Value); - } - - var query = new InternalItemsQuery(user) - { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, - MediaTypes = mediaTypes, - StartIndex = startIndex, - Limit = limit, - IsFavorite = isFavorite, - NameLessThan = nameLessThan, - NameStartsWith = nameStartsWith, - NameStartsWithOrGreater = nameStartsWithOrGreater, - Tags = tags, - OfficialRatings = officialRatings, - Genres = genres, - GenreIds = genreIds, - StudioIds = studioIds, - Person = person, - PersonIds = personIds, - PersonTypes = personTypes, - Years = years, - MinCommunityRating = minCommunityRating, - DtoOptions = dtoOptions, - SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount, - OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) - }; - - if (parentId.HasValue) - { - if (parentItem is Folder) + try { - query.AncestorIds = new[] { parentId.Value }; + return _libraryManager.GetStudio(i); } - else + catch { - query.ItemIds = new[] { parentId.Value }; + return null; } - } - - // Studios - if (studios.Length != 0) - { - query.StudioIds = studios.Select(i => - { - try - { - return _libraryManager.GetStudio(i); - } - catch - { - return null; - } - }).Where(i => i is not null).Select(i => i!.Id).ToArray(); - } - - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - } - } - - var result = _libraryManager.GetAlbumArtists(query); - - var dtos = result.Items.Select(i => - { - var (baseItem, itemCounts) = i; - var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); - - if (includeItemTypes.Length != 0) - { - dto.ChildCount = itemCounts.ItemCount; - dto.ProgramCount = itemCounts.ProgramCount; - dto.SeriesCount = itemCounts.SeriesCount; - dto.EpisodeCount = itemCounts.EpisodeCount; - dto.MovieCount = itemCounts.MovieCount; - dto.TrailerCount = itemCounts.TrailerCount; - dto.AlbumCount = itemCounts.AlbumCount; - dto.SongCount = itemCounts.SongCount; - dto.ArtistCount = itemCounts.ArtistCount; - } - - return dto; - }); - - return new QueryResult( - query.StartIndex, - result.TotalRecordCount, - dtos.ToArray()); + }).Where(i => i is not null).Select(i => i!.Id).ToArray(); } - /// - /// Gets an artist by name. - /// - /// Studio name. - /// Optional. Filter by user id, and attach user data. - /// Artist returned. - /// An containing the artist. - [HttpGet("{name}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId) + foreach (var filter in filters) { - var dtoOptions = new DtoOptions().AddClientFields(User); - - var item = _libraryManager.GetArtist(name, dtoOptions); - - if (userId.HasValue && !userId.Value.Equals(default)) + switch (filter) { - var user = _userManager.GetUserById(userId.Value); + case ItemFilter.Dislikes: + query.IsLiked = false; + break; + case ItemFilter.IsFavorite: + query.IsFavorite = true; + break; + case ItemFilter.IsFavoriteOrLikes: + query.IsFavoriteOrLiked = true; + break; + case ItemFilter.IsFolder: + query.IsFolder = true; + break; + case ItemFilter.IsNotFolder: + query.IsFolder = false; + break; + case ItemFilter.IsPlayed: + query.IsPlayed = true; + break; + case ItemFilter.IsResumable: + query.IsResumable = true; + break; + case ItemFilter.IsUnplayed: + query.IsPlayed = false; + break; + case ItemFilter.Likes: + query.IsLiked = true; + break; + } + } - return _dtoService.GetBaseItemDto(item, dtoOptions, user); + var result = _libraryManager.GetArtists(query); + + var dtos = result.Items.Select(i => + { + var (baseItem, itemCounts) = i; + var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); + + if (includeItemTypes.Length != 0) + { + dto.ChildCount = itemCounts.ItemCount; + dto.ProgramCount = itemCounts.ProgramCount; + dto.SeriesCount = itemCounts.SeriesCount; + dto.EpisodeCount = itemCounts.EpisodeCount; + dto.MovieCount = itemCounts.MovieCount; + dto.TrailerCount = itemCounts.TrailerCount; + dto.AlbumCount = itemCounts.AlbumCount; + dto.SongCount = itemCounts.SongCount; + dto.ArtistCount = itemCounts.ArtistCount; } - return _dtoService.GetBaseItemDto(item, dtoOptions); + return dto; + }); + + return new QueryResult( + query.StartIndex, + result.TotalRecordCount, + dtos.ToArray()); + } + + /// + /// Gets all album artists from a given item, folder, or the entire library. + /// + /// Optional filter by minimum community rating. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Search term. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. Specify additional filters to apply. + /// Optional filter by items that are marked as favorite, or not. + /// Optional filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person ids. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. + /// User id. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional. Specify one or more sort orders, comma delimited. + /// Sort Order - Ascending,Descending. + /// Optional, include image information in output. + /// Total record count. + /// Album artists returned. + /// An containing the album artists. + [HttpGet("AlbumArtists")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetAlbumArtists( + [FromQuery] double? minCommunityRating, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string? searchTerm, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery] bool? isFavorite, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] string? person, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, + [FromQuery] Guid? userId, + [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) + { + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + User? user = null; + BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); + + if (userId.HasValue && !userId.Equals(default)) + { + user = _userManager.GetUserById(userId.Value); } + + var query = new InternalItemsQuery(user) + { + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + MediaTypes = mediaTypes, + StartIndex = startIndex, + Limit = limit, + IsFavorite = isFavorite, + NameLessThan = nameLessThan, + NameStartsWith = nameStartsWith, + NameStartsWithOrGreater = nameStartsWithOrGreater, + Tags = tags, + OfficialRatings = officialRatings, + Genres = genres, + GenreIds = genreIds, + StudioIds = studioIds, + Person = person, + PersonIds = personIds, + PersonTypes = personTypes, + Years = years, + MinCommunityRating = minCommunityRating, + DtoOptions = dtoOptions, + SearchTerm = searchTerm, + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) + }; + + if (parentId.HasValue) + { + if (parentItem is Folder) + { + query.AncestorIds = new[] { parentId.Value }; + } + else + { + query.ItemIds = new[] { parentId.Value }; + } + } + + // Studios + if (studios.Length != 0) + { + query.StudioIds = studios.Select(i => + { + try + { + return _libraryManager.GetStudio(i); + } + catch + { + return null; + } + }).Where(i => i is not null).Select(i => i!.Id).ToArray(); + } + + foreach (var filter in filters) + { + switch (filter) + { + case ItemFilter.Dislikes: + query.IsLiked = false; + break; + case ItemFilter.IsFavorite: + query.IsFavorite = true; + break; + case ItemFilter.IsFavoriteOrLikes: + query.IsFavoriteOrLiked = true; + break; + case ItemFilter.IsFolder: + query.IsFolder = true; + break; + case ItemFilter.IsNotFolder: + query.IsFolder = false; + break; + case ItemFilter.IsPlayed: + query.IsPlayed = true; + break; + case ItemFilter.IsResumable: + query.IsResumable = true; + break; + case ItemFilter.IsUnplayed: + query.IsPlayed = false; + break; + case ItemFilter.Likes: + query.IsLiked = true; + break; + } + } + + var result = _libraryManager.GetAlbumArtists(query); + + var dtos = result.Items.Select(i => + { + var (baseItem, itemCounts) = i; + var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); + + if (includeItemTypes.Length != 0) + { + dto.ChildCount = itemCounts.ItemCount; + dto.ProgramCount = itemCounts.ProgramCount; + dto.SeriesCount = itemCounts.SeriesCount; + dto.EpisodeCount = itemCounts.EpisodeCount; + dto.MovieCount = itemCounts.MovieCount; + dto.TrailerCount = itemCounts.TrailerCount; + dto.AlbumCount = itemCounts.AlbumCount; + dto.SongCount = itemCounts.SongCount; + dto.ArtistCount = itemCounts.ArtistCount; + } + + return dto; + }); + + return new QueryResult( + query.StartIndex, + result.TotalRecordCount, + dtos.ToArray()); + } + + /// + /// Gets an artist by name. + /// + /// Studio name. + /// Optional. Filter by user id, and attach user data. + /// Artist returned. + /// An containing the artist. + [HttpGet("{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId) + { + var dtoOptions = new DtoOptions().AddClientFields(User); + + var item = _libraryManager.GetArtist(name, dtoOptions); + + if (userId.HasValue && !userId.Value.Equals(default)) + { + var user = _userManager.GetUserById(userId.Value); + + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + return _dtoService.GetBaseItemDto(item, dtoOptions); } } diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 94f7a7b827..968193a6f8 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -10,355 +10,354 @@ using MediaBrowser.Model.Dlna; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The audio controller. +/// +// TODO: In order to authenticate this in the future, Dlna playback will require updating +public class AudioController : BaseJellyfinApiController { + private readonly AudioHelper _audioHelper; + + private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; + /// - /// The audio controller. + /// Initializes a new instance of the class. /// - // TODO: In order to authenticate this in the future, Dlna playback will require updating - public class AudioController : BaseJellyfinApiController + /// Instance of . + public AudioController(AudioHelper audioHelper) { - private readonly AudioHelper _audioHelper; + _audioHelper = audioHelper; + } - private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of . - public AudioController(AudioHelper audioHelper) + /// + /// Gets an audio stream. + /// + /// The item id. + /// The audio container. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment length. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamorphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// 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. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Audio stream returned. + /// A containing the audio file. + [HttpGet("{itemId}/stream", Name = "GetAudioStream")] + [HttpHead("{itemId}/stream", Name = "HeadAudioStream")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesAudioFile] + public async Task GetAudioStream( + [FromRoute, Required] Guid itemId, + [FromQuery] string? container, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodeReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary? streamOptions) + { + StreamingRequestDto streamingRequest = new StreamingRequestDto { - _audioHelper = audioHelper; - } + Id = itemId, + Container = container, + Static = @static ?? false, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? false, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodeReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Static, + StreamOptions = streamOptions + }; - /// - /// Gets an audio stream. - /// - /// The item id. - /// The audio container. - /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. - /// The streaming parameters. - /// The tag. - /// Optional. The dlna device profile id to utilize. - /// The play session id. - /// The segment container. - /// The segment length. - /// The minimum number of segments. - /// The media version id, if playing an alternate version. - /// The device id of the client requesting. Used to stop encoding processes when needed. - /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. - /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. - /// Whether or not to allow copying of the video stream url. - /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. - /// Optional. Specify a specific audio sample rate, e.g. 44100. - /// Optional. The maximum audio bit depth. - /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. - /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. - /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. - /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. - /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. - /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. - /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. - /// Optional. The fixed horizontal resolution of the encoded video. - /// Optional. The fixed vertical resolution of the encoded video. - /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. - /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. - /// Optional. Specify the subtitle delivery method. - /// Optional. - /// Optional. The maximum video bit depth. - /// Optional. Whether to require avc. - /// Optional. Whether to deinterlace the video. - /// Optional. Whether to require a non anamorphic stream. - /// Optional. The maximum number of audio channels to transcode. - /// Optional. The limit of how many cpu cores to use. - /// The live stream id. - /// Optional. Whether to enable the MpegtsM2Ts mode. - /// 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. - /// Optional. Specify a subtitle codec to encode to. - /// Optional. The transcoding reason. - /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. - /// Optional. The index of the video stream to use. If omitted the first video stream will be used. - /// Optional. The . - /// Optional. The streaming options. - /// Audio stream returned. - /// A containing the audio file. - [HttpGet("{itemId}/stream", Name = "GetAudioStream")] - [HttpHead("{itemId}/stream", Name = "HeadAudioStream")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesAudioFile] - public async Task GetAudioStream( - [FromRoute, Required] Guid itemId, - [FromQuery] string? container, - [FromQuery] bool? @static, - [FromQuery] string? @params, - [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, - [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, - [FromQuery] int? segmentLength, - [FromQuery] int? minSegments, - [FromQuery] string? mediaSourceId, - [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, - [FromQuery] bool? enableAutoStreamCopy, - [FromQuery] bool? allowVideoStreamCopy, - [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, - [FromQuery] int? audioSampleRate, - [FromQuery] int? maxAudioBitDepth, - [FromQuery] int? audioBitRate, - [FromQuery] int? audioChannels, - [FromQuery] int? maxAudioChannels, - [FromQuery] string? profile, - [FromQuery] string? level, - [FromQuery] float? framerate, - [FromQuery] float? maxFramerate, - [FromQuery] bool? copyTimestamps, - [FromQuery] long? startTimeTicks, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? videoBitRate, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod? subtitleMethod, - [FromQuery] int? maxRefFrames, - [FromQuery] int? maxVideoBitDepth, - [FromQuery] bool? requireAvc, - [FromQuery] bool? deInterlace, - [FromQuery] bool? requireNonAnamorphic, - [FromQuery] int? transcodingMaxAudioChannels, - [FromQuery] int? cpuCoreLimit, - [FromQuery] string? liveStreamId, - [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodeReasons, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext? context, - [FromQuery] Dictionary? streamOptions) + return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false); + } + + /// + /// Gets an audio stream. + /// + /// The item id. + /// The audio container. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment length. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamporphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// 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. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Audio stream returned. + /// A containing the audio file. + [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")] + [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesAudioFile] + public async Task GetAudioStreamByContainer( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] string container, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodeReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary? streamOptions) + { + StreamingRequestDto streamingRequest = new StreamingRequestDto { - StreamingRequestDto streamingRequest = new StreamingRequestDto - { - Id = itemId, - Container = container, - Static = @static ?? false, - Params = @params, - Tag = tag, - DeviceProfileId = deviceProfileId, - PlaySessionId = playSessionId, - SegmentContainer = segmentContainer, - SegmentLength = segmentLength, - MinSegments = minSegments, - MediaSourceId = mediaSourceId, - DeviceId = deviceId, - AudioCodec = audioCodec, - EnableAutoStreamCopy = enableAutoStreamCopy ?? true, - AllowAudioStreamCopy = allowAudioStreamCopy ?? true, - AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, - AudioSampleRate = audioSampleRate, - MaxAudioChannels = maxAudioChannels, - AudioBitRate = audioBitRate, - MaxAudioBitDepth = maxAudioBitDepth, - AudioChannels = audioChannels, - Profile = profile, - Level = level, - Framerate = framerate, - MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? false, - StartTimeTicks = startTimeTicks, - Width = width, - Height = height, - VideoBitRate = videoBitRate, - SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, - MaxRefFrames = maxRefFrames, - MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? false, - DeInterlace = deInterlace ?? false, - RequireNonAnamorphic = requireNonAnamorphic ?? false, - TranscodingMaxAudioChannels = transcodingMaxAudioChannels, - CpuCoreLimit = cpuCoreLimit, - LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, - VideoCodec = videoCodec, - SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodeReasons, - AudioStreamIndex = audioStreamIndex, - VideoStreamIndex = videoStreamIndex, - Context = context ?? EncodingContext.Static, - StreamOptions = streamOptions - }; + Id = itemId, + Container = container, + Static = @static ?? false, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? false, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodeReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Static, + StreamOptions = streamOptions + }; - return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false); - } - - /// - /// Gets an audio stream. - /// - /// The item id. - /// The audio container. - /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. - /// The streaming parameters. - /// The tag. - /// Optional. The dlna device profile id to utilize. - /// The play session id. - /// The segment container. - /// The segment length. - /// The minimum number of segments. - /// The media version id, if playing an alternate version. - /// The device id of the client requesting. Used to stop encoding processes when needed. - /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. - /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. - /// Whether or not to allow copying of the video stream url. - /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. - /// Optional. Specify a specific audio sample rate, e.g. 44100. - /// Optional. The maximum audio bit depth. - /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. - /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. - /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. - /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. - /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. - /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. - /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. - /// Optional. The fixed horizontal resolution of the encoded video. - /// Optional. The fixed vertical resolution of the encoded video. - /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. - /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. - /// Optional. Specify the subtitle delivery method. - /// Optional. - /// Optional. The maximum video bit depth. - /// Optional. Whether to require avc. - /// Optional. Whether to deinterlace the video. - /// Optional. Whether to require a non anamporphic stream. - /// Optional. The maximum number of audio channels to transcode. - /// Optional. The limit of how many cpu cores to use. - /// The live stream id. - /// Optional. Whether to enable the MpegtsM2Ts mode. - /// 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. - /// Optional. Specify a subtitle codec to encode to. - /// Optional. The transcoding reason. - /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. - /// Optional. The index of the video stream to use. If omitted the first video stream will be used. - /// Optional. The . - /// Optional. The streaming options. - /// Audio stream returned. - /// A containing the audio file. - [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")] - [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesAudioFile] - public async Task GetAudioStreamByContainer( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] string container, - [FromQuery] bool? @static, - [FromQuery] string? @params, - [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, - [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, - [FromQuery] int? segmentLength, - [FromQuery] int? minSegments, - [FromQuery] string? mediaSourceId, - [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, - [FromQuery] bool? enableAutoStreamCopy, - [FromQuery] bool? allowVideoStreamCopy, - [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, - [FromQuery] int? audioSampleRate, - [FromQuery] int? maxAudioBitDepth, - [FromQuery] int? audioBitRate, - [FromQuery] int? audioChannels, - [FromQuery] int? maxAudioChannels, - [FromQuery] string? profile, - [FromQuery] string? level, - [FromQuery] float? framerate, - [FromQuery] float? maxFramerate, - [FromQuery] bool? copyTimestamps, - [FromQuery] long? startTimeTicks, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? videoBitRate, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod? subtitleMethod, - [FromQuery] int? maxRefFrames, - [FromQuery] int? maxVideoBitDepth, - [FromQuery] bool? requireAvc, - [FromQuery] bool? deInterlace, - [FromQuery] bool? requireNonAnamorphic, - [FromQuery] int? transcodingMaxAudioChannels, - [FromQuery] int? cpuCoreLimit, - [FromQuery] string? liveStreamId, - [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodeReasons, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext? context, - [FromQuery] Dictionary? streamOptions) - { - StreamingRequestDto streamingRequest = new StreamingRequestDto - { - Id = itemId, - Container = container, - Static = @static ?? false, - Params = @params, - Tag = tag, - DeviceProfileId = deviceProfileId, - PlaySessionId = playSessionId, - SegmentContainer = segmentContainer, - SegmentLength = segmentLength, - MinSegments = minSegments, - MediaSourceId = mediaSourceId, - DeviceId = deviceId, - AudioCodec = audioCodec, - EnableAutoStreamCopy = enableAutoStreamCopy ?? true, - AllowAudioStreamCopy = allowAudioStreamCopy ?? true, - AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, - AudioSampleRate = audioSampleRate, - MaxAudioChannels = maxAudioChannels, - AudioBitRate = audioBitRate, - MaxAudioBitDepth = maxAudioBitDepth, - AudioChannels = audioChannels, - Profile = profile, - Level = level, - Framerate = framerate, - MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? false, - StartTimeTicks = startTimeTicks, - Width = width, - Height = height, - VideoBitRate = videoBitRate, - SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, - MaxRefFrames = maxRefFrames, - MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? false, - DeInterlace = deInterlace ?? false, - RequireNonAnamorphic = requireNonAnamorphic ?? false, - TranscodingMaxAudioChannels = transcodingMaxAudioChannels, - CpuCoreLimit = cpuCoreLimit, - LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, - VideoCodec = videoCodec, - SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodeReasons, - AudioStreamIndex = audioStreamIndex, - VideoStreamIndex = videoStreamIndex, - Context = context ?? EncodingContext.Static, - StreamOptions = streamOptions - }; - - return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false); - } + return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false); } } diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs index d3ea412015..3c2c4b4dbd 100644 --- a/Jellyfin.Api/Controllers/BrandingController.cs +++ b/Jellyfin.Api/Controllers/BrandingController.cs @@ -4,54 +4,53 @@ using MediaBrowser.Model.Branding; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Branding controller. +/// +public class BrandingController : BaseJellyfinApiController { + private readonly IServerConfigurationManager _serverConfigurationManager; + /// - /// Branding controller. + /// Initializes a new instance of the class. /// - public class BrandingController : BaseJellyfinApiController + /// Instance of the interface. + public BrandingController(IServerConfigurationManager serverConfigurationManager) { - private readonly IServerConfigurationManager _serverConfigurationManager; + _serverConfigurationManager = serverConfigurationManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - public BrandingController(IServerConfigurationManager serverConfigurationManager) - { - _serverConfigurationManager = serverConfigurationManager; - } + /// + /// Gets branding configuration. + /// + /// Branding configuration returned. + /// An containing the branding configuration. + [HttpGet("Configuration")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetBrandingOptions() + { + return _serverConfigurationManager.GetConfiguration("branding"); + } - /// - /// Gets branding configuration. - /// - /// Branding configuration returned. - /// An containing the branding configuration. - [HttpGet("Configuration")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetBrandingOptions() - { - return _serverConfigurationManager.GetConfiguration("branding"); - } - - /// - /// Gets branding css. - /// - /// Branding css returned. - /// No branding css configured. - /// - /// An containing the branding css if exist, - /// or a if the css is not configured. - /// - [HttpGet("Css")] - [HttpGet("Css.css", Name = "GetBrandingCss_2")] - [Produces("text/css")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult GetBrandingCss() - { - var options = _serverConfigurationManager.GetConfiguration("branding"); - return options.CustomCss ?? string.Empty; - } + /// + /// Gets branding css. + /// + /// Branding css returned. + /// No branding css configured. + /// + /// An containing the branding css if exist, + /// or a if the css is not configured. + /// + [HttpGet("Css")] + [HttpGet("Css.css", Name = "GetBrandingCss_2")] + [Produces("text/css")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult GetBrandingCss() + { + var options = _serverConfigurationManager.GetConfiguration("branding"); + return options.CustomCss ?? string.Empty; } } diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index d5b589a3fa..573b7069c5 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -18,234 +18,233 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Channels Controller. +/// +[Authorize(Policy = Policies.DefaultAuthorization)] +public class ChannelsController : BaseJellyfinApiController { + private readonly IChannelManager _channelManager; + private readonly IUserManager _userManager; + /// - /// Channels Controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.DefaultAuthorization)] - public class ChannelsController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + public ChannelsController(IChannelManager channelManager, IUserManager userManager) { - private readonly IChannelManager _channelManager; - private readonly IUserManager _userManager; + _channelManager = channelManager; + _userManager = userManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public ChannelsController(IChannelManager channelManager, IUserManager userManager) + /// + /// Gets available channels. + /// + /// User Id to filter by. Use to not filter by user. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Filter by channels that support getting latest items. + /// Optional. Filter by channels that support media deletion. + /// Optional. Filter by channels that are favorite. + /// Channels returned. + /// An containing the channels. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetChannels( + [FromQuery] Guid? userId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] bool? supportsLatestItems, + [FromQuery] bool? supportsMediaDeletion, + [FromQuery] bool? isFavorite) + { + return _channelManager.GetChannels(new ChannelQuery { - _channelManager = channelManager; - _userManager = userManager; - } + Limit = limit, + StartIndex = startIndex, + UserId = userId ?? Guid.Empty, + SupportsLatestItems = supportsLatestItems, + SupportsMediaDeletion = supportsMediaDeletion, + IsFavorite = isFavorite + }); + } - /// - /// Gets available channels. - /// - /// User Id to filter by. Use to not filter by user. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Filter by channels that support getting latest items. - /// Optional. Filter by channels that support media deletion. - /// Optional. Filter by channels that are favorite. - /// Channels returned. - /// An containing the channels. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetChannels( - [FromQuery] Guid? userId, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] bool? supportsLatestItems, - [FromQuery] bool? supportsMediaDeletion, - [FromQuery] bool? isFavorite) + /// + /// Get all channel features. + /// + /// All channel features returned. + /// An containing the channel features. + [HttpGet("Features")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetAllChannelFeatures() + { + return _channelManager.GetAllChannelFeatures(); + } + + /// + /// Get channel features. + /// + /// Channel id. + /// Channel features returned. + /// An containing the channel features. + [HttpGet("{channelId}/Features")] + public ActionResult GetChannelFeatures([FromRoute, Required] Guid channelId) + { + return _channelManager.GetChannelFeatures(channelId); + } + + /// + /// Get channel items. + /// + /// Channel Id. + /// Optional. Folder Id. + /// Optional. User Id. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Sort Order - Ascending,Descending. + /// Optional. Specify additional filters to apply. + /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. + /// Optional. Specify additional fields of information to return in the output. + /// Channel items returned. + /// + /// A representing the request to get the channel items. + /// The task result contains an containing the channel items. + /// + [HttpGet("{channelId}/Items")] + public async Task>> GetChannelItems( + [FromRoute, Required] Guid channelId, + [FromQuery] Guid? folderId, + [FromQuery] Guid? userId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var query = new InternalItemsQuery(user) { - return _channelManager.GetChannels(new ChannelQuery + Limit = limit, + StartIndex = startIndex, + ChannelIds = new[] { channelId }, + ParentId = folderId ?? Guid.Empty, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), + DtoOptions = new DtoOptions { Fields = fields } + }; + + foreach (var filter in filters) + { + switch (filter) { - Limit = limit, - StartIndex = startIndex, - UserId = userId ?? Guid.Empty, - SupportsLatestItems = supportsLatestItems, - SupportsMediaDeletion = supportsMediaDeletion, - IsFavorite = isFavorite - }); - } - - /// - /// Get all channel features. - /// - /// All channel features returned. - /// An containing the channel features. - [HttpGet("Features")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetAllChannelFeatures() - { - return _channelManager.GetAllChannelFeatures(); - } - - /// - /// Get channel features. - /// - /// Channel id. - /// Channel features returned. - /// An containing the channel features. - [HttpGet("{channelId}/Features")] - public ActionResult GetChannelFeatures([FromRoute, Required] Guid channelId) - { - return _channelManager.GetChannelFeatures(channelId); - } - - /// - /// Get channel items. - /// - /// Channel Id. - /// Optional. Folder Id. - /// Optional. User Id. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Sort Order - Ascending,Descending. - /// Optional. Specify additional filters to apply. - /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. - /// Optional. Specify additional fields of information to return in the output. - /// Channel items returned. - /// - /// A representing the request to get the channel items. - /// The task result contains an containing the channel items. - /// - [HttpGet("{channelId}/Items")] - public async Task>> GetChannelItems( - [FromRoute, Required] Guid channelId, - [FromQuery] Guid? folderId, - [FromQuery] Guid? userId, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) - { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var query = new InternalItemsQuery(user) - { - Limit = limit, - StartIndex = startIndex, - ChannelIds = new[] { channelId }, - ParentId = folderId ?? Guid.Empty, - OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), - DtoOptions = new DtoOptions { Fields = fields } - }; - - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - } + case ItemFilter.IsFolder: + query.IsFolder = true; + break; + case ItemFilter.IsNotFolder: + query.IsFolder = false; + break; + case ItemFilter.IsUnplayed: + query.IsPlayed = false; + break; + case ItemFilter.IsPlayed: + query.IsPlayed = true; + break; + case ItemFilter.IsFavorite: + query.IsFavorite = true; + break; + case ItemFilter.IsResumable: + query.IsResumable = true; + break; + case ItemFilter.Likes: + query.IsLiked = true; + break; + case ItemFilter.Dislikes: + query.IsLiked = false; + break; + case ItemFilter.IsFavoriteOrLikes: + query.IsFavoriteOrLiked = true; + break; } - - return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false); } - /// - /// Gets latest channel items. - /// - /// Optional. User Id. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional filters to apply. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Specify one or more channel id's, comma delimited. - /// Latest channel items returned. - /// - /// A representing the request to get the latest channel items. - /// The task result contains an containing the latest channel items. - /// - [HttpGet("Items/Latest")] - public async Task>> GetLatestChannelItems( - [FromQuery] Guid? userId, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds) + return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Gets latest channel items. + /// + /// Optional. User Id. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional filters to apply. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Specify one or more channel id's, comma delimited. + /// Latest channel items returned. + /// + /// A representing the request to get the latest channel items. + /// The task result contains an containing the latest channel items. + /// + [HttpGet("Items/Latest")] + public async Task>> GetLatestChannelItems( + [FromQuery] Guid? userId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var query = new InternalItemsQuery(user) { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); + Limit = limit, + StartIndex = startIndex, + ChannelIds = channelIds, + DtoOptions = new DtoOptions { Fields = fields } + }; - var query = new InternalItemsQuery(user) + foreach (var filter in filters) + { + switch (filter) { - Limit = limit, - StartIndex = startIndex, - ChannelIds = channelIds, - DtoOptions = new DtoOptions { Fields = fields } - }; - - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - } + case ItemFilter.IsFolder: + query.IsFolder = true; + break; + case ItemFilter.IsNotFolder: + query.IsFolder = false; + break; + case ItemFilter.IsUnplayed: + query.IsPlayed = false; + break; + case ItemFilter.IsPlayed: + query.IsPlayed = true; + break; + case ItemFilter.IsFavorite: + query.IsFavorite = true; + break; + case ItemFilter.IsResumable: + query.IsResumable = true; + break; + case ItemFilter.Likes: + query.IsLiked = true; + break; + case ItemFilter.Dislikes: + query.IsLiked = false; + break; + case ItemFilter.IsFavoriteOrLikes: + query.IsFavoriteOrLiked = true; + break; } - - return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false); } + + return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false); } } diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs index ed073a687e..57c2071b88 100644 --- a/Jellyfin.Api/Controllers/ClientLogController.cs +++ b/Jellyfin.Api/Controllers/ClientLogController.cs @@ -11,71 +11,70 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Client log controller. +/// +[Authorize(Policy = Policies.DefaultAuthorization)] +public class ClientLogController : BaseJellyfinApiController { + private const int MaxDocumentSize = 1_000_000; + private readonly IClientEventLogger _clientEventLogger; + private readonly IServerConfigurationManager _serverConfigurationManager; + /// - /// Client log controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.DefaultAuthorization)] - public class ClientLogController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + public ClientLogController( + IClientEventLogger clientEventLogger, + IServerConfigurationManager serverConfigurationManager) { - private const int MaxDocumentSize = 1_000_000; - private readonly IClientEventLogger _clientEventLogger; - private readonly IServerConfigurationManager _serverConfigurationManager; + _clientEventLogger = clientEventLogger; + _serverConfigurationManager = serverConfigurationManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public ClientLogController( - IClientEventLogger clientEventLogger, - IServerConfigurationManager serverConfigurationManager) + /// + /// Upload a document. + /// + /// Document saved. + /// Event logging disabled. + /// Upload size too large. + /// Create response. + [HttpPost("Document")] + [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] + [AcceptsFile(MediaTypeNames.Text.Plain)] + [RequestSizeLimit(MaxDocumentSize)] + public async Task> LogFile() + { + if (!_serverConfigurationManager.Configuration.AllowClientLogUpload) { - _clientEventLogger = clientEventLogger; - _serverConfigurationManager = serverConfigurationManager; + return Forbid(); } - /// - /// Upload a document. - /// - /// Document saved. - /// Event logging disabled. - /// Upload size too large. - /// Create response. - [HttpPost("Document")] - [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] - [AcceptsFile(MediaTypeNames.Text.Plain)] - [RequestSizeLimit(MaxDocumentSize)] - public async Task> LogFile() + if (Request.ContentLength > MaxDocumentSize) { - 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)); + // Manually validate to return proper status code. + return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes"); } - private (string ClientName, string ClientVersion) GetRequestInformation() - { - var clientName = HttpContext.User.GetClient() ?? "unknown-client"; - var clientVersion = HttpContext.User.GetIsApiKey() - ? "apikey" - : HttpContext.User.GetVersion() ?? "unknown-version"; + var (clientName, clientVersion) = GetRequestInformation(); + var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body) + .ConfigureAwait(false); + return Ok(new ClientLogDocumentResponseDto(fileName)); + } - return (clientName, clientVersion); - } + private (string ClientName, string ClientVersion) GetRequestInformation() + { + var clientName = HttpContext.User.GetClient() ?? "unknown-client"; + var clientVersion = HttpContext.User.GetIsApiKey() + ? "apikey" + : HttpContext.User.GetVersion() ?? "unknown-version"; + + return (clientName, clientVersion); } } diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index effc9ed7aa..5a4a9bf079 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -11,101 +11,100 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The collection controller. +/// +[Route("Collections")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class CollectionController : BaseJellyfinApiController { + private readonly ICollectionManager _collectionManager; + private readonly IDtoService _dtoService; + /// - /// The collection controller. + /// Initializes a new instance of the class. /// - [Route("Collections")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class CollectionController : BaseJellyfinApiController + /// Instance of interface. + /// Instance of interface. + public CollectionController( + ICollectionManager collectionManager, + IDtoService dtoService) { - private readonly ICollectionManager _collectionManager; - private readonly IDtoService _dtoService; + _collectionManager = collectionManager; + _dtoService = dtoService; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - /// Instance of interface. - public CollectionController( - ICollectionManager collectionManager, - IDtoService dtoService) + /// + /// Creates a new collection. + /// + /// The name of the collection. + /// Item Ids to add to the collection. + /// Optional. Create the collection within a specific folder. + /// Whether or not to lock the new collection. + /// Collection created. + /// A with information about the new collection. + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CreateCollection( + [FromQuery] string? name, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids, + [FromQuery] Guid? parentId, + [FromQuery] bool isLocked = false) + { + var userId = User.GetUserId(); + + var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions { - _collectionManager = collectionManager; - _dtoService = dtoService; - } + IsLocked = isLocked, + Name = name, + ParentId = parentId, + ItemIdList = ids, + UserIds = new[] { userId } + }).ConfigureAwait(false); - /// - /// Creates a new collection. - /// - /// The name of the collection. - /// Item Ids to add to the collection. - /// Optional. Create the collection within a specific folder. - /// Whether or not to lock the new collection. - /// Collection created. - /// A with information about the new collection. - [HttpPost] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> CreateCollection( - [FromQuery] string? name, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids, - [FromQuery] Guid? parentId, - [FromQuery] bool isLocked = false) + var dtoOptions = new DtoOptions().AddClientFields(User); + + var dto = _dtoService.GetBaseItemDto(item, dtoOptions); + + return new CollectionCreationResult { - var userId = User.GetUserId(); + Id = dto.Id + }; + } - var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions - { - IsLocked = isLocked, - Name = name, - ParentId = parentId, - ItemIdList = ids, - UserIds = new[] { userId } - }).ConfigureAwait(false); + /// + /// Adds items to a collection. + /// + /// The collection id. + /// Item ids, comma delimited. + /// Items added to collection. + /// A indicating success. + [HttpPost("{collectionId}/Items")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task AddToCollection( + [FromRoute, Required] Guid collectionId, + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + { + await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true); + return NoContent(); + } - var dtoOptions = new DtoOptions().AddClientFields(User); - - var dto = _dtoService.GetBaseItemDto(item, dtoOptions); - - return new CollectionCreationResult - { - Id = dto.Id - }; - } - - /// - /// Adds items to a collection. - /// - /// The collection id. - /// Item ids, comma delimited. - /// Items added to collection. - /// A indicating success. - [HttpPost("{collectionId}/Items")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task AddToCollection( - [FromRoute, Required] Guid collectionId, - [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) - { - await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true); - return NoContent(); - } - - /// - /// Removes items from a collection. - /// - /// The collection id. - /// Item ids, comma delimited. - /// Items removed from collection. - /// A indicating success. - [HttpDelete("{collectionId}/Items")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task RemoveFromCollection( - [FromRoute, Required] Guid collectionId, - [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) - { - await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false); - return NoContent(); - } + /// + /// Removes items from a collection. + /// + /// The collection id. + /// Item ids, comma delimited. + /// Items removed from collection. + /// A indicating success. + [HttpDelete("{collectionId}/Items")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task RemoveFromCollection( + [FromRoute, Required] Guid collectionId, + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + { + await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false); + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index a00ac1b0af..d53d7cefd0 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -13,124 +13,123 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Configuration Controller. +/// +[Route("System")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class ConfigurationController : BaseJellyfinApiController { + private readonly IServerConfigurationManager _configurationManager; + private readonly IMediaEncoder _mediaEncoder; + + private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options; + /// - /// Configuration Controller. + /// Initializes a new instance of the class. /// - [Route("System")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class ConfigurationController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + public ConfigurationController( + IServerConfigurationManager configurationManager, + IMediaEncoder mediaEncoder) { - private readonly IServerConfigurationManager _configurationManager; - private readonly IMediaEncoder _mediaEncoder; + _configurationManager = configurationManager; + _mediaEncoder = mediaEncoder; + } - private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options; + /// + /// Gets application configuration. + /// + /// Application configuration returned. + /// Application configuration. + [HttpGet("Configuration")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetConfiguration() + { + return _configurationManager.Configuration; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public ConfigurationController( - IServerConfigurationManager configurationManager, - IMediaEncoder mediaEncoder) + /// + /// Updates application configuration. + /// + /// Configuration. + /// Configuration updated. + /// Update status. + [HttpPost("Configuration")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration) + { + _configurationManager.ReplaceConfiguration(configuration); + return NoContent(); + } + + /// + /// Gets a named configuration. + /// + /// Configuration key. + /// Configuration returned. + /// Configuration. + [HttpGet("Configuration/{key}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile(MediaTypeNames.Application.Json)] + public ActionResult GetNamedConfiguration([FromRoute, Required] string key) + { + return _configurationManager.GetConfiguration(key); + } + + /// + /// Updates named configuration. + /// + /// Configuration key. + /// Configuration. + /// Named configuration updated. + /// Update status. + [HttpPost("Configuration/{key}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateNamedConfiguration([FromRoute, Required] string key, [FromBody, Required] JsonDocument configuration) + { + var configurationType = _configurationManager.GetConfigurationType(key); + var deserializedConfiguration = configuration.Deserialize(configurationType, _serializerOptions); + + if (deserializedConfiguration is null) { - _configurationManager = configurationManager; - _mediaEncoder = mediaEncoder; + throw new ArgumentException("Body doesn't contain a valid configuration"); } - /// - /// Gets application configuration. - /// - /// Application configuration returned. - /// Application configuration. - [HttpGet("Configuration")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetConfiguration() - { - return _configurationManager.Configuration; - } + _configurationManager.SaveConfiguration(key, deserializedConfiguration); + return NoContent(); + } - /// - /// Updates application configuration. - /// - /// Configuration. - /// Configuration updated. - /// Update status. - [HttpPost("Configuration")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration) - { - _configurationManager.ReplaceConfiguration(configuration); - return NoContent(); - } + /// + /// Gets a default MetadataOptions object. + /// + /// Metadata options returned. + /// Default MetadataOptions. + [HttpGet("Configuration/MetadataOptions/Default")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDefaultMetadataOptions() + { + return new MetadataOptions(); + } - /// - /// Gets a named configuration. - /// - /// Configuration key. - /// Configuration returned. - /// Configuration. - [HttpGet("Configuration/{key}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesFile(MediaTypeNames.Application.Json)] - public ActionResult GetNamedConfiguration([FromRoute, Required] string key) - { - return _configurationManager.GetConfiguration(key); - } - - /// - /// Updates named configuration. - /// - /// Configuration key. - /// Configuration. - /// Named configuration updated. - /// Update status. - [HttpPost("Configuration/{key}")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateNamedConfiguration([FromRoute, Required] string key, [FromBody, Required] JsonDocument configuration) - { - var configurationType = _configurationManager.GetConfigurationType(key); - var deserializedConfiguration = configuration.Deserialize(configurationType, _serializerOptions); - - if (deserializedConfiguration is null) - { - throw new ArgumentException("Body doesn't contain a valid configuration"); - } - - _configurationManager.SaveConfiguration(key, deserializedConfiguration); - return NoContent(); - } - - /// - /// Gets a default MetadataOptions object. - /// - /// Metadata options returned. - /// Default MetadataOptions. - [HttpGet("Configuration/MetadataOptions/Default")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetDefaultMetadataOptions() - { - return new MetadataOptions(); - } - - /// - /// Updates the path to the media encoder. - /// - /// Media encoder path form body. - /// Media encoder path updated. - /// Status. - [HttpPost("MediaEncoder/Path")] - [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath) - { - _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); - return NoContent(); - } + /// + /// Updates the path to the media encoder. + /// + /// Media encoder path form body. + /// Media encoder path updated. + /// Status. + [HttpPost("MediaEncoder/Path")] + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath) + { + _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index 3894e6c5fc..f7e978bad0 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -14,103 +14,102 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The dashboard controller. +/// +[Route("")] +public class DashboardController : BaseJellyfinApiController { + private readonly ILogger _logger; + private readonly IPluginManager _pluginManager; + /// - /// The dashboard controller. + /// Initializes a new instance of the class. /// - [Route("")] - public class DashboardController : BaseJellyfinApiController + /// Instance of interface. + /// Instance of interface. + public DashboardController( + ILogger logger, + IPluginManager pluginManager) { - private readonly ILogger _logger; - private readonly IPluginManager _pluginManager; + _logger = logger; + _pluginManager = pluginManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - /// Instance of interface. - public DashboardController( - ILogger logger, - IPluginManager pluginManager) + /// + /// Gets the configuration pages. + /// + /// Whether to enable in the main menu. + /// ConfigurationPages returned. + /// Server still loading. + /// An with infos about the plugins. + [HttpGet("web/ConfigurationPages")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult> GetConfigurationPages( + [FromQuery] bool? enableInMainMenu) + { + var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList(); + + if (enableInMainMenu.HasValue) { - _logger = logger; - _pluginManager = pluginManager; + configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList(); } - /// - /// Gets the configuration pages. - /// - /// Whether to enable in the main menu. - /// ConfigurationPages returned. - /// Server still loading. - /// An with infos about the plugins. - [HttpGet("web/ConfigurationPages")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public ActionResult> GetConfigurationPages( - [FromQuery] bool? enableInMainMenu) + return configPages; + } + + /// + /// Gets a dashboard configuration page. + /// + /// The name of the page. + /// ConfigurationPage returned. + /// Plugin configuration page not found. + /// The configuration page. + [HttpGet("web/ConfigurationPage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")] + public ActionResult GetDashboardConfigurationPage([FromQuery] string? name) + { + var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase)); + if (altPage is null) { - var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList(); - - if (enableInMainMenu.HasValue) - { - configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList(); - } - - return configPages; + return NotFound(); } - /// - /// Gets a dashboard configuration page. - /// - /// The name of the page. - /// ConfigurationPage returned. - /// Plugin configuration page not found. - /// The configuration page. - [HttpGet("web/ConfigurationPage")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")] - public ActionResult GetDashboardConfigurationPage([FromQuery] string? name) + IPlugin plugin = altPage.Item2; + string resourcePath = altPage.Item1.EmbeddedResourcePath; + Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath); + if (stream is null) { - var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase)); - if (altPage is null) - { - return NotFound(); - } - - IPlugin plugin = altPage.Item2; - string resourcePath = altPage.Item1.EmbeddedResourcePath; - Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath); - if (stream is null) - { - _logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name); - return NotFound(); - } - - return File(stream, MimeTypes.GetMimeType(resourcePath)); + _logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name); + return NotFound(); } - private IEnumerable GetConfigPages(LocalPlugin plugin) + return File(stream, MimeTypes.GetMimeType(resourcePath)); + } + + private IEnumerable GetConfigPages(LocalPlugin plugin) + { + return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1)); + } + + private IEnumerable> GetPluginPages(LocalPlugin plugin) + { + if (plugin.Instance is not IHasWebPages hasWebPages) { - return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1)); + return Enumerable.Empty>(); } - private IEnumerable> GetPluginPages(LocalPlugin plugin) - { - if (plugin.Instance is not IHasWebPages hasWebPages) - { - return Enumerable.Empty>(); - } + return hasWebPages.GetPages().Select(i => new Tuple(i, plugin.Instance)); + } - return hasWebPages.GetPages().Select(i => new Tuple(i, plugin.Instance)); - } - - private IEnumerable> GetPluginPages() - { - return _pluginManager.Plugins.SelectMany(GetPluginPages); - } + private IEnumerable> GetPluginPages() + { + return _pluginManager.Plugins.SelectMany(GetPluginPages); } } diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index aad60cf5cc..4978622369 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -13,129 +13,128 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Devices Controller. +/// +[Authorize(Policy = Policies.RequiresElevation)] +public class DevicesController : BaseJellyfinApiController { + private readonly IDeviceManager _deviceManager; + private readonly ISessionManager _sessionManager; + /// - /// Devices Controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.RequiresElevation)] - public class DevicesController : BaseJellyfinApiController + /// Instance of interface. + /// Instance of interface. + public DevicesController( + IDeviceManager deviceManager, + ISessionManager sessionManager) { - private readonly IDeviceManager _deviceManager; - private readonly ISessionManager _sessionManager; + _deviceManager = deviceManager; + _sessionManager = sessionManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - /// Instance of interface. - public DevicesController( - IDeviceManager deviceManager, - ISessionManager sessionManager) + /// + /// Get Devices. + /// + /// Gets or sets a value indicating whether [supports synchronize]. + /// Gets or sets the user identifier. + /// Devices retrieved. + /// An containing the list of devices. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + { + return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false); + } + + /// + /// Get info for a device. + /// + /// Device Id. + /// Device info retrieved. + /// Device not found. + /// An containing the device info on success, or a if the device could not be found. + [HttpGet("Info")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetDeviceInfo([FromQuery, Required] string id) + { + var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false); + if (deviceInfo is null) { - _deviceManager = deviceManager; - _sessionManager = sessionManager; + return NotFound(); } - /// - /// Get Devices. - /// - /// Gets or sets a value indicating whether [supports synchronize]. - /// Gets or sets the user identifier. - /// Devices retrieved. - /// An containing the list of devices. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + return deviceInfo; + } + + /// + /// Get options for a device. + /// + /// Device Id. + /// Device options retrieved. + /// Device not found. + /// An containing the device info on success, or a if the device could not be found. + [HttpGet("Options")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetDeviceOptions([FromQuery, Required] string id) + { + var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false); + if (deviceInfo is null) { - return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false); + return NotFound(); } - /// - /// Get info for a device. - /// - /// Device Id. - /// Device info retrieved. - /// Device not found. - /// An containing the device info on success, or a if the device could not be found. - [HttpGet("Info")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetDeviceInfo([FromQuery, Required] string id) - { - var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false); - if (deviceInfo is null) - { - return NotFound(); - } + return deviceInfo; + } - return deviceInfo; + /// + /// Update device options. + /// + /// Device Id. + /// Device Options. + /// Device options updated. + /// A . + [HttpPost("Options")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task UpdateDeviceOptions( + [FromQuery, Required] string id, + [FromBody, Required] DeviceOptionsDto deviceOptions) + { + await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Deletes a device. + /// + /// Device Id. + /// Device deleted. + /// Device not found. + /// A on success, or a if the device could not be found. + [HttpDelete] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteDevice([FromQuery, Required] string id) + { + var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false); + if (existingDevice is null) + { + return NotFound(); } - /// - /// Get options for a device. - /// - /// Device Id. - /// Device options retrieved. - /// Device not found. - /// An containing the device info on success, or a if the device could not be found. - [HttpGet("Options")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetDeviceOptions([FromQuery, Required] string id) - { - var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false); - if (deviceInfo is null) - { - return NotFound(); - } + var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false); - return deviceInfo; + foreach (var session in sessions.Items) + { + await _sessionManager.Logout(session).ConfigureAwait(false); } - /// - /// Update device options. - /// - /// Device Id. - /// Device Options. - /// Device options updated. - /// A . - [HttpPost("Options")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task UpdateDeviceOptions( - [FromQuery, Required] string id, - [FromBody, Required] DeviceOptionsDto deviceOptions) - { - await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Deletes a device. - /// - /// Device Id. - /// Device deleted. - /// Device not found. - /// A on success, or a if the device could not be found. - [HttpDelete] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteDevice([FromQuery, Required] string id) - { - var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false); - if (existingDevice is null) - { - return NotFound(); - } - - var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false); - - foreach (var session in sessions.Items) - { - await _sessionManager.Logout(session).ConfigureAwait(false); - } - - return NoContent(); - } + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 67cceb4a8c..49d87a3621 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -14,201 +14,200 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Display Preferences Controller. +/// +[Authorize(Policy = Policies.DefaultAuthorization)] +public class DisplayPreferencesController : BaseJellyfinApiController { + private readonly IDisplayPreferencesManager _displayPreferencesManager; + private readonly ILogger _logger; + /// - /// Display Preferences Controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.DefaultAuthorization)] - public class DisplayPreferencesController : BaseJellyfinApiController + /// Instance of interface. + /// Instance of interface. + public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger logger) { - private readonly IDisplayPreferencesManager _displayPreferencesManager; - private readonly ILogger _logger; + _displayPreferencesManager = displayPreferencesManager; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - /// Instance of interface. - public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger logger) + /// + /// Get Display Preferences. + /// + /// Display preferences id. + /// User id. + /// Client. + /// Display preferences retrieved. + /// An containing the display preferences on success, or a if the display preferences could not be found. + [HttpGet("{displayPreferencesId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")] + public ActionResult GetDisplayPreferences( + [FromRoute, Required] string displayPreferencesId, + [FromQuery, Required] Guid userId, + [FromQuery, Required] string client) + { + if (!Guid.TryParse(displayPreferencesId, out var itemId)) { - _displayPreferencesManager = displayPreferencesManager; - _logger = logger; + itemId = displayPreferencesId.GetMD5(); } - /// - /// Get Display Preferences. - /// - /// Display preferences id. - /// User id. - /// Client. - /// Display preferences retrieved. - /// An containing the display preferences on success, or a if the display preferences could not be found. - [HttpGet("{displayPreferencesId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")] - public ActionResult GetDisplayPreferences( - [FromRoute, Required] string displayPreferencesId, - [FromQuery, Required] Guid userId, - [FromQuery, Required] string client) + var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client); + var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client); + itemPreferences.ItemId = itemId; + + var dto = new DisplayPreferencesDto { - if (!Guid.TryParse(displayPreferencesId, out var itemId)) - { - itemId = displayPreferencesId.GetMD5(); - } + Client = displayPreferences.Client, + Id = displayPreferences.ItemId.ToString(), + SortBy = itemPreferences.SortBy, + SortOrder = itemPreferences.SortOrder, + IndexBy = displayPreferences.IndexBy?.ToString(), + RememberIndexing = itemPreferences.RememberIndexing, + RememberSorting = itemPreferences.RememberSorting, + ScrollDirection = displayPreferences.ScrollDirection, + ShowBackdrop = displayPreferences.ShowBackdrop, + ShowSidebar = displayPreferences.ShowSidebar + }; - var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client); - var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client); - itemPreferences.ItemId = itemId; - - var dto = new DisplayPreferencesDto - { - Client = displayPreferences.Client, - Id = displayPreferences.ItemId.ToString(), - SortBy = itemPreferences.SortBy, - SortOrder = itemPreferences.SortOrder, - IndexBy = displayPreferences.IndexBy?.ToString(), - RememberIndexing = itemPreferences.RememberIndexing, - RememberSorting = itemPreferences.RememberSorting, - ScrollDirection = displayPreferences.ScrollDirection, - ShowBackdrop = displayPreferences.ShowBackdrop, - ShowSidebar = displayPreferences.ShowSidebar - }; - - foreach (var homeSection in displayPreferences.HomeSections) - { - dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant(); - } - - dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant(); - dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture); - dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture); - dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture); - dto.CustomPrefs["tvhome"] = displayPreferences.TvHome; - dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme; - - // Load all custom display preferences - var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client); - foreach (var (key, value) in customDisplayPreferences) - { - dto.CustomPrefs.TryAdd(key, value); - } - - // This will essentially be a noop if no changes have been made, but new prefs must be saved at least. - _displayPreferencesManager.SaveChanges(); - - return dto; + foreach (var homeSection in displayPreferences.HomeSections) + { + dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant(); } - /// - /// Update Display Preferences. - /// - /// Display preferences id. - /// User Id. - /// Client. - /// New Display Preferences object. - /// Display preferences updated. - /// An on success. - [HttpPost("{displayPreferencesId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")] - public ActionResult UpdateDisplayPreferences( - [FromRoute, Required] string displayPreferencesId, - [FromQuery, Required] Guid userId, - [FromQuery, Required] string client, - [FromBody, Required] DisplayPreferencesDto displayPreferences) - { - HomeSectionType[] defaults = - { - HomeSectionType.SmallLibraryTiles, - HomeSectionType.Resume, - HomeSectionType.ResumeAudio, - HomeSectionType.ResumeBook, - HomeSectionType.LiveTv, - HomeSectionType.NextUp, - HomeSectionType.LatestMedia, - HomeSectionType.None, - }; + dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant(); + dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture); + dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture); + dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture); + dto.CustomPrefs["tvhome"] = displayPreferences.TvHome; + dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme; - if (!Guid.TryParse(displayPreferencesId, out var itemId)) + // Load all custom display preferences + var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client); + foreach (var (key, value) in customDisplayPreferences) + { + dto.CustomPrefs.TryAdd(key, value); + } + + // This will essentially be a noop if no changes have been made, but new prefs must be saved at least. + _displayPreferencesManager.SaveChanges(); + + return dto; + } + + /// + /// Update Display Preferences. + /// + /// Display preferences id. + /// User Id. + /// Client. + /// New Display Preferences object. + /// Display preferences updated. + /// An on success. + [HttpPost("{displayPreferencesId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")] + public ActionResult UpdateDisplayPreferences( + [FromRoute, Required] string displayPreferencesId, + [FromQuery, Required] Guid userId, + [FromQuery, Required] string client, + [FromBody, Required] DisplayPreferencesDto displayPreferences) + { + HomeSectionType[] defaults = + { + HomeSectionType.SmallLibraryTiles, + HomeSectionType.Resume, + HomeSectionType.ResumeAudio, + HomeSectionType.ResumeBook, + HomeSectionType.LiveTv, + HomeSectionType.NextUp, + HomeSectionType.LatestMedia, + HomeSectionType.None, + }; + + if (!Guid.TryParse(displayPreferencesId, out var itemId)) + { + itemId = displayPreferencesId.GetMD5(); + } + + var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client); + existingDisplayPreferences.IndexBy = Enum.TryParse(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null; + existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop; + existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar; + + existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection; + existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion) + && !string.IsNullOrEmpty(chromecastVersion) + ? Enum.Parse(chromecastVersion, true) + : ChromecastVersion.Stable; + displayPreferences.CustomPrefs.Remove("chromecastVersion"); + + 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"); + + existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme) + ? theme + : string.Empty; + displayPreferences.CustomPrefs.Remove("dashboardTheme"); + + existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home) + ? home + : string.Empty; + displayPreferences.CustomPrefs.Remove("tvhome"); + + existingDisplayPreferences.HomeSections.Clear(); + + foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase))) + { + var order = int.Parse(key.AsSpan().Slice("homesection".Length), CultureInfo.InvariantCulture); + if (!Enum.TryParse(displayPreferences.CustomPrefs[key], true, out var type)) { - itemId = displayPreferencesId.GetMD5(); + type = order < 8 ? defaults[order] : HomeSectionType.None; } - var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client); - existingDisplayPreferences.IndexBy = Enum.TryParse(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null; - existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop; - existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar; + displayPreferences.CustomPrefs.Remove(key); + existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type }); + } - existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection; - existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion) - && !string.IsNullOrEmpty(chromecastVersion) - ? Enum.Parse(chromecastVersion, true) - : ChromecastVersion.Stable; - displayPreferences.CustomPrefs.Remove("chromecastVersion"); - - 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"); - - existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme) - ? theme - : string.Empty; - displayPreferences.CustomPrefs.Remove("dashboardTheme"); - - existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home) - ? home - : string.Empty; - displayPreferences.CustomPrefs.Remove("tvhome"); - - existingDisplayPreferences.HomeSections.Clear(); - - foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase))) + foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase))) + { + if (!Enum.TryParse(displayPreferences.CustomPrefs[key], true, out var type)) { - var order = int.Parse(key.AsSpan().Slice("homesection".Length), CultureInfo.InvariantCulture); - if (!Enum.TryParse(displayPreferences.CustomPrefs[key], true, out var type)) - { - type = order < 8 ? defaults[order] : HomeSectionType.None; - } - + _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]); displayPreferences.CustomPrefs.Remove(key); - existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type }); } - - foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase))) - { - if (!Enum.TryParse(displayPreferences.CustomPrefs[key], true, out var type)) - { - _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]); - displayPreferences.CustomPrefs.Remove(key); - } - } - - var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, itemId, existingDisplayPreferences.Client); - itemPrefs.SortBy = displayPreferences.SortBy ?? "SortName"; - itemPrefs.SortOrder = displayPreferences.SortOrder; - itemPrefs.RememberIndexing = displayPreferences.RememberIndexing; - itemPrefs.RememberSorting = displayPreferences.RememberSorting; - itemPrefs.ItemId = itemId; - - // Set all remaining custom preferences. - _displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs); - _displayPreferencesManager.SaveChanges(); - - return NoContent(); } + + var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, itemId, existingDisplayPreferences.Client); + itemPrefs.SortBy = displayPreferences.SortBy ?? "SortName"; + itemPrefs.SortOrder = displayPreferences.SortOrder; + itemPrefs.RememberIndexing = displayPreferences.RememberIndexing; + itemPrefs.RememberSorting = displayPreferences.RememberSorting; + itemPrefs.ItemId = itemId; + + // Set all remaining custom preferences. + _displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs); + _displayPreferencesManager.SaveChanges(); + + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs index 07e0590a10..415385463d 100644 --- a/Jellyfin.Api/Controllers/DlnaController.cs +++ b/Jellyfin.Api/Controllers/DlnaController.cs @@ -7,127 +7,126 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Dlna Controller. +/// +[Authorize(Policy = Policies.RequiresElevation)] +public class DlnaController : BaseJellyfinApiController { + private readonly IDlnaManager _dlnaManager; + /// - /// Dlna Controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.RequiresElevation)] - public class DlnaController : BaseJellyfinApiController + /// Instance of the interface. + public DlnaController(IDlnaManager dlnaManager) { - private readonly IDlnaManager _dlnaManager; + _dlnaManager = dlnaManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - public DlnaController(IDlnaManager dlnaManager) + /// + /// Get profile infos. + /// + /// Device profile infos returned. + /// An containing the device profile infos. + [HttpGet("ProfileInfos")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetProfileInfos() + { + return Ok(_dlnaManager.GetProfileInfos()); + } + + /// + /// Gets the default profile. + /// + /// Default device profile returned. + /// An containing the default profile. + [HttpGet("Profiles/Default")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDefaultProfile() + { + return _dlnaManager.GetDefaultProfile(); + } + + /// + /// Gets a single profile. + /// + /// Profile Id. + /// Device profile returned. + /// Device profile not found. + /// An containing the profile on success, or a if device profile not found. + [HttpGet("Profiles/{profileId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetProfile([FromRoute, Required] string profileId) + { + var profile = _dlnaManager.GetProfile(profileId); + if (profile is null) { - _dlnaManager = dlnaManager; + return NotFound(); } - /// - /// Get profile infos. - /// - /// Device profile infos returned. - /// An containing the device profile infos. - [HttpGet("ProfileInfos")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetProfileInfos() + return profile; + } + + /// + /// Deletes a profile. + /// + /// Profile id. + /// Device profile deleted. + /// Device profile not found. + /// A on success, or a if profile not found. + [HttpDelete("Profiles/{profileId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult DeleteProfile([FromRoute, Required] string profileId) + { + var existingDeviceProfile = _dlnaManager.GetProfile(profileId); + if (existingDeviceProfile is null) { - return Ok(_dlnaManager.GetProfileInfos()); + return NotFound(); } - /// - /// Gets the default profile. - /// - /// Default device profile returned. - /// An containing the default profile. - [HttpGet("Profiles/Default")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetDefaultProfile() + _dlnaManager.DeleteProfile(profileId); + return NoContent(); + } + + /// + /// Creates a profile. + /// + /// Device profile. + /// Device profile created. + /// A . + [HttpPost("Profiles")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile) + { + _dlnaManager.CreateProfile(deviceProfile); + return NoContent(); + } + + /// + /// Updates a profile. + /// + /// Profile id. + /// Device profile. + /// Device profile updated. + /// Device profile not found. + /// A on success, or a if profile not found. + [HttpPost("Profiles/{profileId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile) + { + var existingDeviceProfile = _dlnaManager.GetProfile(profileId); + if (existingDeviceProfile is null) { - return _dlnaManager.GetDefaultProfile(); + return NotFound(); } - /// - /// Gets a single profile. - /// - /// Profile Id. - /// Device profile returned. - /// Device profile not found. - /// An containing the profile on success, or a if device profile not found. - [HttpGet("Profiles/{profileId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetProfile([FromRoute, Required] string profileId) - { - var profile = _dlnaManager.GetProfile(profileId); - if (profile is null) - { - return NotFound(); - } - - return profile; - } - - /// - /// Deletes a profile. - /// - /// Profile id. - /// Device profile deleted. - /// Device profile not found. - /// A on success, or a if profile not found. - [HttpDelete("Profiles/{profileId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteProfile([FromRoute, Required] string profileId) - { - var existingDeviceProfile = _dlnaManager.GetProfile(profileId); - if (existingDeviceProfile is null) - { - return NotFound(); - } - - _dlnaManager.DeleteProfile(profileId); - return NoContent(); - } - - /// - /// Creates a profile. - /// - /// Device profile. - /// Device profile created. - /// A . - [HttpPost("Profiles")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile) - { - _dlnaManager.CreateProfile(deviceProfile); - return NoContent(); - } - - /// - /// Updates a profile. - /// - /// Profile id. - /// Device profile. - /// Device profile updated. - /// Device profile not found. - /// A on success, or a if profile not found. - [HttpPost("Profiles/{profileId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile) - { - var existingDeviceProfile = _dlnaManager.GetProfile(profileId); - if (existingDeviceProfile is null) - { - return NotFound(); - } - - _dlnaManager.UpdateProfile(profileId, deviceProfile); - return NoContent(); - } + _dlnaManager.UpdateProfile(profileId, deviceProfile); + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 96c492b3ec..95b296fae9 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -14,311 +14,310 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Dlna Server Controller. +/// +[Route("Dlna")] +[DlnaEnabled] +[Authorize(Policy = Policies.AnonymousLanAccessPolicy)] +public class DlnaServerController : BaseJellyfinApiController { + private readonly IDlnaManager _dlnaManager; + private readonly IContentDirectory _contentDirectory; + private readonly IConnectionManager _connectionManager; + private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar; + /// - /// Dlna Server Controller. + /// Initializes a new instance of the class. /// - [Route("Dlna")] - [DlnaEnabled] - [Authorize(Policy = Policies.AnonymousLanAccessPolicy)] - public class DlnaServerController : BaseJellyfinApiController + /// Instance of the interface. + public DlnaServerController(IDlnaManager dlnaManager) { - private readonly IDlnaManager _dlnaManager; - private readonly IContentDirectory _contentDirectory; - private readonly IConnectionManager _connectionManager; - private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar; + _dlnaManager = dlnaManager; + _contentDirectory = DlnaEntryPoint.Current.ContentDirectory; + _connectionManager = DlnaEntryPoint.Current.ConnectionManager; + _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - public DlnaServerController(IDlnaManager dlnaManager) + /// + /// Get Description Xml. + /// + /// Server UUID. + /// Description xml returned. + /// DLNA is disabled. + /// An containing the description xml. + [HttpGet("{serverId}/description")] + [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] + public ActionResult GetDescriptionXml([FromRoute, Required] string serverId) + { + var url = GetAbsoluteUri(); + var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); + var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); + return Ok(xml); + } + + /// + /// Gets Dlna content directory xml. + /// + /// Server UUID. + /// Dlna content directory returned. + /// DLNA is disabled. + /// An containing the dlna content directory xml. + [HttpGet("{serverId}/ContentDirectory")] + [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")] + [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] + public ActionResult GetContentDirectory([FromRoute, Required] string serverId) + { + return Ok(_contentDirectory.GetServiceXml()); + } + + /// + /// Gets Dlna media receiver registrar xml. + /// + /// Server UUID. + /// Dlna media receiver registrar xml returned. + /// DLNA is disabled. + /// Dlna media receiver registrar xml. + [HttpGet("{serverId}/MediaReceiverRegistrar")] + [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")] + [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] + public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId) + { + return Ok(_mediaReceiverRegistrar.GetServiceXml()); + } + + /// + /// Gets Dlna media receiver registrar xml. + /// + /// Server UUID. + /// Dlna media receiver registrar xml returned. + /// DLNA is disabled. + /// Dlna media receiver registrar xml. + [HttpGet("{serverId}/ConnectionManager")] + [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")] + [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] + public ActionResult GetConnectionManager([FromRoute, Required] string serverId) + { + return Ok(_connectionManager.GetServiceXml()); + } + + /// + /// Process a content directory control request. + /// + /// Server UUID. + /// Request processed. + /// DLNA is disabled. + /// Control response. + [HttpPost("{serverId}/ContentDirectory/Control")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] + public async Task> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) + { + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); + } + + /// + /// Process a connection manager control request. + /// + /// Server UUID. + /// Request processed. + /// DLNA is disabled. + /// Control response. + [HttpPost("{serverId}/ConnectionManager/Control")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] + public async Task> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) + { + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); + } + + /// + /// Process a media receiver registrar control request. + /// + /// Server UUID. + /// Request processed. + /// DLNA is disabled. + /// Control response. + [HttpPost("{serverId}/MediaReceiverRegistrar/Control")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] + public async Task> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) + { + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); + } + + /// + /// Processes an event subscription request. + /// + /// Server UUID. + /// Request processed. + /// DLNA is disabled. + /// Event subscription response. + [HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")] + [HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")] + [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] + public ActionResult ProcessMediaReceiverRegistrarEventRequest(string serverId) + { + return ProcessEventRequest(_mediaReceiverRegistrar); + } + + /// + /// Processes an event subscription request. + /// + /// Server UUID. + /// Request processed. + /// DLNA is disabled. + /// Event subscription response. + [HttpSubscribe("{serverId}/ContentDirectory/Events")] + [HttpUnsubscribe("{serverId}/ContentDirectory/Events")] + [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] + public ActionResult ProcessContentDirectoryEventRequest(string serverId) + { + return ProcessEventRequest(_contentDirectory); + } + + /// + /// Processes an event subscription request. + /// + /// Server UUID. + /// Request processed. + /// DLNA is disabled. + /// Event subscription response. + [HttpSubscribe("{serverId}/ConnectionManager/Events")] + [HttpUnsubscribe("{serverId}/ConnectionManager/Events")] + [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] + public ActionResult ProcessConnectionManagerEventRequest(string serverId) + { + return ProcessEventRequest(_connectionManager); + } + + /// + /// Gets a server icon. + /// + /// Server UUID. + /// The icon filename. + /// Request processed. + /// Not Found. + /// DLNA is disabled. + /// Icon stream. + [HttpGet("{serverId}/icons/{fileName}")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [ProducesImageFile] + public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName) + { + return GetIconInternal(fileName); + } + + /// + /// Gets a server icon. + /// + /// The icon filename. + /// Icon stream. + /// Request processed. + /// Not Found. + /// DLNA is disabled. + [HttpGet("icons/{fileName}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] + [ProducesImageFile] + public ActionResult GetIcon([FromRoute, Required] string fileName) + { + return GetIconInternal(fileName); + } + + private ActionResult GetIconInternal(string fileName) + { + var icon = _dlnaManager.GetIcon(fileName); + if (icon is null) { - _dlnaManager = dlnaManager; - _contentDirectory = DlnaEntryPoint.Current.ContentDirectory; - _connectionManager = DlnaEntryPoint.Current.ConnectionManager; - _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar; + return NotFound(); } - /// - /// Get Description Xml. - /// - /// Server UUID. - /// Description xml returned. - /// DLNA is disabled. - /// An containing the description xml. - [HttpGet("{serverId}/description")] - [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult GetDescriptionXml([FromRoute, Required] string serverId) - { - var url = GetAbsoluteUri(); - var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); - var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); - return Ok(xml); - } + return File(icon.Stream, MimeTypes.GetMimeType(fileName)); + } - /// - /// Gets Dlna content directory xml. - /// - /// Server UUID. - /// Dlna content directory returned. - /// DLNA is disabled. - /// An containing the dlna content directory xml. - [HttpGet("{serverId}/ContentDirectory")] - [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")] - [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult GetContentDirectory([FromRoute, Required] string serverId) - { - return Ok(_contentDirectory.GetServiceXml()); - } + private string GetAbsoluteUri() + { + return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}"; + } - /// - /// Gets Dlna media receiver registrar xml. - /// - /// Server UUID. - /// Dlna media receiver registrar xml returned. - /// DLNA is disabled. - /// Dlna media receiver registrar xml. - [HttpGet("{serverId}/MediaReceiverRegistrar")] - [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")] - [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId) + private Task ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service) + { + return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers) { - return Ok(_mediaReceiverRegistrar.GetServiceXml()); - } + InputXml = requestStream, + TargetServerUuId = id, + RequestedUrl = GetAbsoluteUri() + }); + } - /// - /// Gets Dlna media receiver registrar xml. - /// - /// Server UUID. - /// Dlna media receiver registrar xml returned. - /// DLNA is disabled. - /// Dlna media receiver registrar xml. - [HttpGet("{serverId}/ConnectionManager")] - [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")] - [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult GetConnectionManager([FromRoute, Required] string serverId) + private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager) + { + var subscriptionId = Request.Headers["SID"]; + if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase)) { - return Ok(_connectionManager.GetServiceXml()); - } + var notificationType = Request.Headers["NT"]; + var callback = Request.Headers["CALLBACK"]; + var timeoutString = Request.Headers["TIMEOUT"]; - /// - /// Process a content directory control request. - /// - /// Server UUID. - /// Request processed. - /// DLNA is disabled. - /// Control response. - [HttpPost("{serverId}/ContentDirectory/Control")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public async Task> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); - } - - /// - /// Process a connection manager control request. - /// - /// Server UUID. - /// Request processed. - /// DLNA is disabled. - /// Control response. - [HttpPost("{serverId}/ConnectionManager/Control")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public async Task> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); - } - - /// - /// Process a media receiver registrar control request. - /// - /// Server UUID. - /// Request processed. - /// DLNA is disabled. - /// Control response. - [HttpPost("{serverId}/MediaReceiverRegistrar/Control")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public async Task> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); - } - - /// - /// Processes an event subscription request. - /// - /// Server UUID. - /// Request processed. - /// DLNA is disabled. - /// Event subscription response. - [HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")] - [HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")] - [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult ProcessMediaReceiverRegistrarEventRequest(string serverId) - { - return ProcessEventRequest(_mediaReceiverRegistrar); - } - - /// - /// Processes an event subscription request. - /// - /// Server UUID. - /// Request processed. - /// DLNA is disabled. - /// Event subscription response. - [HttpSubscribe("{serverId}/ContentDirectory/Events")] - [HttpUnsubscribe("{serverId}/ContentDirectory/Events")] - [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult ProcessContentDirectoryEventRequest(string serverId) - { - return ProcessEventRequest(_contentDirectory); - } - - /// - /// Processes an event subscription request. - /// - /// Server UUID. - /// Request processed. - /// DLNA is disabled. - /// Event subscription response. - [HttpSubscribe("{serverId}/ConnectionManager/Events")] - [HttpUnsubscribe("{serverId}/ConnectionManager/Events")] - [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [Produces(MediaTypeNames.Text.Xml)] - [ProducesFile(MediaTypeNames.Text.Xml)] - public ActionResult ProcessConnectionManagerEventRequest(string serverId) - { - return ProcessEventRequest(_connectionManager); - } - - /// - /// Gets a server icon. - /// - /// Server UUID. - /// The icon filename. - /// Request processed. - /// Not Found. - /// DLNA is disabled. - /// Icon stream. - [HttpGet("{serverId}/icons/{fileName}")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [ProducesImageFile] - public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName) - { - return GetIconInternal(fileName); - } - - /// - /// Gets a server icon. - /// - /// The icon filename. - /// Icon stream. - /// Request processed. - /// Not Found. - /// DLNA is disabled. - [HttpGet("icons/{fileName}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - [ProducesImageFile] - public ActionResult GetIcon([FromRoute, Required] string fileName) - { - return GetIconInternal(fileName); - } - - private ActionResult GetIconInternal(string fileName) - { - var icon = _dlnaManager.GetIcon(fileName); - if (icon is null) + if (string.IsNullOrEmpty(notificationType)) { - return NotFound(); + return dlnaEventManager.RenewEventSubscription( + subscriptionId, + notificationType, + timeoutString, + callback); } - return File(icon.Stream, MimeTypes.GetMimeType(fileName)); + return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback); } - private string GetAbsoluteUri() - { - return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}"; - } - - private Task ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service) - { - return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers) - { - InputXml = requestStream, - TargetServerUuId = id, - RequestedUrl = GetAbsoluteUri() - }); - } - - private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager) - { - var subscriptionId = Request.Headers["SID"]; - if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase)) - { - var notificationType = Request.Headers["NT"]; - var callback = Request.Headers["CALLBACK"]; - var timeoutString = Request.Headers["TIMEOUT"]; - - if (string.IsNullOrEmpty(notificationType)) - { - return dlnaEventManager.RenewEventSubscription( - subscriptionId, - notificationType, - timeoutString, - callback); - } - - return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback); - } - - return dlnaEventManager.CancelEventSubscription(subscriptionId); - } + return dlnaEventManager.CancelEventSubscription(subscriptionId); } } diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b41e239255..b68849171f 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -30,2026 +30,2025 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Dynamic hls controller. +/// +[Route("")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class DynamicHlsController : BaseJellyfinApiController { + private const string DefaultVodEncoderPreset = "veryfast"; + private const string DefaultEventEncoderPreset = "superfast"; + private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; + + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDlnaManager _dlnaManager; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly IDeviceManager _deviceManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly ILogger _logger; + private readonly EncodingHelper _encodingHelper; + private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator; + private readonly DynamicHlsHelper _dynamicHlsHelper; + private readonly EncodingOptions _encodingOptions; + /// - /// Dynamic hls controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class DynamicHlsController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the class. + /// Instance of the interface. + /// Instance of . + /// Instance of . + /// Instance of . + public DynamicHlsController( + ILibraryManager libraryManager, + IUserManager userManager, + IDlnaManager dlnaManager, + IMediaSourceManager mediaSourceManager, + IServerConfigurationManager serverConfigurationManager, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + IDeviceManager deviceManager, + TranscodingJobHelper transcodingJobHelper, + ILogger logger, + DynamicHlsHelper dynamicHlsHelper, + EncodingHelper encodingHelper, + IDynamicHlsPlaylistGenerator dynamicHlsPlaylistGenerator) { - private const string DefaultVodEncoderPreset = "veryfast"; - private const string DefaultEventEncoderPreset = "superfast"; - private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; + _libraryManager = libraryManager; + _userManager = userManager; + _dlnaManager = dlnaManager; + _mediaSourceManager = mediaSourceManager; + _serverConfigurationManager = serverConfigurationManager; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _deviceManager = deviceManager; + _transcodingJobHelper = transcodingJobHelper; + _logger = logger; + _dynamicHlsHelper = dynamicHlsHelper; + _encodingHelper = encodingHelper; + _dynamicHlsPlaylistGenerator = dynamicHlsPlaylistGenerator; - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDlnaManager _dlnaManager; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly IDeviceManager _deviceManager; - private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly ILogger _logger; - private readonly EncodingHelper _encodingHelper; - private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator; - private readonly DynamicHlsHelper _dynamicHlsHelper; - private readonly EncodingOptions _encodingOptions; + _encodingOptions = serverConfigurationManager.GetEncodingOptions(); + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the class. - /// Instance of the interface. - /// Instance of . - /// Instance of . - /// Instance of . - public DynamicHlsController( - ILibraryManager libraryManager, - IUserManager userManager, - IDlnaManager dlnaManager, - IMediaSourceManager mediaSourceManager, - IServerConfigurationManager serverConfigurationManager, - IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - IDeviceManager deviceManager, - TranscodingJobHelper transcodingJobHelper, - ILogger logger, - DynamicHlsHelper dynamicHlsHelper, - EncodingHelper encodingHelper, - IDynamicHlsPlaylistGenerator dynamicHlsPlaylistGenerator) + /// + /// Gets a hls live stream. + /// + /// The item id. + /// The audio container. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment length. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamorphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// 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. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Optional. The max width. + /// Optional. The max height. + /// Optional. Whether to enable subtitles in the manifest. + /// Hls live stream retrieved. + /// A containing the hls file. + [HttpGet("Videos/{itemId}/live.m3u8")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] + public async Task GetLiveHlsStream( + [FromRoute, Required] Guid itemId, + [FromQuery] string? container, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodeReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary streamOptions, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] bool? enableSubtitlesInManifest) + { + VideoRequestDto streamingRequest = new VideoRequestDto { - _libraryManager = libraryManager; - _userManager = userManager; - _dlnaManager = dlnaManager; - _mediaSourceManager = mediaSourceManager; - _serverConfigurationManager = serverConfigurationManager; - _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _deviceManager = deviceManager; - _transcodingJobHelper = transcodingJobHelper; - _logger = logger; - _dynamicHlsHelper = dynamicHlsHelper; - _encodingHelper = encodingHelper; - _dynamicHlsPlaylistGenerator = dynamicHlsPlaylistGenerator; + Id = itemId, + Container = container, + Static = @static ?? false, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? false, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodeReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Streaming, + StreamOptions = streamOptions, + MaxHeight = maxHeight, + MaxWidth = maxWidth, + EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true + }; - _encodingOptions = serverConfigurationManager.GetEncodingOptions(); - } + // 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; + var state = await StreamingHelpers.GetStreamingState( + streamingRequest, + HttpContext, + _mediaSourceManager, + _userManager, + _libraryManager, + _serverConfigurationManager, + _mediaEncoder, + _encodingHelper, + _dlnaManager, + _deviceManager, + _transcodingJobHelper, + TranscodingJobType, + cancellationToken) + .ConfigureAwait(false); - /// - /// Gets a hls live stream. - /// - /// The item id. - /// The audio container. - /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. - /// The streaming parameters. - /// The tag. - /// Optional. The dlna device profile id to utilize. - /// The play session id. - /// The segment container. - /// The segment length. - /// The minimum number of segments. - /// The media version id, if playing an alternate version. - /// The device id of the client requesting. Used to stop encoding processes when needed. - /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. - /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. - /// Whether or not to allow copying of the video stream url. - /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. - /// Optional. Specify a specific audio sample rate, e.g. 44100. - /// Optional. The maximum audio bit depth. - /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. - /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. - /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. - /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. - /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. - /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. - /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. - /// Optional. The fixed horizontal resolution of the encoded video. - /// Optional. The fixed vertical resolution of the encoded video. - /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. - /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. - /// Optional. Specify the subtitle delivery method. - /// Optional. - /// Optional. The maximum video bit depth. - /// Optional. Whether to require avc. - /// Optional. Whether to deinterlace the video. - /// Optional. Whether to require a non anamorphic stream. - /// Optional. The maximum number of audio channels to transcode. - /// Optional. The limit of how many cpu cores to use. - /// The live stream id. - /// Optional. Whether to enable the MpegtsM2Ts mode. - /// 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. - /// Optional. Specify a subtitle codec to encode to. - /// Optional. The transcoding reason. - /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. - /// Optional. The index of the video stream to use. If omitted the first video stream will be used. - /// Optional. The . - /// Optional. The streaming options. - /// Optional. The max width. - /// Optional. The max height. - /// Optional. Whether to enable subtitles in the manifest. - /// Hls live stream retrieved. - /// A containing the hls file. - [HttpGet("Videos/{itemId}/live.m3u8")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesPlaylistFile] - public async Task GetLiveHlsStream( - [FromRoute, Required] Guid itemId, - [FromQuery] string? container, - [FromQuery] bool? @static, - [FromQuery] string? @params, - [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, - [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, - [FromQuery] int? segmentLength, - [FromQuery] int? minSegments, - [FromQuery] string? mediaSourceId, - [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, - [FromQuery] bool? enableAutoStreamCopy, - [FromQuery] bool? allowVideoStreamCopy, - [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, - [FromQuery] int? audioSampleRate, - [FromQuery] int? maxAudioBitDepth, - [FromQuery] int? audioBitRate, - [FromQuery] int? audioChannels, - [FromQuery] int? maxAudioChannels, - [FromQuery] string? profile, - [FromQuery] string? level, - [FromQuery] float? framerate, - [FromQuery] float? maxFramerate, - [FromQuery] bool? copyTimestamps, - [FromQuery] long? startTimeTicks, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? videoBitRate, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod? subtitleMethod, - [FromQuery] int? maxRefFrames, - [FromQuery] int? maxVideoBitDepth, - [FromQuery] bool? requireAvc, - [FromQuery] bool? deInterlace, - [FromQuery] bool? requireNonAnamorphic, - [FromQuery] int? transcodingMaxAudioChannels, - [FromQuery] int? cpuCoreLimit, - [FromQuery] string? liveStreamId, - [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodeReasons, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext? context, - [FromQuery] Dictionary streamOptions, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] bool? enableSubtitlesInManifest) + TranscodingJobDto? job = null; + var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); + + if (!System.IO.File.Exists(playlistPath)) { - VideoRequestDto streamingRequest = new VideoRequestDto - { - Id = itemId, - Container = container, - Static = @static ?? false, - Params = @params, - Tag = tag, - DeviceProfileId = deviceProfileId, - PlaySessionId = playSessionId, - SegmentContainer = segmentContainer, - SegmentLength = segmentLength, - MinSegments = minSegments, - MediaSourceId = mediaSourceId, - DeviceId = deviceId, - AudioCodec = audioCodec, - EnableAutoStreamCopy = enableAutoStreamCopy ?? true, - AllowAudioStreamCopy = allowAudioStreamCopy ?? true, - AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, - AudioSampleRate = audioSampleRate, - MaxAudioChannels = maxAudioChannels, - AudioBitRate = audioBitRate, - MaxAudioBitDepth = maxAudioBitDepth, - AudioChannels = audioChannels, - Profile = profile, - Level = level, - Framerate = framerate, - MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? false, - StartTimeTicks = startTimeTicks, - Width = width, - Height = height, - VideoBitRate = videoBitRate, - SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, - MaxRefFrames = maxRefFrames, - MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? false, - DeInterlace = deInterlace ?? false, - RequireNonAnamorphic = requireNonAnamorphic ?? false, - TranscodingMaxAudioChannels = transcodingMaxAudioChannels, - CpuCoreLimit = cpuCoreLimit, - LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, - VideoCodec = videoCodec, - SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodeReasons, - AudioStreamIndex = audioStreamIndex, - VideoStreamIndex = videoStreamIndex, - Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions, - MaxHeight = maxHeight, - MaxWidth = maxWidth, - 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; - var state = await StreamingHelpers.GetStreamingState( - streamingRequest, - HttpContext, - _mediaSourceManager, - _userManager, - _libraryManager, - _serverConfigurationManager, - _mediaEncoder, - _encodingHelper, - _dlnaManager, - _deviceManager, - _transcodingJobHelper, - TranscodingJobType, - cancellationToken) - .ConfigureAwait(false); - - TranscodingJobDto? job = null; - var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); - - if (!System.IO.File.Exists(playlistPath)) - { - var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); - await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - if (!System.IO.File.Exists(playlistPath)) - { - // If the playlist doesn't already exist, startup ffmpeg - try - { - job = await _transcodingJobHelper.StartFfMpeg( - state, - playlistPath, - GetCommandLineArguments(playlistPath, state, true, 0), - Request, - TranscodingJobType, - cancellationTokenSource) - .ConfigureAwait(false); - job.IsLiveOutput = true; - } - catch - { - state.Dispose(); - throw; - } - - minSegments = state.MinSegments; - if (minSegments > 0) - { - await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false); - } - } - } - finally - { - transcodingLock.Release(); - } - } - - job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); - - if (job is not null) - { - _transcodingJobHelper.OnTranscodeEndRequest(job); - } - - var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state); - - return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8")); - } - - /// - /// Gets a video hls playlist stream. - /// - /// The item id. - /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. - /// The streaming parameters. - /// The tag. - /// Optional. The dlna device profile id to utilize. - /// The play session id. - /// The segment container. - /// The segment length. - /// The minimum number of segments. - /// The media version id, if playing an alternate version. - /// The device id of the client requesting. Used to stop encoding processes when needed. - /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. - /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. - /// Whether or not to allow copying of the video stream url. - /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. - /// Optional. Specify a specific audio sample rate, e.g. 44100. - /// Optional. The maximum audio bit depth. - /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. - /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. - /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. - /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. - /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. - /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. - /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. - /// Optional. The fixed horizontal resolution of the encoded video. - /// Optional. The fixed vertical resolution of the encoded video. - /// Optional. The maximum horizontal resolution of the encoded video. - /// Optional. The maximum vertical resolution of the encoded video. - /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. - /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. - /// Optional. Specify the subtitle delivery method. - /// Optional. - /// Optional. The maximum video bit depth. - /// Optional. Whether to require avc. - /// Optional. Whether to deinterlace the video. - /// Optional. Whether to require a non anamorphic stream. - /// Optional. The maximum number of audio channels to transcode. - /// Optional. The limit of how many cpu cores to use. - /// The live stream id. - /// Optional. Whether to enable the MpegtsM2Ts mode. - /// 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. - /// Optional. Specify a subtitle codec to encode to. - /// Optional. The transcoding reason. - /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. - /// Optional. The index of the video stream to use. If omitted the first video stream will be used. - /// Optional. The . - /// Optional. The streaming options. - /// Enable adaptive bitrate streaming. - /// Video stream returned. - /// A containing the playlist file. - [HttpGet("Videos/{itemId}/master.m3u8")] - [HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesPlaylistFile] - public async Task GetMasterHlsVideoPlaylist( - [FromRoute, Required] Guid itemId, - [FromQuery] bool? @static, - [FromQuery] string? @params, - [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, - [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, - [FromQuery] int? segmentLength, - [FromQuery] int? minSegments, - [FromQuery, Required] string mediaSourceId, - [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, - [FromQuery] bool? enableAutoStreamCopy, - [FromQuery] bool? allowVideoStreamCopy, - [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, - [FromQuery] int? audioSampleRate, - [FromQuery] int? maxAudioBitDepth, - [FromQuery] int? audioBitRate, - [FromQuery] int? audioChannels, - [FromQuery] int? maxAudioChannels, - [FromQuery] string? profile, - [FromQuery] string? level, - [FromQuery] float? framerate, - [FromQuery] float? maxFramerate, - [FromQuery] bool? copyTimestamps, - [FromQuery] long? startTimeTicks, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] int? videoBitRate, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod? subtitleMethod, - [FromQuery] int? maxRefFrames, - [FromQuery] int? maxVideoBitDepth, - [FromQuery] bool? requireAvc, - [FromQuery] bool? deInterlace, - [FromQuery] bool? requireNonAnamorphic, - [FromQuery] int? transcodingMaxAudioChannels, - [FromQuery] int? cpuCoreLimit, - [FromQuery] string? liveStreamId, - [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodeReasons, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext? context, - [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAdaptiveBitrateStreaming = true) - { - var streamingRequest = new HlsVideoRequestDto - { - Id = itemId, - Static = @static ?? false, - Params = @params, - Tag = tag, - DeviceProfileId = deviceProfileId, - PlaySessionId = playSessionId, - SegmentContainer = segmentContainer, - SegmentLength = segmentLength, - MinSegments = minSegments, - MediaSourceId = mediaSourceId, - DeviceId = deviceId, - AudioCodec = audioCodec, - EnableAutoStreamCopy = enableAutoStreamCopy ?? true, - AllowAudioStreamCopy = allowAudioStreamCopy ?? true, - AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, - AudioSampleRate = audioSampleRate, - MaxAudioChannels = maxAudioChannels, - AudioBitRate = audioBitRate, - MaxAudioBitDepth = maxAudioBitDepth, - AudioChannels = audioChannels, - Profile = profile, - Level = level, - Framerate = framerate, - MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? false, - StartTimeTicks = startTimeTicks, - Width = width, - Height = height, - MaxWidth = maxWidth, - MaxHeight = maxHeight, - VideoBitRate = videoBitRate, - SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, - MaxRefFrames = maxRefFrames, - MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? false, - DeInterlace = deInterlace ?? false, - RequireNonAnamorphic = requireNonAnamorphic ?? false, - TranscodingMaxAudioChannels = transcodingMaxAudioChannels, - CpuCoreLimit = cpuCoreLimit, - LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, - VideoCodec = videoCodec, - SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodeReasons, - AudioStreamIndex = audioStreamIndex, - VideoStreamIndex = videoStreamIndex, - Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions, - EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming - }; - - return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); - } - - /// - /// Gets an audio hls playlist stream. - /// - /// The item id. - /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. - /// The streaming parameters. - /// The tag. - /// Optional. The dlna device profile id to utilize. - /// The play session id. - /// The segment container. - /// The segment length. - /// The minimum number of segments. - /// The media version id, if playing an alternate version. - /// The device id of the client requesting. Used to stop encoding processes when needed. - /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. - /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. - /// Whether or not to allow copying of the video stream url. - /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. - /// Optional. Specify a specific audio sample rate, e.g. 44100. - /// Optional. The maximum audio bit depth. - /// Optional. The maximum streaming bitrate. - /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. - /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. - /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. - /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. - /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. - /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. - /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. - /// Optional. The fixed horizontal resolution of the encoded video. - /// Optional. The fixed vertical resolution of the encoded video. - /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. - /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. - /// Optional. Specify the subtitle delivery method. - /// Optional. - /// Optional. The maximum video bit depth. - /// Optional. Whether to require avc. - /// Optional. Whether to deinterlace the video. - /// Optional. Whether to require a non anamorphic stream. - /// Optional. The maximum number of audio channels to transcode. - /// Optional. The limit of how many cpu cores to use. - /// The live stream id. - /// Optional. Whether to enable the MpegtsM2Ts mode. - /// 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. - /// Optional. Specify a subtitle codec to encode to. - /// Optional. The transcoding reason. - /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. - /// Optional. The index of the video stream to use. If omitted the first video stream will be used. - /// Optional. The . - /// Optional. The streaming options. - /// Enable adaptive bitrate streaming. - /// Audio stream returned. - /// A containing the playlist file. - [HttpGet("Audio/{itemId}/master.m3u8")] - [HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesPlaylistFile] - public async Task GetMasterHlsAudioPlaylist( - [FromRoute, Required] Guid itemId, - [FromQuery] bool? @static, - [FromQuery] string? @params, - [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, - [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, - [FromQuery] int? segmentLength, - [FromQuery] int? minSegments, - [FromQuery, Required] string mediaSourceId, - [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, - [FromQuery] bool? enableAutoStreamCopy, - [FromQuery] bool? allowVideoStreamCopy, - [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, - [FromQuery] int? audioSampleRate, - [FromQuery] int? maxAudioBitDepth, - [FromQuery] int? maxStreamingBitrate, - [FromQuery] int? audioBitRate, - [FromQuery] int? audioChannels, - [FromQuery] int? maxAudioChannels, - [FromQuery] string? profile, - [FromQuery] string? level, - [FromQuery] float? framerate, - [FromQuery] float? maxFramerate, - [FromQuery] bool? copyTimestamps, - [FromQuery] long? startTimeTicks, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? videoBitRate, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod? subtitleMethod, - [FromQuery] int? maxRefFrames, - [FromQuery] int? maxVideoBitDepth, - [FromQuery] bool? requireAvc, - [FromQuery] bool? deInterlace, - [FromQuery] bool? requireNonAnamorphic, - [FromQuery] int? transcodingMaxAudioChannels, - [FromQuery] int? cpuCoreLimit, - [FromQuery] string? liveStreamId, - [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodeReasons, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext? context, - [FromQuery] Dictionary streamOptions, - [FromQuery] bool enableAdaptiveBitrateStreaming = true) - { - var streamingRequest = new HlsAudioRequestDto - { - Id = itemId, - Static = @static ?? false, - Params = @params, - Tag = tag, - DeviceProfileId = deviceProfileId, - PlaySessionId = playSessionId, - SegmentContainer = segmentContainer, - SegmentLength = segmentLength, - MinSegments = minSegments, - MediaSourceId = mediaSourceId, - DeviceId = deviceId, - AudioCodec = audioCodec, - EnableAutoStreamCopy = enableAutoStreamCopy ?? true, - AllowAudioStreamCopy = allowAudioStreamCopy ?? true, - AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, - AudioSampleRate = audioSampleRate, - MaxAudioChannels = maxAudioChannels, - AudioBitRate = audioBitRate ?? maxStreamingBitrate, - MaxAudioBitDepth = maxAudioBitDepth, - AudioChannels = audioChannels, - Profile = profile, - Level = level, - Framerate = framerate, - MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? false, - StartTimeTicks = startTimeTicks, - Width = width, - Height = height, - VideoBitRate = videoBitRate, - SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, - MaxRefFrames = maxRefFrames, - MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? false, - DeInterlace = deInterlace ?? false, - RequireNonAnamorphic = requireNonAnamorphic ?? false, - TranscodingMaxAudioChannels = transcodingMaxAudioChannels, - CpuCoreLimit = cpuCoreLimit, - LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, - VideoCodec = videoCodec, - SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodeReasons, - AudioStreamIndex = audioStreamIndex, - VideoStreamIndex = videoStreamIndex, - Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions, - EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming - }; - - return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); - } - - /// - /// Gets a video stream using HTTP live streaming. - /// - /// The item id. - /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. - /// The streaming parameters. - /// The tag. - /// Optional. The dlna device profile id to utilize. - /// The play session id. - /// The segment container. - /// The segment length. - /// The minimum number of segments. - /// The media version id, if playing an alternate version. - /// The device id of the client requesting. Used to stop encoding processes when needed. - /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. - /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. - /// Whether or not to allow copying of the video stream url. - /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. - /// Optional. Specify a specific audio sample rate, e.g. 44100. - /// Optional. The maximum audio bit depth. - /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. - /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. - /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. - /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. - /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. - /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. - /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. - /// Optional. The fixed horizontal resolution of the encoded video. - /// Optional. The fixed vertical resolution of the encoded video. - /// Optional. The maximum horizontal resolution of the encoded video. - /// Optional. The maximum vertical resolution of the encoded video. - /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. - /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. - /// Optional. Specify the subtitle delivery method. - /// Optional. - /// Optional. The maximum video bit depth. - /// Optional. Whether to require avc. - /// Optional. Whether to deinterlace the video. - /// Optional. Whether to require a non anamorphic stream. - /// Optional. The maximum number of audio channels to transcode. - /// Optional. The limit of how many cpu cores to use. - /// The live stream id. - /// Optional. Whether to enable the MpegtsM2Ts mode. - /// 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. - /// Optional. Specify a subtitle codec to encode to. - /// Optional. The transcoding reason. - /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. - /// Optional. The index of the video stream to use. If omitted the first video stream will be used. - /// Optional. The . - /// Optional. The streaming options. - /// Video stream returned. - /// A containing the audio file. - [HttpGet("Videos/{itemId}/main.m3u8")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesPlaylistFile] - public async Task GetVariantHlsVideoPlaylist( - [FromRoute, Required] Guid itemId, - [FromQuery] bool? @static, - [FromQuery] string? @params, - [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, - [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, - [FromQuery] int? segmentLength, - [FromQuery] int? minSegments, - [FromQuery] string? mediaSourceId, - [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, - [FromQuery] bool? enableAutoStreamCopy, - [FromQuery] bool? allowVideoStreamCopy, - [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, - [FromQuery] int? audioSampleRate, - [FromQuery] int? maxAudioBitDepth, - [FromQuery] int? audioBitRate, - [FromQuery] int? audioChannels, - [FromQuery] int? maxAudioChannels, - [FromQuery] string? profile, - [FromQuery] string? level, - [FromQuery] float? framerate, - [FromQuery] float? maxFramerate, - [FromQuery] bool? copyTimestamps, - [FromQuery] long? startTimeTicks, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] int? videoBitRate, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod? subtitleMethod, - [FromQuery] int? maxRefFrames, - [FromQuery] int? maxVideoBitDepth, - [FromQuery] bool? requireAvc, - [FromQuery] bool? deInterlace, - [FromQuery] bool? requireNonAnamorphic, - [FromQuery] int? transcodingMaxAudioChannels, - [FromQuery] int? cpuCoreLimit, - [FromQuery] string? liveStreamId, - [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodeReasons, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext? context, - [FromQuery] Dictionary streamOptions) - { - using var cancellationTokenSource = new CancellationTokenSource(); - var streamingRequest = new VideoRequestDto - { - Id = itemId, - Static = @static ?? false, - Params = @params, - Tag = tag, - DeviceProfileId = deviceProfileId, - PlaySessionId = playSessionId, - SegmentContainer = segmentContainer, - SegmentLength = segmentLength, - MinSegments = minSegments, - MediaSourceId = mediaSourceId, - DeviceId = deviceId, - AudioCodec = audioCodec, - EnableAutoStreamCopy = enableAutoStreamCopy ?? true, - AllowAudioStreamCopy = allowAudioStreamCopy ?? true, - AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, - AudioSampleRate = audioSampleRate, - MaxAudioChannels = maxAudioChannels, - AudioBitRate = audioBitRate, - MaxAudioBitDepth = maxAudioBitDepth, - AudioChannels = audioChannels, - Profile = profile, - Level = level, - Framerate = framerate, - MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? false, - StartTimeTicks = startTimeTicks, - Width = width, - Height = height, - MaxWidth = maxWidth, - MaxHeight = maxHeight, - VideoBitRate = videoBitRate, - SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, - MaxRefFrames = maxRefFrames, - MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? false, - DeInterlace = deInterlace ?? false, - RequireNonAnamorphic = requireNonAnamorphic ?? false, - TranscodingMaxAudioChannels = transcodingMaxAudioChannels, - CpuCoreLimit = cpuCoreLimit, - LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, - VideoCodec = videoCodec, - SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodeReasons, - AudioStreamIndex = audioStreamIndex, - VideoStreamIndex = videoStreamIndex, - Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions - }; - - return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) - .ConfigureAwait(false); - } - - /// - /// Gets an audio stream using HTTP live streaming. - /// - /// The item id. - /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. - /// The streaming parameters. - /// The tag. - /// Optional. The dlna device profile id to utilize. - /// The play session id. - /// The segment container. - /// The segment length. - /// The minimum number of segments. - /// The media version id, if playing an alternate version. - /// The device id of the client requesting. Used to stop encoding processes when needed. - /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. - /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. - /// Whether or not to allow copying of the video stream url. - /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. - /// Optional. Specify a specific audio sample rate, e.g. 44100. - /// Optional. The maximum audio bit depth. - /// Optional. The maximum streaming bitrate. - /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. - /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. - /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. - /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. - /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. - /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. - /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. - /// Optional. The fixed horizontal resolution of the encoded video. - /// Optional. The fixed vertical resolution of the encoded video. - /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. - /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. - /// Optional. Specify the subtitle delivery method. - /// Optional. - /// Optional. The maximum video bit depth. - /// Optional. Whether to require avc. - /// Optional. Whether to deinterlace the video. - /// Optional. Whether to require a non anamorphic stream. - /// Optional. The maximum number of audio channels to transcode. - /// Optional. The limit of how many cpu cores to use. - /// The live stream id. - /// Optional. Whether to enable the MpegtsM2Ts mode. - /// 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. - /// Optional. Specify a subtitle codec to encode to. - /// Optional. The transcoding reason. - /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. - /// Optional. The index of the video stream to use. If omitted the first video stream will be used. - /// Optional. The . - /// Optional. The streaming options. - /// Audio stream returned. - /// A containing the audio file. - [HttpGet("Audio/{itemId}/main.m3u8")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesPlaylistFile] - public async Task GetVariantHlsAudioPlaylist( - [FromRoute, Required] Guid itemId, - [FromQuery] bool? @static, - [FromQuery] string? @params, - [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, - [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, - [FromQuery] int? segmentLength, - [FromQuery] int? minSegments, - [FromQuery] string? mediaSourceId, - [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, - [FromQuery] bool? enableAutoStreamCopy, - [FromQuery] bool? allowVideoStreamCopy, - [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, - [FromQuery] int? audioSampleRate, - [FromQuery] int? maxAudioBitDepth, - [FromQuery] int? maxStreamingBitrate, - [FromQuery] int? audioBitRate, - [FromQuery] int? audioChannels, - [FromQuery] int? maxAudioChannels, - [FromQuery] string? profile, - [FromQuery] string? level, - [FromQuery] float? framerate, - [FromQuery] float? maxFramerate, - [FromQuery] bool? copyTimestamps, - [FromQuery] long? startTimeTicks, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? videoBitRate, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod? subtitleMethod, - [FromQuery] int? maxRefFrames, - [FromQuery] int? maxVideoBitDepth, - [FromQuery] bool? requireAvc, - [FromQuery] bool? deInterlace, - [FromQuery] bool? requireNonAnamorphic, - [FromQuery] int? transcodingMaxAudioChannels, - [FromQuery] int? cpuCoreLimit, - [FromQuery] string? liveStreamId, - [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodeReasons, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext? context, - [FromQuery] Dictionary streamOptions) - { - using var cancellationTokenSource = new CancellationTokenSource(); - var streamingRequest = new StreamingRequestDto - { - Id = itemId, - Static = @static ?? false, - Params = @params, - Tag = tag, - DeviceProfileId = deviceProfileId, - PlaySessionId = playSessionId, - SegmentContainer = segmentContainer, - SegmentLength = segmentLength, - MinSegments = minSegments, - MediaSourceId = mediaSourceId, - DeviceId = deviceId, - AudioCodec = audioCodec, - EnableAutoStreamCopy = enableAutoStreamCopy ?? true, - AllowAudioStreamCopy = allowAudioStreamCopy ?? true, - AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, - AudioSampleRate = audioSampleRate, - MaxAudioChannels = maxAudioChannels, - AudioBitRate = audioBitRate ?? maxStreamingBitrate, - MaxAudioBitDepth = maxAudioBitDepth, - AudioChannels = audioChannels, - Profile = profile, - Level = level, - Framerate = framerate, - MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? false, - StartTimeTicks = startTimeTicks, - Width = width, - Height = height, - VideoBitRate = videoBitRate, - SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, - MaxRefFrames = maxRefFrames, - MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? false, - DeInterlace = deInterlace ?? false, - RequireNonAnamorphic = requireNonAnamorphic ?? false, - TranscodingMaxAudioChannels = transcodingMaxAudioChannels, - CpuCoreLimit = cpuCoreLimit, - LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, - VideoCodec = videoCodec, - SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodeReasons, - AudioStreamIndex = audioStreamIndex, - VideoStreamIndex = videoStreamIndex, - Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions - }; - - return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) - .ConfigureAwait(false); - } - - /// - /// Gets a video stream using HTTP live streaming. - /// - /// The item id. - /// The playlist id. - /// The segment id. - /// The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. - /// The position of the requested segment in ticks. - /// The length of the requested segment in ticks. - /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. - /// The streaming parameters. - /// The tag. - /// Optional. The dlna device profile id to utilize. - /// The play session id. - /// The segment container. - /// The desired segment length. - /// The minimum number of segments. - /// The media version id, if playing an alternate version. - /// The device id of the client requesting. Used to stop encoding processes when needed. - /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. - /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. - /// Whether or not to allow copying of the video stream url. - /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. - /// Optional. Specify a specific audio sample rate, e.g. 44100. - /// Optional. The maximum audio bit depth. - /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. - /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. - /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. - /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. - /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. - /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. - /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. - /// Optional. The fixed horizontal resolution of the encoded video. - /// Optional. The fixed vertical resolution of the encoded video. - /// Optional. The maximum horizontal resolution of the encoded video. - /// Optional. The maximum vertical resolution of the encoded video. - /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. - /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. - /// Optional. Specify the subtitle delivery method. - /// Optional. - /// Optional. The maximum video bit depth. - /// Optional. Whether to require avc. - /// Optional. Whether to deinterlace the video. - /// Optional. Whether to require a non anamorphic stream. - /// Optional. The maximum number of audio channels to transcode. - /// Optional. The limit of how many cpu cores to use. - /// The live stream id. - /// Optional. Whether to enable the MpegtsM2Ts mode. - /// 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. - /// Optional. Specify a subtitle codec to encode to. - /// Optional. The transcoding reason. - /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. - /// Optional. The index of the video stream to use. If omitted the first video stream will be used. - /// Optional. The . - /// Optional. The streaming options. - /// Video stream returned. - /// A containing the audio file. - [HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesVideoFile] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")] - public async Task GetHlsVideoSegment( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] string playlistId, - [FromRoute, Required] int segmentId, - [FromRoute, Required] string container, - [FromQuery, Required] long runtimeTicks, - [FromQuery, Required] long actualSegmentLengthTicks, - [FromQuery] bool? @static, - [FromQuery] string? @params, - [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, - [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, - [FromQuery] int? segmentLength, - [FromQuery] int? minSegments, - [FromQuery] string? mediaSourceId, - [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, - [FromQuery] bool? enableAutoStreamCopy, - [FromQuery] bool? allowVideoStreamCopy, - [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, - [FromQuery] int? audioSampleRate, - [FromQuery] int? maxAudioBitDepth, - [FromQuery] int? audioBitRate, - [FromQuery] int? audioChannels, - [FromQuery] int? maxAudioChannels, - [FromQuery] string? profile, - [FromQuery] string? level, - [FromQuery] float? framerate, - [FromQuery] float? maxFramerate, - [FromQuery] bool? copyTimestamps, - [FromQuery] long? startTimeTicks, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] int? videoBitRate, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod? subtitleMethod, - [FromQuery] int? maxRefFrames, - [FromQuery] int? maxVideoBitDepth, - [FromQuery] bool? requireAvc, - [FromQuery] bool? deInterlace, - [FromQuery] bool? requireNonAnamorphic, - [FromQuery] int? transcodingMaxAudioChannels, - [FromQuery] int? cpuCoreLimit, - [FromQuery] string? liveStreamId, - [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodeReasons, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext? context, - [FromQuery] Dictionary streamOptions) - { - var streamingRequest = new VideoRequestDto - { - Id = itemId, - CurrentRuntimeTicks = runtimeTicks, - ActualSegmentLengthTicks = actualSegmentLengthTicks, - Container = container, - Static = @static ?? false, - Params = @params, - Tag = tag, - DeviceProfileId = deviceProfileId, - PlaySessionId = playSessionId, - SegmentContainer = segmentContainer, - SegmentLength = segmentLength, - MinSegments = minSegments, - MediaSourceId = mediaSourceId, - DeviceId = deviceId, - AudioCodec = audioCodec, - EnableAutoStreamCopy = enableAutoStreamCopy ?? true, - AllowAudioStreamCopy = allowAudioStreamCopy ?? true, - AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, - AudioSampleRate = audioSampleRate, - MaxAudioChannels = maxAudioChannels, - AudioBitRate = audioBitRate, - MaxAudioBitDepth = maxAudioBitDepth, - AudioChannels = audioChannels, - Profile = profile, - Level = level, - Framerate = framerate, - MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? false, - StartTimeTicks = startTimeTicks, - Width = width, - Height = height, - MaxWidth = maxWidth, - MaxHeight = maxHeight, - VideoBitRate = videoBitRate, - SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, - MaxRefFrames = maxRefFrames, - MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? false, - DeInterlace = deInterlace ?? false, - RequireNonAnamorphic = requireNonAnamorphic ?? false, - TranscodingMaxAudioChannels = transcodingMaxAudioChannels, - CpuCoreLimit = cpuCoreLimit, - LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, - VideoCodec = videoCodec, - SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodeReasons, - AudioStreamIndex = audioStreamIndex, - VideoStreamIndex = videoStreamIndex, - Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions - }; - - return await GetDynamicSegment(streamingRequest, segmentId) - .ConfigureAwait(false); - } - - /// - /// Gets a video stream using HTTP live streaming. - /// - /// The item id. - /// The playlist id. - /// The segment id. - /// The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. - /// The position of the requested segment in ticks. - /// The length of the requested segment in ticks. - /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. - /// The streaming parameters. - /// The tag. - /// Optional. The dlna device profile id to utilize. - /// The play session id. - /// The segment container. - /// The segment length. - /// The minimum number of segments. - /// The media version id, if playing an alternate version. - /// The device id of the client requesting. Used to stop encoding processes when needed. - /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. - /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. - /// Whether or not to allow copying of the video stream url. - /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. - /// Optional. Specify a specific audio sample rate, e.g. 44100. - /// Optional. The maximum audio bit depth. - /// Optional. The maximum streaming bitrate. - /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. - /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. - /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. - /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. - /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. - /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. - /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. - /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. - /// Optional. The fixed horizontal resolution of the encoded video. - /// Optional. The fixed vertical resolution of the encoded video. - /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. - /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. - /// Optional. Specify the subtitle delivery method. - /// Optional. - /// Optional. The maximum video bit depth. - /// Optional. Whether to require avc. - /// Optional. Whether to deinterlace the video. - /// Optional. Whether to require a non anamorphic stream. - /// Optional. The maximum number of audio channels to transcode. - /// Optional. The limit of how many cpu cores to use. - /// The live stream id. - /// Optional. Whether to enable the MpegtsM2Ts mode. - /// 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. - /// Optional. Specify a subtitle codec to encode to. - /// Optional. The transcoding reason. - /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. - /// Optional. The index of the video stream to use. If omitted the first video stream will be used. - /// Optional. The . - /// Optional. The streaming options. - /// Video stream returned. - /// A containing the audio file. - [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesAudioFile] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")] - public async Task GetHlsAudioSegment( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] string playlistId, - [FromRoute, Required] int segmentId, - [FromRoute, Required] string container, - [FromQuery, Required] long runtimeTicks, - [FromQuery, Required] long actualSegmentLengthTicks, - [FromQuery] bool? @static, - [FromQuery] string? @params, - [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, - [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, - [FromQuery] int? segmentLength, - [FromQuery] int? minSegments, - [FromQuery] string? mediaSourceId, - [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, - [FromQuery] bool? enableAutoStreamCopy, - [FromQuery] bool? allowVideoStreamCopy, - [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, - [FromQuery] int? audioSampleRate, - [FromQuery] int? maxAudioBitDepth, - [FromQuery] int? maxStreamingBitrate, - [FromQuery] int? audioBitRate, - [FromQuery] int? audioChannels, - [FromQuery] int? maxAudioChannels, - [FromQuery] string? profile, - [FromQuery] string? level, - [FromQuery] float? framerate, - [FromQuery] float? maxFramerate, - [FromQuery] bool? copyTimestamps, - [FromQuery] long? startTimeTicks, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? videoBitRate, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod? subtitleMethod, - [FromQuery] int? maxRefFrames, - [FromQuery] int? maxVideoBitDepth, - [FromQuery] bool? requireAvc, - [FromQuery] bool? deInterlace, - [FromQuery] bool? requireNonAnamorphic, - [FromQuery] int? transcodingMaxAudioChannels, - [FromQuery] int? cpuCoreLimit, - [FromQuery] string? liveStreamId, - [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodeReasons, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext? context, - [FromQuery] Dictionary streamOptions) - { - var streamingRequest = new StreamingRequestDto - { - Id = itemId, - Container = container, - CurrentRuntimeTicks = runtimeTicks, - ActualSegmentLengthTicks = actualSegmentLengthTicks, - Static = @static ?? false, - Params = @params, - Tag = tag, - DeviceProfileId = deviceProfileId, - PlaySessionId = playSessionId, - SegmentContainer = segmentContainer, - SegmentLength = segmentLength, - MinSegments = minSegments, - MediaSourceId = mediaSourceId, - DeviceId = deviceId, - AudioCodec = audioCodec, - EnableAutoStreamCopy = enableAutoStreamCopy ?? true, - AllowAudioStreamCopy = allowAudioStreamCopy ?? true, - AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, - AudioSampleRate = audioSampleRate, - MaxAudioChannels = maxAudioChannels, - AudioBitRate = audioBitRate ?? maxStreamingBitrate, - MaxAudioBitDepth = maxAudioBitDepth, - AudioChannels = audioChannels, - Profile = profile, - Level = level, - Framerate = framerate, - MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? false, - StartTimeTicks = startTimeTicks, - Width = width, - Height = height, - VideoBitRate = videoBitRate, - SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, - MaxRefFrames = maxRefFrames, - MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? false, - DeInterlace = deInterlace ?? false, - RequireNonAnamorphic = requireNonAnamorphic ?? false, - TranscodingMaxAudioChannels = transcodingMaxAudioChannels, - CpuCoreLimit = cpuCoreLimit, - LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, - VideoCodec = videoCodec, - SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodeReasons, - AudioStreamIndex = audioStreamIndex, - VideoStreamIndex = videoStreamIndex, - Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions - }; - - return await GetDynamicSegment(streamingRequest, segmentId) - .ConfigureAwait(false); - } - - private async Task GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, CancellationTokenSource cancellationTokenSource) - { - using var state = await StreamingHelpers.GetStreamingState( - streamingRequest, - HttpContext, - _mediaSourceManager, - _userManager, - _libraryManager, - _serverConfigurationManager, - _mediaEncoder, - _encodingHelper, - _dlnaManager, - _deviceManager, - _transcodingJobHelper, - TranscodingJobType, - cancellationTokenSource.Token) - .ConfigureAwait(false); - - var request = new CreateMainPlaylistRequest( - state.MediaPath, - state.SegmentLength * 1000, - state.RunTimeTicks ?? 0, - state.Request.SegmentContainer ?? string.Empty, - "hls1/main/", - Request.QueryString.ToString(), - EncodingHelper.IsCopyCodec(state.OutputVideoCodec)); - var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request); - - return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8")); - } - - private async Task GetDynamicSegment(StreamingRequestDto streamingRequest, int segmentId) - { - if ((streamingRequest.StartTimeTicks ?? 0) > 0) - { - throw new ArgumentException("StartTimeTicks is not allowed."); - } - - // CTS lifecycle is managed internally. - var cancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = cancellationTokenSource.Token; - - var state = await StreamingHelpers.GetStreamingState( - streamingRequest, - HttpContext, - _mediaSourceManager, - _userManager, - _libraryManager, - _serverConfigurationManager, - _mediaEncoder, - _encodingHelper, - _dlnaManager, - _deviceManager, - _transcodingJobHelper, - TranscodingJobType, - cancellationToken) - .ConfigureAwait(false); - - var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); - - var segmentPath = GetSegmentPath(state, playlistPath, segmentId); - - var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); - - TranscodingJobDto? job; - - if (System.IO.File.Exists(segmentPath)) - { - job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); - _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath); - return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); - } - var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false); - var released = false; - var startTranscoding = false; - try { - if (System.IO.File.Exists(segmentPath)) + if (!System.IO.File.Exists(playlistPath)) { - job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); - transcodingLock.Release(); - released = true; - _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath); - return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); - } - else - { - var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); - var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength; - - if (segmentId == -1) + // If the playlist doesn't already exist, startup ffmpeg + try { - _logger.LogDebug("Starting transcoding because fmp4 init file is being requested"); - startTranscoding = true; - segmentId = 0; - } - else if (currentTranscodingIndex is null) - { - _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null"); - startTranscoding = true; - } - else if (segmentId < currentTranscodingIndex.Value) - { - _logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex); - startTranscoding = true; - } - else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange) - { - _logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId); - startTranscoding = true; - } - - if (startTranscoding) - { - // If the playlist doesn't already exist, startup ffmpeg - try - { - await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false) - .ConfigureAwait(false); - - if (currentTranscodingIndex.HasValue) - { - DeleteLastFile(playlistPath, segmentExtension, 0); - } - - streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks; - - state.WaitForPath = segmentPath; - job = await _transcodingJobHelper.StartFfMpeg( + job = await _transcodingJobHelper.StartFfMpeg( state, playlistPath, - GetCommandLineArguments(playlistPath, state, false, segmentId), + GetCommandLineArguments(playlistPath, state, true, 0), Request, TranscodingJobType, - cancellationTokenSource).ConfigureAwait(false); - } - catch - { - state.Dispose(); - throw; - } - - // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false); + cancellationTokenSource) + .ConfigureAwait(false); + job.IsLiveOutput = true; } - else + catch { - job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); - if (job?.TranscodingThrottler is not null) - { - await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false); - } + state.Dispose(); + throw; + } + + minSegments = state.MinSegments; + if (minSegments > 0) + { + await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false); } } } finally { - if (!released) - { - transcodingLock.Release(); - } + transcodingLock.Release(); } + } - _logger.LogDebug("returning {0} [general case]", segmentPath); - job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + + if (job is not null) + { + _transcodingJobHelper.OnTranscodeEndRequest(job); + } + + var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state); + + return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8")); + } + + /// + /// Gets a video hls playlist stream. + /// + /// The item id. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment length. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. The maximum horizontal resolution of the encoded video. + /// Optional. The maximum vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamorphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// 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. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Enable adaptive bitrate streaming. + /// Video stream returned. + /// A containing the playlist file. + [HttpGet("Videos/{itemId}/master.m3u8")] + [HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] + public async Task GetMasterHlsVideoPlaylist( + [FromRoute, Required] Guid itemId, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery, Required] string mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodeReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary streamOptions, + [FromQuery] bool enableAdaptiveBitrateStreaming = true) + { + var streamingRequest = new HlsVideoRequestDto + { + Id = itemId, + Static = @static ?? false, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? false, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + MaxWidth = maxWidth, + MaxHeight = maxHeight, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodeReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Streaming, + StreamOptions = streamOptions, + EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming + }; + + return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); + } + + /// + /// Gets an audio hls playlist stream. + /// + /// The item id. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment length. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. The maximum streaming bitrate. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamorphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// 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. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Enable adaptive bitrate streaming. + /// Audio stream returned. + /// A containing the playlist file. + [HttpGet("Audio/{itemId}/master.m3u8")] + [HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] + public async Task GetMasterHlsAudioPlaylist( + [FromRoute, Required] Guid itemId, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery, Required] string mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? maxStreamingBitrate, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodeReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary streamOptions, + [FromQuery] bool enableAdaptiveBitrateStreaming = true) + { + var streamingRequest = new HlsAudioRequestDto + { + Id = itemId, + Static = @static ?? false, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate ?? maxStreamingBitrate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? false, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodeReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Streaming, + StreamOptions = streamOptions, + EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming + }; + + return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); + } + + /// + /// Gets a video stream using HTTP live streaming. + /// + /// The item id. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment length. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. The maximum horizontal resolution of the encoded video. + /// Optional. The maximum vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamorphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// 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. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Video stream returned. + /// A containing the audio file. + [HttpGet("Videos/{itemId}/main.m3u8")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] + public async Task GetVariantHlsVideoPlaylist( + [FromRoute, Required] Guid itemId, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodeReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary streamOptions) + { + using var cancellationTokenSource = new CancellationTokenSource(); + var streamingRequest = new VideoRequestDto + { + Id = itemId, + Static = @static ?? false, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? false, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + MaxWidth = maxWidth, + MaxHeight = maxHeight, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodeReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Streaming, + StreamOptions = streamOptions + }; + + return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) + .ConfigureAwait(false); + } + + /// + /// Gets an audio stream using HTTP live streaming. + /// + /// The item id. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment length. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. The maximum streaming bitrate. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamorphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// 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. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Audio stream returned. + /// A containing the audio file. + [HttpGet("Audio/{itemId}/main.m3u8")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] + public async Task GetVariantHlsAudioPlaylist( + [FromRoute, Required] Guid itemId, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? maxStreamingBitrate, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodeReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary streamOptions) + { + using var cancellationTokenSource = new CancellationTokenSource(); + var streamingRequest = new StreamingRequestDto + { + Id = itemId, + Static = @static ?? false, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate ?? maxStreamingBitrate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? false, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodeReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Streaming, + StreamOptions = streamOptions + }; + + return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) + .ConfigureAwait(false); + } + + /// + /// Gets a video stream using HTTP live streaming. + /// + /// The item id. + /// The playlist id. + /// The segment id. + /// The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. + /// The position of the requested segment in ticks. + /// The length of the requested segment in ticks. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The desired segment length. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. The maximum horizontal resolution of the encoded video. + /// Optional. The maximum vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamorphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// 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. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Video stream returned. + /// A containing the audio file. + [HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesVideoFile] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")] + public async Task GetHlsVideoSegment( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] string playlistId, + [FromRoute, Required] int segmentId, + [FromRoute, Required] string container, + [FromQuery, Required] long runtimeTicks, + [FromQuery, Required] long actualSegmentLengthTicks, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodeReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary streamOptions) + { + var streamingRequest = new VideoRequestDto + { + Id = itemId, + CurrentRuntimeTicks = runtimeTicks, + ActualSegmentLengthTicks = actualSegmentLengthTicks, + Container = container, + Static = @static ?? false, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? false, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + MaxWidth = maxWidth, + MaxHeight = maxHeight, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodeReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Streaming, + StreamOptions = streamOptions + }; + + return await GetDynamicSegment(streamingRequest, segmentId) + .ConfigureAwait(false); + } + + /// + /// Gets a video stream using HTTP live streaming. + /// + /// The item id. + /// The playlist id. + /// The segment id. + /// The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. + /// The position of the requested segment in ticks. + /// The length of the requested segment in ticks. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment length. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. The maximum streaming bitrate. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamorphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// 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. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Video stream returned. + /// A containing the audio file. + [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesAudioFile] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")] + public async Task GetHlsAudioSegment( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] string playlistId, + [FromRoute, Required] int segmentId, + [FromRoute, Required] string container, + [FromQuery, Required] long runtimeTicks, + [FromQuery, Required] long actualSegmentLengthTicks, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? maxStreamingBitrate, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodeReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary streamOptions) + { + var streamingRequest = new StreamingRequestDto + { + Id = itemId, + Container = container, + CurrentRuntimeTicks = runtimeTicks, + ActualSegmentLengthTicks = actualSegmentLengthTicks, + Static = @static ?? false, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate ?? maxStreamingBitrate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? false, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodeReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Streaming, + StreamOptions = streamOptions + }; + + return await GetDynamicSegment(streamingRequest, segmentId) + .ConfigureAwait(false); + } + + private async Task GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, CancellationTokenSource cancellationTokenSource) + { + using var state = await StreamingHelpers.GetStreamingState( + streamingRequest, + HttpContext, + _mediaSourceManager, + _userManager, + _libraryManager, + _serverConfigurationManager, + _mediaEncoder, + _encodingHelper, + _dlnaManager, + _deviceManager, + _transcodingJobHelper, + TranscodingJobType, + cancellationTokenSource.Token) + .ConfigureAwait(false); + + var request = new CreateMainPlaylistRequest( + state.MediaPath, + state.SegmentLength * 1000, + state.RunTimeTicks ?? 0, + state.Request.SegmentContainer ?? string.Empty, + "hls1/main/", + Request.QueryString.ToString(), + EncodingHelper.IsCopyCodec(state.OutputVideoCodec)); + var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request); + + return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8")); + } + + private async Task GetDynamicSegment(StreamingRequestDto streamingRequest, int segmentId) + { + if ((streamingRequest.StartTimeTicks ?? 0) > 0) + { + throw new ArgumentException("StartTimeTicks is not allowed."); + } + + // CTS lifecycle is managed internally. + var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + + var state = await StreamingHelpers.GetStreamingState( + streamingRequest, + HttpContext, + _mediaSourceManager, + _userManager, + _libraryManager, + _serverConfigurationManager, + _mediaEncoder, + _encodingHelper, + _dlnaManager, + _deviceManager, + _transcodingJobHelper, + TranscodingJobType, + cancellationToken) + .ConfigureAwait(false); + + var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); + + var segmentPath = GetSegmentPath(state, playlistPath, segmentId); + + var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); + + TranscodingJobDto? job; + + if (System.IO.File.Exists(segmentPath)) + { + job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); } - private static double[] GetSegmentLengths(StreamState state) - => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength); + var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); + await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false); + var released = false; + var startTranscoding = false; - internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength) + try { - var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks; - var wholeSegments = runtimeTicks / segmentLengthTicks; - var remainingTicks = runtimeTicks % segmentLengthTicks; - - var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1); - var segments = new double[segmentsLen]; - for (int i = 0; i < wholeSegments; i++) + if (System.IO.File.Exists(segmentPath)) { - segments[i] = segmentlength; - } - - if (remainingTicks != 0) - { - segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds; - } - - return segments; - } - - private string GetCommandLineArguments(string outputPath, StreamState state, bool isEventPlaylist, int startNumber) - { - var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); - var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); - - if (state.BaseRequest.BreakOnNonKeyFrames) - { - // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe - // breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable - // to produce a missing part of video stream before first keyframe is encountered, which may lead to - // awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js - _logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request"); - state.BaseRequest.BreakOnNonKeyFrames = false; - } - - var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; - - var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); - var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath); - var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension); - var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); - var outputTsArg = outputPrefix + "%d" + outputExtension; - - var segmentFormat = string.Empty; - var segmentContainer = outputExtension.TrimStart('.'); - var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer); - - if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase)) - { - segmentFormat = "mpegts"; - } - else if (string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)) - { - var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch - { - // on Windows, the path of fmp4 header file needs to be configured - true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"", - // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder - false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"" - }; - - segmentFormat = "fmp4" + outputFmp4HeaderArg; + job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + transcodingLock.Release(); + released = true; + _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath); + return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); } else { - _logger.LogError("Invalid HLS segment container: {SegmentContainer}, default to mpegts", segmentContainer); - segmentFormat = "mpegts"; + var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); + var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength; + + if (segmentId == -1) + { + _logger.LogDebug("Starting transcoding because fmp4 init file is being requested"); + startTranscoding = true; + segmentId = 0; + } + else if (currentTranscodingIndex is null) + { + _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null"); + startTranscoding = true; + } + else if (segmentId < currentTranscodingIndex.Value) + { + _logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex); + startTranscoding = true; + } + else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange) + { + _logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId); + startTranscoding = true; + } + + if (startTranscoding) + { + // If the playlist doesn't already exist, startup ffmpeg + try + { + await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false) + .ConfigureAwait(false); + + if (currentTranscodingIndex.HasValue) + { + DeleteLastFile(playlistPath, segmentExtension, 0); + } + + streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks; + + state.WaitForPath = segmentPath; + job = await _transcodingJobHelper.StartFfMpeg( + state, + playlistPath, + GetCommandLineArguments(playlistPath, state, false, segmentId), + Request, + TranscodingJobType, + cancellationTokenSource).ConfigureAwait(false); + } + catch + { + state.Dispose(); + throw; + } + + // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false); + } + else + { + job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + if (job?.TranscodingThrottler is not null) + { + await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false); + } + } } - - var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128 - ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) - : "128"; - - var baseUrlParam = string.Empty; - if (isEventPlaylist) + } + finally + { + if (!released) { - baseUrlParam = string.Format( - CultureInfo.InvariantCulture, - " -hls_base_url \"hls/{0}/\"", - Path.GetFileNameWithoutExtension(outputPath)); + transcodingLock.Release(); } - - return string.Format( - CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{12}\" -hls_playlist_type {11} -hls_list_size 0 -y \"{13}\"", - inputModifier, - _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer), - threads, - mapArgs, - GetVideoArguments(state, startNumber, isEventPlaylist), - GetAudioArguments(state), - maxMuxingQueueSize, - state.SegmentLength.ToString(CultureInfo.InvariantCulture), - segmentFormat, - startNumber.ToString(CultureInfo.InvariantCulture), - baseUrlParam, - isEventPlaylist ? "event" : "vod", - outputTsArg, - outputPath).Trim(); } - /// - /// Gets the audio arguments for transcoding. - /// - /// The . - /// The command line arguments for audio transcoding. - private string GetAudioArguments(StreamState state) + _logger.LogDebug("returning {0} [general case]", segmentPath); + job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); + } + + private static double[] GetSegmentLengths(StreamState state) + => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength); + + internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength) + { + var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks; + var wholeSegments = runtimeTicks / segmentLengthTicks; + var remainingTicks = runtimeTicks % segmentLengthTicks; + + var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1); + var segments = new double[segmentsLen]; + for (int i = 0; i < wholeSegments; i++) { - if (state.AudioStream is null) + segments[i] = segmentlength; + } + + if (remainingTicks != 0) + { + segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds; + } + + return segments; + } + + private string GetCommandLineArguments(string outputPath, StreamState state, bool isEventPlaylist, int startNumber) + { + var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); + var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); + + if (state.BaseRequest.BreakOnNonKeyFrames) + { + // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe + // breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable + // to produce a missing part of video stream before first keyframe is encountered, which may lead to + // awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js + _logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request"); + state.BaseRequest.BreakOnNonKeyFrames = false; + } + + var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; + + var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); + var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath); + var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension); + var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); + var outputTsArg = outputPrefix + "%d" + outputExtension; + + var segmentFormat = string.Empty; + var segmentContainer = outputExtension.TrimStart('.'); + var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer); + + if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase)) + { + segmentFormat = "mpegts"; + } + else if (string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)) + { + var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch { - return string.Empty; - } + // on Windows, the path of fmp4 header file needs to be configured + true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"", + // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder + false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"" + }; - var audioCodec = _encodingHelper.GetAudioEncoder(state); + segmentFormat = "fmp4" + outputFmp4HeaderArg; + } + else + { + _logger.LogError("Invalid HLS segment container: {SegmentContainer}, default to mpegts", segmentContainer); + segmentFormat = "mpegts"; + } - if (!state.IsOutputVideo) - { - if (EncodingHelper.IsCopyCodec(audioCodec)) - { - var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); + var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128 + ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) + : "128"; - return "-acodec copy -strict -2" + bitStreamArgs; - } + var baseUrlParam = string.Empty; + if (isEventPlaylist) + { + baseUrlParam = string.Format( + CultureInfo.InvariantCulture, + " -hls_base_url \"hls/{0}/\"", + Path.GetFileNameWithoutExtension(outputPath)); + } - var audioTranscodeParams = string.Empty; + return string.Format( + CultureInfo.InvariantCulture, + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{12}\" -hls_playlist_type {11} -hls_list_size 0 -y \"{13}\"", + inputModifier, + _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer), + threads, + mapArgs, + GetVideoArguments(state, startNumber, isEventPlaylist), + GetAudioArguments(state), + maxMuxingQueueSize, + state.SegmentLength.ToString(CultureInfo.InvariantCulture), + segmentFormat, + startNumber.ToString(CultureInfo.InvariantCulture), + baseUrlParam, + isEventPlaylist ? "event" : "vod", + outputTsArg, + outputPath).Trim(); + } - audioTranscodeParams += "-acodec " + audioCodec; + /// + /// Gets the audio arguments for transcoding. + /// + /// The . + /// The command line arguments for audio transcoding. + private string GetAudioArguments(StreamState state) + { + if (state.AudioStream is null) + { + return string.Empty; + } - if (state.OutputAudioBitrate.HasValue) - { - audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture); - } - - if (state.OutputAudioChannels.HasValue) - { - audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture); - } - - if (state.OutputAudioSampleRate.HasValue) - { - audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); - } - - audioTranscodeParams += " -vn"; - return audioTranscodeParams; - } - - // dts, flac, opus and truehd are experimental in mp4 muxer - var strictArgs = string.Empty; - - if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) - { - strictArgs = " -strict -2"; - } + var audioCodec = _encodingHelper.GetAudioEncoder(state); + if (!state.IsOutputVideo) + { if (EncodingHelper.IsCopyCodec(audioCodec)) { - var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); - var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs; - if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) - { - return copyArgs + " -copypriorss:a:0 0"; - } - - return copyArgs; + return "-acodec copy -strict -2" + bitStreamArgs; } - var args = "-codec:a:0 " + audioCodec + strictArgs; + var audioTranscodeParams = string.Empty; - var channels = state.OutputAudioChannels; + audioTranscodeParams += "-acodec " + audioCodec; - if (channels.HasValue - && (channels.Value != 2 - || (state.AudioStream is not null - && state.AudioStream.Channels.HasValue - && state.AudioStream.Channels.Value > 5 - && _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None))) + if (state.OutputAudioBitrate.HasValue) { - args += " -ac " + channels.Value; + audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture); } - var bitrate = state.OutputAudioBitrate; - - if (bitrate.HasValue) + if (state.OutputAudioChannels.HasValue) { - args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); + audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture); } if (state.OutputAudioSampleRate.HasValue) { - args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); + audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); } - args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions); - - return args; + audioTranscodeParams += " -vn"; + return audioTranscodeParams; } - /// - /// Gets the video arguments for transcoding. - /// - /// The . - /// The first number in the hls sequence. - /// Whether the playlist is EVENT or VOD. - /// The command line arguments for video transcoding. - private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist) + // dts, flac, opus and truehd are experimental in mp4 muxer + var strictArgs = string.Empty; + + if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) { - if (state.VideoStream is null) + strictArgs = " -strict -2"; + } + + if (EncodingHelper.IsCopyCodec(audioCodec)) + { + var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); + var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); + var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs; + + if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) { - return string.Empty; + return copyArgs + " -copypriorss:a:0 0"; } - if (!state.IsOutputVideo) + return copyArgs; + } + + var args = "-codec:a:0 " + audioCodec + strictArgs; + + var channels = state.OutputAudioChannels; + + if (channels.HasValue + && (channels.Value != 2 + || (state.AudioStream is not null + && state.AudioStream.Channels.HasValue + && state.AudioStream.Channels.Value > 5 + && _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None))) + { + args += " -ac " + channels.Value; + } + + var bitrate = state.OutputAudioBitrate; + + if (bitrate.HasValue) + { + args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); + } + + if (state.OutputAudioSampleRate.HasValue) + { + args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); + } + + args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions); + + return args; + } + + /// + /// Gets the video arguments for transcoding. + /// + /// The . + /// The first number in the hls sequence. + /// Whether the playlist is EVENT or VOD. + /// The command line arguments for video transcoding. + private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist) + { + if (state.VideoStream is null) + { + return string.Empty; + } + + if (!state.IsOutputVideo) + { + return string.Empty; + } + + var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); + + var args = "-codec:v:0 " + codec; + + if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) + { + if (EncodingHelper.IsCopyCodec(codec) + && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase))) { - return string.Empty; - } - - var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); - - var args = "-codec:v:0 " + codec; - - if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) - { - if (EncodingHelper.IsCopyCodec(codec) - && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase))) - { - // Prefer dvh1 to dvhe - args += " -tag:v:0 dvh1 -strict -2"; - } - else - { - // Prefer hvc1 to hev1 - args += " -tag:v:0 hvc1"; - } - } - - // if (state.EnableMpegtsM2TsMode) - // { - // args += " -mpegts_m2ts_mode 1"; - // } - - // See if we can save come cpu cycles by avoiding encoding. - if (EncodingHelper.IsCopyCodec(codec)) - { - // If h264_mp4toannexb is ever added, do not use it for live tv. - if (state.VideoStream is not null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) - { - string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream); - if (!string.IsNullOrEmpty(bitStreamArgs)) - { - args += " " + bitStreamArgs; - } - } - - args += " -start_at_zero"; + // Prefer dvh1 to dvhe + args += " -tag:v:0 dvh1 -strict -2"; } else { - args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, isEventPlaylist ? DefaultEventEncoderPreset : DefaultVodEncoderPreset); + // Prefer hvc1 to hev1 + args += " -tag:v:0 hvc1"; + } + } - // Set the key frame params for video encoding to match the hls segment time. - args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber); + // if (state.EnableMpegtsM2TsMode) + // { + // args += " -mpegts_m2ts_mode 1"; + // } - // Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now. - if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) + // See if we can save come cpu cycles by avoiding encoding. + if (EncodingHelper.IsCopyCodec(codec)) + { + // If h264_mp4toannexb is ever added, do not use it for live tv. + if (state.VideoStream is not null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) + { + string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream); + if (!string.IsNullOrEmpty(bitStreamArgs)) { - args += " -bf 0"; + args += " " + bitStreamArgs; } + } - // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; + args += " -start_at_zero"; + } + else + { + args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, isEventPlaylist ? DefaultEventEncoderPreset : DefaultVodEncoderPreset); - // video processing filters. - args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec); + // Set the key frame params for video encoding to match the hls segment time. + args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber); - // -start_at_zero is necessary to use with -ss when seeking, - // otherwise the target position cannot be determined. - if (state.SubtitleStream is not null) + // Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now. + if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) + { + args += " -bf 0"; + } + + // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; + + // video processing filters. + args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec); + + // -start_at_zero is necessary to use with -ss when seeking, + // otherwise the target position cannot be determined. + if (state.SubtitleStream is not null) + { + // Disable start_at_zero for external graphical subs + if (!(state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) { - // Disable start_at_zero for external graphical subs - if (!(state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) + args += " -start_at_zero"; + } + } + } + + // TODO why was this not enabled for VOD? + if (isEventPlaylist) + { + args += " -flags -global_header"; + } + + if (!string.IsNullOrEmpty(state.OutputVideoSync)) + { + args += " -vsync " + state.OutputVideoSync; + } + + args += _encodingHelper.GetOutputFFlags(state); + + return args; + } + + private string GetSegmentPath(StreamState state, string playlist, int index) + { + var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist)); + var filename = Path.GetFileNameWithoutExtension(playlist); + + return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer)); + } + + private async Task GetSegmentResult( + StreamState state, + string playlistPath, + string segmentPath, + string segmentExtension, + int segmentIndex, + TranscodingJobDto? transcodingJob, + CancellationToken cancellationToken) + { + var segmentExists = System.IO.File.Exists(segmentPath); + if (segmentExists) + { + if (transcodingJob is not null && transcodingJob.HasExited) + { + // Transcoding job is over, so assume all existing files are ready + _logger.LogDebug("serving up {0} as transcode is over", segmentPath); + return GetSegmentResult(state, segmentPath, transcodingJob); + } + + var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); + + // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready + if (segmentIndex < currentTranscodingIndex) + { + _logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex); + return GetSegmentResult(state, segmentPath, transcodingJob); + } + } + + var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1); + if (transcodingJob is not null) + { + while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited) + { + // To be considered ready, the segment file has to exist AND + // either the transcoding job should be done or next segment should also exist + if (segmentExists) + { + if (transcodingJob.HasExited || System.IO.File.Exists(nextSegmentPath)) { - args += " -start_at_zero"; + _logger.LogDebug("Serving up {SegmentPath} as it deemed ready", segmentPath); + return GetSegmentResult(state, segmentPath, transcodingJob); } } - } - - // TODO why was this not enabled for VOD? - if (isEventPlaylist) - { - args += " -flags -global_header"; - } - - if (!string.IsNullOrEmpty(state.OutputVideoSync)) - { - args += " -vsync " + state.OutputVideoSync; - } - - args += _encodingHelper.GetOutputFFlags(state); - - return args; - } - - private string GetSegmentPath(StreamState state, string playlist, int index) - { - var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist)); - var filename = Path.GetFileNameWithoutExtension(playlist); - - return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer)); - } - - private async Task GetSegmentResult( - StreamState state, - string playlistPath, - string segmentPath, - string segmentExtension, - int segmentIndex, - TranscodingJobDto? transcodingJob, - CancellationToken cancellationToken) - { - var segmentExists = System.IO.File.Exists(segmentPath); - if (segmentExists) - { - if (transcodingJob is not null && transcodingJob.HasExited) + else { - // Transcoding job is over, so assume all existing files are ready - _logger.LogDebug("serving up {0} as transcode is over", segmentPath); - return GetSegmentResult(state, segmentPath, transcodingJob); - } - - var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); - - // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready - if (segmentIndex < currentTranscodingIndex) - { - _logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex); - return GetSegmentResult(state, segmentPath, transcodingJob); - } - } - - var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1); - if (transcodingJob is not null) - { - while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited) - { - // To be considered ready, the segment file has to exist AND - // either the transcoding job should be done or next segment should also exist + segmentExists = System.IO.File.Exists(segmentPath); if (segmentExists) { - if (transcodingJob.HasExited || System.IO.File.Exists(nextSegmentPath)) - { - _logger.LogDebug("Serving up {SegmentPath} as it deemed ready", segmentPath); - return GetSegmentResult(state, segmentPath, transcodingJob); - } + continue; // avoid unnecessary waiting if segment just became available } - else - { - segmentExists = System.IO.File.Exists(segmentPath); - if (segmentExists) - { - continue; // avoid unnecessary waiting if segment just became available - } - } - - await Task.Delay(100, cancellationToken).ConfigureAwait(false); } - if (!System.IO.File.Exists(segmentPath)) - { - _logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath); - } - else - { - _logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath); - } + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } - cancellationToken.ThrowIfCancellationRequested(); + if (!System.IO.File.Exists(segmentPath)) + { + _logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath); } else { - _logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath); + _logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath); } - return GetSegmentResult(state, segmentPath, transcodingJob); + cancellationToken.ThrowIfCancellationRequested(); + } + else + { + _logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath); } - private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJobDto? transcodingJob) + return GetSegmentResult(state, segmentPath, transcodingJob); + } + + private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJobDto? transcodingJob) + { + var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks; + + Response.OnCompleted(() => { - var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks; - - Response.OnCompleted(() => + _logger.LogDebug("Finished serving {SegmentPath}", segmentPath); + if (transcodingJob is not null) { - _logger.LogDebug("Finished serving {SegmentPath}", segmentPath); - if (transcodingJob is not null) - { - transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); - _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob); - } + transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); + _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob); + } - return Task.CompletedTask; - }); + return Task.CompletedTask; + }); - return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)); + return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)); + } + + private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) + { + var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType); + + if (job is null || job.HasExited) + { + return null; } - private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) + var file = GetLastTranscodingFile(playlist, segmentExtension, _fileSystem); + + if (file is null) { - var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType); - - if (job is null || job.HasExited) - { - return null; - } - - var file = GetLastTranscodingFile(playlist, segmentExtension, _fileSystem); - - if (file is null) - { - return null; - } - - var playlistFilename = Path.GetFileNameWithoutExtension(playlist); - - var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); - - return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture); + return null; } - private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem) + var playlistFilename = Path.GetFileNameWithoutExtension(playlist); + + var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); + + return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + + private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem) + { + var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist)); + + var filePrefix = Path.GetFileNameWithoutExtension(playlist); + + try { - var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist)); + return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false) + .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(fileSystem.GetLastWriteTimeUtc) + .FirstOrDefault(); + } + catch (IOException) + { + return null; + } + } - var filePrefix = Path.GetFileNameWithoutExtension(playlist); + private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount) + { + var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem); - try - { - return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false) - .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase)) - .OrderByDescending(fileSystem.GetLastWriteTimeUtc) - .FirstOrDefault(); - } - catch (IOException) - { - return null; - } + if (file is not null) + { + DeleteFile(file.FullName, retryCount); + } + } + + private void DeleteFile(string path, int retryCount) + { + if (retryCount >= 5) + { + return; } - private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount) - { - var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem); + _logger.LogDebug("Deleting partial HLS file {Path}", path); - if (file is not null) - { - DeleteFile(file.FullName, retryCount); - } + try + { + _fileSystem.DeleteFile(path); } - - private void DeleteFile(string path, int retryCount) + catch (IOException ex) { - if (retryCount >= 5) - { - return; - } + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); - _logger.LogDebug("Deleting partial HLS file {Path}", path); - - try - { - _fileSystem.DeleteFile(path); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); - - var task = Task.Delay(100); - task.Wait(); - DeleteFile(path, retryCount + 1); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); - } + var task = Task.Delay(100); + task.Wait(); + DeleteFile(path, retryCount + 1); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); } } } diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 6c78a79875..8c9ee1a19e 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -12,186 +12,185 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Environment Controller. +/// +[Authorize(Policy = Policies.FirstTimeSetupOrElevated)] +public class EnvironmentController : BaseJellyfinApiController { + private const char UncSeparator = '\\'; + private const string UncStartPrefix = @"\\"; + + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + /// - /// Environment Controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] - public class EnvironmentController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + public EnvironmentController(IFileSystem fileSystem, ILogger logger) { - private const char UncSeparator = '\\'; - private const string UncStartPrefix = @"\\"; + _fileSystem = fileSystem; + _logger = logger; + } - private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public EnvironmentController(IFileSystem fileSystem, ILogger logger) + /// + /// Gets the contents of a given directory in the file system. + /// + /// The path. + /// An optional filter to include or exclude files from the results. true/false. + /// An optional filter to include or exclude folders from the results. true/false. + /// Directory contents returned. + /// Directory contents. + [HttpGet("DirectoryContents")] + [ProducesResponseType(StatusCodes.Status200OK)] + public IEnumerable GetDirectoryContents( + [FromQuery, Required] string path, + [FromQuery] bool includeFiles = false, + [FromQuery] bool includeDirectories = false) + { + if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase) + && path.LastIndexOf(UncSeparator) == 1) { - _fileSystem = fileSystem; - _logger = logger; + return Array.Empty(); } - /// - /// Gets the contents of a given directory in the file system. - /// - /// The path. - /// An optional filter to include or exclude files from the results. true/false. - /// An optional filter to include or exclude folders from the results. true/false. - /// Directory contents returned. - /// Directory contents. - [HttpGet("DirectoryContents")] - [ProducesResponseType(StatusCodes.Status200OK)] - public IEnumerable GetDirectoryContents( - [FromQuery, Required] string path, - [FromQuery] bool includeFiles = false, - [FromQuery] bool includeDirectories = false) + var entries = + _fileSystem.GetFileSystemEntries(path) + .Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles)) + .OrderBy(i => i.FullName); + + return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File)); + } + + /// + /// Validates path. + /// + /// Validate request object. + /// Path validated. + /// Path not found. + /// Validation status. + [HttpPost("ValidatePath")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto) + { + if (validatePathDto.IsFile.HasValue) { - if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase) - && path.LastIndexOf(UncSeparator) == 1) + if (validatePathDto.IsFile.Value) { - return Array.Empty(); - } - - var entries = - _fileSystem.GetFileSystemEntries(path) - .Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles)) - .OrderBy(i => i.FullName); - - return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File)); - } - - /// - /// Validates path. - /// - /// Validate request object. - /// Path validated. - /// Path not found. - /// Validation status. - [HttpPost("ValidatePath")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto) - { - if (validatePathDto.IsFile.HasValue) - { - if (validatePathDto.IsFile.Value) + if (!System.IO.File.Exists(validatePathDto.Path)) { - if (!System.IO.File.Exists(validatePathDto.Path)) - { - return NotFound(); - } - } - else - { - if (!Directory.Exists(validatePathDto.Path)) - { - return NotFound(); - } + return NotFound(); } } else { - if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path)) + if (!Directory.Exists(validatePathDto.Path)) { return NotFound(); } - - if (validatePathDto.ValidateWritable) - { - if (validatePathDto.Path is null) - { - throw new ResourceNotFoundException(nameof(validatePathDto.Path)); - } - - var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString()); - try - { - System.IO.File.WriteAllText(file, string.Empty); - } - finally - { - if (System.IO.File.Exists(file)) - { - System.IO.File.Delete(file); - } - } - } } - - return NoContent(); } - - /// - /// Gets network paths. - /// - /// Empty array returned. - /// List of entries. - [Obsolete("This endpoint is obsolete.")] - [HttpGet("NetworkShares")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetNetworkShares() + else { - _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares"); - return Array.Empty(); - } - - /// - /// Gets available drives from the server's file system. - /// - /// List of entries returned. - /// List of entries. - [HttpGet("Drives")] - [ProducesResponseType(StatusCodes.Status200OK)] - public IEnumerable GetDrives() - { - return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory)); - } - - /// - /// Gets the parent path of a given path. - /// - /// The path. - /// Parent path. - [HttpGet("ParentPath")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetParentPath([FromQuery, Required] string path) - { - string? parent = Path.GetDirectoryName(path); - if (string.IsNullOrEmpty(parent)) + if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path)) { - // Check if unc share - var index = path.LastIndexOf(UncSeparator); + return NotFound(); + } - if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0) + if (validatePathDto.ValidateWritable) + { + if (validatePathDto.Path is null) { - parent = path.Substring(0, index); + throw new ResourceNotFoundException(nameof(validatePathDto.Path)); + } - if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator))) + var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString()); + try + { + System.IO.File.WriteAllText(file, string.Empty); + } + finally + { + if (System.IO.File.Exists(file)) { - parent = null; + System.IO.File.Delete(file); } } } - - return parent; } - /// - /// Get Default directory browser. - /// - /// Default directory browser returned. - /// Default directory browser. - [HttpGet("DefaultDirectoryBrowser")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetDefaultDirectoryBrowser() + return NoContent(); + } + + /// + /// Gets network paths. + /// + /// Empty array returned. + /// List of entries. + [Obsolete("This endpoint is obsolete.")] + [HttpGet("NetworkShares")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetNetworkShares() + { + _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares"); + return Array.Empty(); + } + + /// + /// Gets available drives from the server's file system. + /// + /// List of entries returned. + /// List of entries. + [HttpGet("Drives")] + [ProducesResponseType(StatusCodes.Status200OK)] + public IEnumerable GetDrives() + { + return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory)); + } + + /// + /// Gets the parent path of a given path. + /// + /// The path. + /// Parent path. + [HttpGet("ParentPath")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetParentPath([FromQuery, Required] string path) + { + string? parent = Path.GetDirectoryName(path); + if (string.IsNullOrEmpty(parent)) { - return new DefaultDirectoryBrowserInfoDto(); + // Check if unc share + var index = path.LastIndexOf(UncSeparator); + + if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0) + { + parent = path.Substring(0, index); + + if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator))) + { + parent = null; + } + } } + + return parent; + } + + /// + /// Get Default directory browser. + /// + /// Default directory browser returned. + /// Default directory browser. + [HttpGet("DefaultDirectoryBrowser")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDefaultDirectoryBrowser() + { + return new DefaultDirectoryBrowserInfoDto(); } } diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 17d136384e..2378aada5f 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -12,205 +12,204 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Filters controller. +/// +[Route("")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class FilterController : BaseJellyfinApiController { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + /// - /// Filters controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class FilterController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + public FilterController(ILibraryManager libraryManager, IUserManager userManager) { - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; + _libraryManager = libraryManager; + _userManager = userManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public FilterController(ILibraryManager libraryManager, IUserManager userManager) + /// + /// Gets legacy query filters. + /// + /// Optional. User id. + /// Optional. Parent id. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. Filter by MediaType. Allows multiple, comma delimited. + /// Legacy filters retrieved. + /// Legacy query filters. + [HttpGet("Items/Filters")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetQueryFiltersLegacy( + [FromQuery] Guid? userId, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + BaseItem? item = null; + if (includeItemTypes.Length != 1 + || !(includeItemTypes[0] == BaseItemKind.BoxSet + || includeItemTypes[0] == BaseItemKind.Playlist + || includeItemTypes[0] == BaseItemKind.Trailer + || includeItemTypes[0] == BaseItemKind.Program)) { - _libraryManager = libraryManager; - _userManager = userManager; + item = _libraryManager.GetParentItem(parentId, user?.Id); } - /// - /// Gets legacy query filters. - /// - /// Optional. User id. - /// Optional. Parent id. - /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional. Filter by MediaType. Allows multiple, comma delimited. - /// Legacy filters retrieved. - /// Legacy query filters. - [HttpGet("Items/Filters")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetQueryFiltersLegacy( - [FromQuery] Guid? userId, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) + var query = new InternalItemsQuery { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - BaseItem? item = null; - if (includeItemTypes.Length != 1 - || !(includeItemTypes[0] == BaseItemKind.BoxSet - || includeItemTypes[0] == BaseItemKind.Playlist - || includeItemTypes[0] == BaseItemKind.Trailer - || includeItemTypes[0] == BaseItemKind.Program)) + User = user, + MediaTypes = mediaTypes, + IncludeItemTypes = includeItemTypes, + Recursive = true, + EnableTotalRecordCount = false, + DtoOptions = new DtoOptions { - item = _libraryManager.GetParentItem(parentId, user?.Id); + Fields = new[] { ItemFields.Genres, ItemFields.Tags }, + EnableImages = false, + EnableUserData = false } + }; - var query = new InternalItemsQuery - { - User = user, - MediaTypes = mediaTypes, - IncludeItemTypes = includeItemTypes, - Recursive = true, - EnableTotalRecordCount = false, - DtoOptions = new DtoOptions - { - Fields = new[] { ItemFields.Genres, ItemFields.Tags }, - EnableImages = false, - EnableUserData = false - } - }; - - if (item is not Folder folder) - { - return new QueryFiltersLegacy(); - } - - var itemList = folder.GetItemList(query); - return new QueryFiltersLegacy - { - Years = itemList.Select(i => i.ProductionYear ?? -1) - .Where(i => i > 0) - .Distinct() - .Order() - .ToArray(), - - Genres = itemList.SelectMany(i => i.Genres) - .DistinctNames() - .Order() - .ToArray(), - - Tags = itemList - .SelectMany(i => i.Tags) - .Distinct(StringComparer.OrdinalIgnoreCase) - .Order() - .ToArray(), - - OfficialRatings = itemList - .Select(i => i.OfficialRating) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .Order() - .ToArray() - }; + if (item is not Folder folder) + { + return new QueryFiltersLegacy(); } - /// - /// Gets query filters. - /// - /// Optional. User id. - /// Optional. Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional. Is item airing. - /// Optional. Is item movie. - /// Optional. Is item sports. - /// Optional. Is item kids. - /// Optional. Is item news. - /// Optional. Is item series. - /// Optional. Search recursive. - /// Filters retrieved. - /// Query filters. - [HttpGet("Items/Filters2")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetQueryFilters( - [FromQuery] Guid? userId, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery] bool? isAiring, - [FromQuery] bool? isMovie, - [FromQuery] bool? isSports, - [FromQuery] bool? isKids, - [FromQuery] bool? isNews, - [FromQuery] bool? isSeries, - [FromQuery] bool? recursive) + var itemList = folder.GetItemList(query); + return new QueryFiltersLegacy { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); + Years = itemList.Select(i => i.ProductionYear ?? -1) + .Where(i => i > 0) + .Distinct() + .Order() + .ToArray(), - BaseItem? parentItem = null; - if (includeItemTypes.Length == 1 - && (includeItemTypes[0] == BaseItemKind.BoxSet - || includeItemTypes[0] == BaseItemKind.Playlist - || includeItemTypes[0] == BaseItemKind.Trailer - || includeItemTypes[0] == BaseItemKind.Program)) - { - parentItem = null; - } - else if (parentId.HasValue) - { - parentItem = _libraryManager.GetItemById(parentId.Value); - } + Genres = itemList.SelectMany(i => i.Genres) + .DistinctNames() + .Order() + .ToArray(), - var filters = new QueryFilters(); - var genreQuery = new InternalItemsQuery(user) - { - IncludeItemTypes = includeItemTypes, - DtoOptions = new DtoOptions - { - Fields = Array.Empty(), - EnableImages = false, - EnableUserData = false - }, - IsAiring = isAiring, - IsMovie = isMovie, - IsSports = isSports, - IsKids = isKids, - IsNews = isNews, - IsSeries = isSeries - }; + Tags = itemList + .SelectMany(i => i.Tags) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Order() + .ToArray(), - if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder) - { - genreQuery.AncestorIds = parentItem is null ? Array.Empty() : new[] { parentItem.Id }; - } - else - { - genreQuery.Parent = parentItem; - } + OfficialRatings = itemList + .Select(i => i.OfficialRating) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Order() + .ToArray() + }; + } - if (includeItemTypes.Length == 1 - && (includeItemTypes[0] == BaseItemKind.MusicAlbum - || includeItemTypes[0] == BaseItemKind.MusicVideo - || includeItemTypes[0] == BaseItemKind.MusicArtist - || includeItemTypes[0] == BaseItemKind.Audio)) - { - filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair - { - Name = i.Item.Name, - Id = i.Item.Id - }).ToArray(); - } - else - { - filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair - { - Name = i.Item.Name, - Id = i.Item.Id - }).ToArray(); - } + /// + /// Gets query filters. + /// + /// Optional. User id. + /// Optional. Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. Is item airing. + /// Optional. Is item movie. + /// Optional. Is item sports. + /// Optional. Is item kids. + /// Optional. Is item news. + /// Optional. Is item series. + /// Optional. Search recursive. + /// Filters retrieved. + /// Query filters. + [HttpGet("Items/Filters2")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetQueryFilters( + [FromQuery] Guid? userId, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery] bool? isAiring, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSports, + [FromQuery] bool? isKids, + [FromQuery] bool? isNews, + [FromQuery] bool? isSeries, + [FromQuery] bool? recursive) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); - return filters; + BaseItem? parentItem = null; + if (includeItemTypes.Length == 1 + && (includeItemTypes[0] == BaseItemKind.BoxSet + || includeItemTypes[0] == BaseItemKind.Playlist + || includeItemTypes[0] == BaseItemKind.Trailer + || includeItemTypes[0] == BaseItemKind.Program)) + { + parentItem = null; } + else if (parentId.HasValue) + { + parentItem = _libraryManager.GetItemById(parentId.Value); + } + + var filters = new QueryFilters(); + var genreQuery = new InternalItemsQuery(user) + { + IncludeItemTypes = includeItemTypes, + DtoOptions = new DtoOptions + { + Fields = Array.Empty(), + EnableImages = false, + EnableUserData = false + }, + IsAiring = isAiring, + IsMovie = isMovie, + IsSports = isSports, + IsKids = isKids, + IsNews = isNews, + IsSeries = isSeries + }; + + if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder) + { + genreQuery.AncestorIds = parentItem is null ? Array.Empty() : new[] { parentItem.Id }; + } + else + { + genreQuery.Parent = parentItem; + } + + if (includeItemTypes.Length == 1 + && (includeItemTypes[0] == BaseItemKind.MusicAlbum + || includeItemTypes[0] == BaseItemKind.MusicVideo + || includeItemTypes[0] == BaseItemKind.MusicArtist + || includeItemTypes[0] == BaseItemKind.Audio)) + { + filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair + { + Name = i.Item.Name, + Id = i.Item.Id + }).ToArray(); + } + else + { + filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair + { + Name = i.Item.Name, + Id = i.Item.Id + }).ToArray(); + } + + return filters; } } diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 611643bd8a..28ebe20475 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -18,194 +18,193 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Genre = MediaBrowser.Controller.Entities.Genre; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The genres controller. +/// +[Authorize(Policy = Policies.DefaultAuthorization)] +public class GenresController : BaseJellyfinApiController { + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + /// - /// The genres controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.DefaultAuthorization)] - public class GenresController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public GenresController( + IUserManager userManager, + ILibraryManager libraryManager, + IDtoService dtoService) { - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly IDtoService _dtoService; + _userManager = userManager; + _libraryManager = libraryManager; + _dtoService = dtoService; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public GenresController( - IUserManager userManager, - ILibraryManager libraryManager, - IDtoService dtoService) + /// + /// Gets all genres from a given item, folder, or the entire library. + /// + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// The search term. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited. + /// Optional filter by items that are marked as favorite, or not. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// User id. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional. Specify one or more sort orders, comma delimited. + /// Sort Order - Ascending,Descending. + /// Optional, include image information in output. + /// Optional. Include total record count. + /// Genres returned. + /// An containing the queryresult of genres. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetGenres( + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string? searchTerm, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery] bool? isFavorite, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] Guid? userId, + [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) + { + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); + + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var parentItem = _libraryManager.GetParentItem(parentId, userId); + + var query = new InternalItemsQuery(user) { - _userManager = userManager; - _libraryManager = libraryManager; - _dtoService = dtoService; - } + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + StartIndex = startIndex, + Limit = limit, + IsFavorite = isFavorite, + NameLessThan = nameLessThan, + NameStartsWith = nameStartsWith, + NameStartsWithOrGreater = nameStartsWithOrGreater, + DtoOptions = dtoOptions, + SearchTerm = searchTerm, + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) + }; - /// - /// Gets all genres from a given item, folder, or the entire library. - /// - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// The search term. - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. - /// Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited. - /// Optional filter by items that are marked as favorite, or not. - /// Optional, the max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// User id. - /// Optional filter by items whose name is sorted equally or greater than a given input string. - /// Optional filter by items whose name is sorted equally than a given input string. - /// Optional filter by items whose name is equally or lesser than a given input string. - /// Optional. Specify one or more sort orders, comma delimited. - /// Sort Order - Ascending,Descending. - /// Optional, include image information in output. - /// Optional. Include total record count. - /// Genres returned. - /// An containing the queryresult of genres. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetGenres( - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] string? searchTerm, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery] bool? isFavorite, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] Guid? userId, - [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) + if (parentId.HasValue) { - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - - User? user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var parentItem = _libraryManager.GetParentItem(parentId, userId); - - var query = new InternalItemsQuery(user) + if (parentItem is Folder) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, - StartIndex = startIndex, - Limit = limit, - IsFavorite = isFavorite, - NameLessThan = nameLessThan, - NameStartsWith = nameStartsWith, - NameStartsWithOrGreater = nameStartsWithOrGreater, - DtoOptions = dtoOptions, - SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount, - OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) - }; - - if (parentId.HasValue) - { - if (parentItem is Folder) - { - query.AncestorIds = new[] { parentId.Value }; - } - else - { - query.ItemIds = new[] { parentId.Value }; - } - } - - QueryResult<(BaseItem, ItemCounts)> result; - if (parentItem is ICollectionFolder parentCollectionFolder - && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal) - || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal))) - { - result = _libraryManager.GetMusicGenres(query); + query.AncestorIds = new[] { parentId.Value }; } else { - result = _libraryManager.GetGenres(query); + query.ItemIds = new[] { parentId.Value }; } - - var shouldIncludeItemTypes = includeItemTypes.Length != 0; - return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); } - /// - /// Gets a genre, by name. - /// - /// The genre name. - /// The user id. - /// Genres returned. - /// An containing the genre. - [HttpGet("{genreName}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) + QueryResult<(BaseItem, ItemCounts)> result; + if (parentItem is ICollectionFolder parentCollectionFolder + && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal) + || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal))) { - var dtoOptions = new DtoOptions() - .AddClientFields(User); - - Genre? item; - if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase)) - { - item = GetItemFromSlugName(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre); - } - else - { - item = _libraryManager.GetGenre(genreName); - } - - item ??= new Genre(); - - if (userId is null || userId.Value.Equals(default)) - { - return _dtoService.GetBaseItemDto(item, dtoOptions); - } - - var user = _userManager.GetUserById(userId.Value); - - return _dtoService.GetBaseItemDto(item, dtoOptions, user); + result = _libraryManager.GetMusicGenres(query); } - - private T? GetItemFromSlugName(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind) - where T : BaseItem, new() + else { - var result = libraryManager.GetItemList(new InternalItemsQuery - { - Name = name.Replace(BaseItem.SlugChar, '&'), - IncludeItemTypes = new[] { baseItemKind }, - DtoOptions = dtoOptions - }).OfType().FirstOrDefault(); - - result ??= libraryManager.GetItemList(new InternalItemsQuery - { - Name = name.Replace(BaseItem.SlugChar, '/'), - IncludeItemTypes = new[] { baseItemKind }, - DtoOptions = dtoOptions - }).OfType().FirstOrDefault(); - - result ??= libraryManager.GetItemList(new InternalItemsQuery - { - Name = name.Replace(BaseItem.SlugChar, '?'), - IncludeItemTypes = new[] { baseItemKind }, - DtoOptions = dtoOptions - }).OfType().FirstOrDefault(); - - return result; + result = _libraryManager.GetGenres(query); } + + var shouldIncludeItemTypes = includeItemTypes.Length != 0; + return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); + } + + /// + /// Gets a genre, by name. + /// + /// The genre name. + /// The user id. + /// Genres returned. + /// An containing the genre. + [HttpGet("{genreName}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) + { + var dtoOptions = new DtoOptions() + .AddClientFields(User); + + Genre? item; + if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase)) + { + item = GetItemFromSlugName(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre); + } + else + { + item = _libraryManager.GetGenre(genreName); + } + + item ??= new Genre(); + + if (userId is null || userId.Value.Equals(default)) + { + return _dtoService.GetBaseItemDto(item, dtoOptions); + } + + var user = _userManager.GetUserById(userId.Value); + + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + private T? GetItemFromSlugName(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind) + where T : BaseItem, new() + { + var result = libraryManager.GetItemList(new InternalItemsQuery + { + Name = name.Replace(BaseItem.SlugChar, '&'), + IncludeItemTypes = new[] { baseItemKind }, + DtoOptions = dtoOptions + }).OfType().FirstOrDefault(); + + result ??= libraryManager.GetItemList(new InternalItemsQuery + { + Name = name.Replace(BaseItem.SlugChar, '/'), + IncludeItemTypes = new[] { baseItemKind }, + DtoOptions = dtoOptions + }).OfType().FirstOrDefault(); + + result ??= libraryManager.GetItemList(new InternalItemsQuery + { + Name = name.Replace(BaseItem.SlugChar, '?'), + IncludeItemTypes = new[] { baseItemKind }, + DtoOptions = dtoOptions + }).OfType().FirstOrDefault(); + + return result; } } diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 50fee233a8..085115e1c8 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -15,178 +15,177 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The hls segment controller. +/// +[Route("")] +public class HlsSegmentController : BaseJellyfinApiController { + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + /// - /// The hls segment controller. + /// Initializes a new instance of the class. /// - [Route("")] - public class HlsSegmentController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Initialized instance of the . + public HlsSegmentController( + IFileSystem fileSystem, + IServerConfigurationManager serverConfigurationManager, + TranscodingJobHelper transcodingJobHelper) { - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly TranscodingJobHelper _transcodingJobHelper; + _fileSystem = fileSystem; + _serverConfigurationManager = serverConfigurationManager; + _transcodingJobHelper = transcodingJobHelper; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Initialized instance of the . - public HlsSegmentController( - IFileSystem fileSystem, - IServerConfigurationManager serverConfigurationManager, - TranscodingJobHelper transcodingJobHelper) + /// + /// Gets the specified audio segment for an audio item. + /// + /// The item id. + /// The segment id. + /// Hls audio segment returned. + /// A containing the audio stream. + // Can't require authentication just yet due to seeing some requests come from Chrome without full query string + // [Authenticated] + [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")] + [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesAudioFile] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] + public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId) + { + // TODO: Deprecate with new iOS app + var file = segmentId + Path.GetExtension(Request.Path); + var transcodePath = _serverConfigurationManager.GetTranscodePath(); + file = Path.GetFullPath(Path.Combine(transcodePath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)) { - _fileSystem = fileSystem; - _serverConfigurationManager = serverConfigurationManager; - _transcodingJobHelper = transcodingJobHelper; + return BadRequest("Invalid segment."); } - /// - /// Gets the specified audio segment for an audio item. - /// - /// The item id. - /// The segment id. - /// Hls audio segment returned. - /// A containing the audio stream. - // Can't require authentication just yet due to seeing some requests come from Chrome without full query string - // [Authenticated] - [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")] - [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesAudioFile] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] - public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId) + return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)); + } + + /// + /// Gets a hls video playlist. + /// + /// The video id. + /// The playlist id. + /// Hls video playlist returned. + /// A containing the playlist. + [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] + public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId) + { + var file = playlistId + Path.GetExtension(Request.Path); + var transcodePath = _serverConfigurationManager.GetTranscodePath(); + file = Path.GetFullPath(Path.Combine(transcodePath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8") { - // TODO: Deprecate with new iOS app - var file = segmentId + Path.GetExtension(Request.Path); - var transcodePath = _serverConfigurationManager.GetTranscodePath(); - file = Path.GetFullPath(Path.Combine(transcodePath, file)); - var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)) + return BadRequest("Invalid segment."); + } + + return GetFileResult(file, file); + } + + /// + /// Stops an active encoding. + /// + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// The play session id. + /// Encoding stopped successfully. + /// A indicating success. + [HttpDelete("Videos/ActiveEncodings")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult StopEncodingProcess( + [FromQuery, Required] string deviceId, + [FromQuery, Required] string playSessionId) + { + _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true); + return NoContent(); + } + + /// + /// Gets a hls video segment. + /// + /// The item id. + /// The playlist id. + /// The segment id. + /// The segment container. + /// Hls video segment returned. + /// Hls segment not found. + /// A containing the video segment. + // Can't require authentication just yet due to seeing some requests come from Chrome without full query string + // [Authenticated] + [HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesVideoFile] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] + public ActionResult GetHlsVideoSegmentLegacy( + [FromRoute, Required] string itemId, + [FromRoute, Required] string playlistId, + [FromRoute, Required] string segmentId, + [FromRoute, Required] string segmentContainer) + { + var file = segmentId + Path.GetExtension(Request.Path); + var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); + + file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture)) + { + return BadRequest("Invalid segment."); + } + + var normalizedPlaylistId = playlistId; + + var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath); + // Add . to start of segment container for future use. + segmentContainer = segmentContainer.Insert(0, "."); + string? playlistPath = null; + foreach (var path in filePaths) + { + var pathExtension = Path.GetExtension(path); + if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase) + || string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase)) + && path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) { - return BadRequest("Invalid segment."); + playlistPath = path; + break; + } + } + + return playlistPath is null + ? NotFound("Hls segment not found.") + : GetFileResult(file, playlistPath); + } + + private ActionResult GetFileResult(string path, string playlistPath) + { + var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); + + Response.OnCompleted(() => + { + if (transcodingJob is not null) + { + _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob); } - return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)); - } + return Task.CompletedTask; + }); - /// - /// Gets a hls video playlist. - /// - /// The video id. - /// The playlist id. - /// Hls video playlist returned. - /// A containing the playlist. - [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesPlaylistFile] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] - public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId) - { - var file = playlistId + Path.GetExtension(Request.Path); - var transcodePath = _serverConfigurationManager.GetTranscodePath(); - file = Path.GetFullPath(Path.Combine(transcodePath, file)); - var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8") - { - return BadRequest("Invalid segment."); - } - - return GetFileResult(file, file); - } - - /// - /// Stops an active encoding. - /// - /// The device id of the client requesting. Used to stop encoding processes when needed. - /// The play session id. - /// Encoding stopped successfully. - /// A indicating success. - [HttpDelete("Videos/ActiveEncodings")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult StopEncodingProcess( - [FromQuery, Required] string deviceId, - [FromQuery, Required] string playSessionId) - { - _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true); - return NoContent(); - } - - /// - /// Gets a hls video segment. - /// - /// The item id. - /// The playlist id. - /// The segment id. - /// The segment container. - /// Hls video segment returned. - /// Hls segment not found. - /// A containing the video segment. - // Can't require authentication just yet due to seeing some requests come from Chrome without full query string - // [Authenticated] - [HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesVideoFile] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] - public ActionResult GetHlsVideoSegmentLegacy( - [FromRoute, Required] string itemId, - [FromRoute, Required] string playlistId, - [FromRoute, Required] string segmentId, - [FromRoute, Required] string segmentContainer) - { - var file = segmentId + Path.GetExtension(Request.Path); - var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); - - file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); - var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture)) - { - return BadRequest("Invalid segment."); - } - - var normalizedPlaylistId = playlistId; - - var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath); - // Add . to start of segment container for future use. - segmentContainer = segmentContainer.Insert(0, "."); - string? playlistPath = null; - foreach (var path in filePaths) - { - var pathExtension = Path.GetExtension(path); - if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase) - || string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase)) - && path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) - { - playlistPath = path; - break; - } - } - - return playlistPath is null - ? NotFound("Hls segment not found.") - : GetFileResult(file, playlistPath); - } - - private ActionResult GetFileResult(string path, string playlistPath) - { - var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); - - Response.OnCompleted(() => - { - if (transcodingJob is not null) - { - _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob); - } - - return Task.CompletedTask; - }); - - return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)); - } + return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)); } } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 996dc08196..cc824c65ab 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -30,2071 +30,2070 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Image controller. +/// +[Route("")] +public class ImageController : BaseJellyfinApiController { + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IProviderManager _providerManager; + private readonly IImageProcessor _imageProcessor; + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IApplicationPaths _appPaths; + /// - /// Image controller. + /// Initializes a new instance of the class. /// - [Route("")] - public class ImageController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ImageController( + IUserManager userManager, + ILibraryManager libraryManager, + IProviderManager providerManager, + IImageProcessor imageProcessor, + IFileSystem fileSystem, + ILogger logger, + IServerConfigurationManager serverConfigurationManager, + IApplicationPaths appPaths) { - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly IProviderManager _providerManager; - private readonly IImageProcessor _imageProcessor; - private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IApplicationPaths _appPaths; + _userManager = userManager; + _libraryManager = libraryManager; + _providerManager = providerManager; + _imageProcessor = imageProcessor; + _fileSystem = fileSystem; + _logger = logger; + _serverConfigurationManager = serverConfigurationManager; + _appPaths = appPaths; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public ImageController( - IUserManager userManager, - ILibraryManager libraryManager, - IProviderManager providerManager, - IImageProcessor imageProcessor, - IFileSystem fileSystem, - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IApplicationPaths appPaths) + /// + /// Sets the user image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image updated. + /// User does not have permission to delete the image. + /// A . + [HttpPost("Users/{userId}/Images/{imageType}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [AcceptsImageFile] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + public async Task PostUserImage( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromQuery] int? index = null) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) { - _userManager = userManager; - _libraryManager = libraryManager; - _providerManager = providerManager; - _imageProcessor = imageProcessor; - _fileSystem = fileSystem; - _logger = logger; - _serverConfigurationManager = serverConfigurationManager; - _appPaths = appPaths; + return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } - /// - /// Sets the user image. - /// - /// User Id. - /// (Unused) Image type. - /// (Unused) Image index. - /// Image updated. - /// User does not have permission to delete the image. - /// A . - [HttpPost("Users/{userId}/Images/{imageType}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [AcceptsImageFile] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] - public async Task PostUserImage( - [FromRoute, Required] Guid userId, - [FromRoute, Required] ImageType imageType, - [FromQuery] int? index = null) + var user = _userManager.GetUserById(userId); + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + await using (memoryStream.ConfigureAwait(false)) { - if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); + var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); + if (user.ProfileImage is not null) { - return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); + await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } - var user = _userManager.GetUserById(userId); - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) - { - // Handle image/png; charset=utf-8 - var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); - var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); - if (user.ProfileImage is not null) - { - await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); - } + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); + await _providerManager + .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) + .ConfigureAwait(false); + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); - await _providerManager - .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) - .ConfigureAwait(false); - await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + return NoContent(); + } + } - return NoContent(); - } + /// + /// Sets the user image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image updated. + /// User does not have permission to delete the image. + /// A . + [HttpPost("Users/{userId}/Images/{imageType}/{index}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [AcceptsImageFile] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + public async Task PostUserImageByIndex( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int index) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) + { + return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); } - /// - /// Sets the user image. - /// - /// User Id. - /// (Unused) Image type. - /// (Unused) Image index. - /// Image updated. - /// User does not have permission to delete the image. - /// A . - [HttpPost("Users/{userId}/Images/{imageType}/{index}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [AcceptsImageFile] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] - public async Task PostUserImageByIndex( - [FromRoute, Required] Guid userId, - [FromRoute, Required] ImageType imageType, - [FromRoute] int index) + var user = _userManager.GetUserById(userId); + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + await using (memoryStream.ConfigureAwait(false)) { - if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); + var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); + if (user.ProfileImage is not null) { - return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); + await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } - var user = _userManager.GetUserById(userId); - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) - { - // Handle image/png; charset=utf-8 - var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); - var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); - if (user.ProfileImage is not null) - { - await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); - } + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); + await _providerManager + .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) + .ConfigureAwait(false); + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); - await _providerManager - .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) - .ConfigureAwait(false); - await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + return NoContent(); + } + } - return NoContent(); - } + /// + /// Delete the user's image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image deleted. + /// User does not have permission to delete the image. + /// A . + [HttpDelete("Users/{userId}/Images/{imageType}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task DeleteUserImage( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromQuery] int? index = null) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) + { + return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); } - /// - /// Delete the user's image. - /// - /// User Id. - /// (Unused) Image type. - /// (Unused) Image index. - /// Image deleted. - /// User does not have permission to delete the image. - /// A . - [HttpDelete("Users/{userId}/Images/{imageType}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task DeleteUserImage( - [FromRoute, Required] Guid userId, - [FromRoute, Required] ImageType imageType, - [FromQuery] int? index = null) + var user = _userManager.GetUserById(userId); + if (user?.ProfileImage is null) { - if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) - { - return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); - } - - var user = _userManager.GetUserById(userId); - if (user?.ProfileImage is null) - { - return NoContent(); - } - - try - { - System.IO.File.Delete(user.ProfileImage.Path); - } - catch (IOException e) - { - _logger.LogError(e, "Error deleting user profile image:"); - } - - await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); return NoContent(); } - /// - /// Delete the user's image. - /// - /// User Id. - /// (Unused) Image type. - /// (Unused) Image index. - /// Image deleted. - /// User does not have permission to delete the image. - /// A . - [HttpDelete("Users/{userId}/Images/{imageType}/{index}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task DeleteUserImageByIndex( - [FromRoute, Required] Guid userId, - [FromRoute, Required] ImageType imageType, - [FromRoute] int index) + try { - if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) - { - return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); - } + System.IO.File.Delete(user.ProfileImage.Path); + } + catch (IOException e) + { + _logger.LogError(e, "Error deleting user profile image:"); + } - var user = _userManager.GetUserById(userId); - if (user?.ProfileImage is null) - { - return NoContent(); - } + await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); + return NoContent(); + } - try - { - System.IO.File.Delete(user.ProfileImage.Path); - } - catch (IOException e) - { - _logger.LogError(e, "Error deleting user profile image:"); - } + /// + /// Delete the user's image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image deleted. + /// User does not have permission to delete the image. + /// A . + [HttpDelete("Users/{userId}/Images/{imageType}/{index}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task DeleteUserImageByIndex( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int index) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true)) + { + return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image."); + } - await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); + var user = _userManager.GetUserById(userId); + if (user?.ProfileImage is null) + { return NoContent(); } - /// - /// Delete an item's image. - /// - /// Item id. - /// Image type. - /// The image index. - /// Image deleted. - /// Item not found. - /// A on success, or a if item not found. - [HttpDelete("Items/{itemId}/Images/{imageType}")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteItemImage( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] ImageType imageType, - [FromQuery] int? imageIndex) + try { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } + System.IO.File.Delete(user.ProfileImage.Path); + } + catch (IOException e) + { + _logger.LogError(e, "Error deleting user profile image:"); + } + + await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Delete an item's image. + /// + /// Item id. + /// Image type. + /// The image index. + /// Image deleted. + /// Item not found. + /// A on success, or a if item not found. + [HttpDelete("Items/{itemId}/Images/{imageType}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteItemImage( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType, + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Delete an item's image. + /// + /// Item id. + /// Image type. + /// The image index. + /// Image deleted. + /// Item not found. + /// A on success, or a if item not found. + [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteItemImageByIndex( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int imageIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Set item image. + /// + /// Item id. + /// Image type. + /// Image saved. + /// Item not found. + /// A on success, or a if item not found. + [HttpPost("Items/{itemId}/Images/{imageType}")] + [Authorize(Policy = Policies.RequiresElevation)] + [AcceptsImageFile] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + public async Task SetItemImage( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + await using (memoryStream.ConfigureAwait(false)) + { + // Handle image/png; charset=utf-8 + 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); - await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false); return NoContent(); } + } - /// - /// Delete an item's image. - /// - /// Item id. - /// Image type. - /// The image index. - /// Image deleted. - /// Item not found. - /// A on success, or a if item not found. - [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteItemImageByIndex( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] ImageType imageType, - [FromRoute] int imageIndex) + /// + /// Set item image. + /// + /// Item id. + /// Image type. + /// (Unused) Image index. + /// Image saved. + /// Item not found. + /// A on success, or a if item not found. + [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")] + [Authorize(Policy = Policies.RequiresElevation)] + [AcceptsImageFile] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + public async Task SetItemImageByIndex( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int imageIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } + return NotFound(); + } + + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + await using (memoryStream.ConfigureAwait(false)) + { + // Handle image/png; charset=utf-8 + 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); - await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false); return NoContent(); } + } - /// - /// Set item image. - /// - /// Item id. - /// Image type. - /// Image saved. - /// Item not found. - /// A on success, or a if item not found. - [HttpPost("Items/{itemId}/Images/{imageType}")] - [Authorize(Policy = Policies.RequiresElevation)] - [AcceptsImageFile] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] - public async Task SetItemImage( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] ImageType imageType) + /// + /// Updates the index for an item image. + /// + /// Item id. + /// Image type. + /// Old image index. + /// New image index. + /// Image index updated. + /// Item not found. + /// A on success, or a if item not found. + [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateItemImageIndex( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery, Required] int newIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } - - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) - { - // Handle image/png; charset=utf-8 - 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); - - return NoContent(); - } + return NotFound(); } - /// - /// Set item image. - /// - /// Item id. - /// Image type. - /// (Unused) Image index. - /// Image saved. - /// Item not found. - /// A on success, or a if item not found. - [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")] - [Authorize(Policy = Policies.RequiresElevation)] - [AcceptsImageFile] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] - public async Task SetItemImageByIndex( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] ImageType imageType, - [FromRoute] int imageIndex) + await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Get item image infos. + /// + /// Item id. + /// Item images returned. + /// Item not found. + /// The list of image infos on success, or if item not found. + [HttpGet("Items/{itemId}/Images")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetItemImageInfos([FromRoute, Required] Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } - - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) - { - // Handle image/png; charset=utf-8 - 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); - - return NoContent(); - } + return NotFound(); } - /// - /// Updates the index for an item image. - /// - /// Item id. - /// Image type. - /// Old image index. - /// New image index. - /// Image index updated. - /// Item not found. - /// A on success, or a if item not found. - [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task UpdateItemImageIndex( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] ImageType imageType, - [FromRoute, Required] int imageIndex, - [FromQuery, Required] int newIndex) + var list = new List(); + var itemImages = item.ImageInfos; + + if (itemImages.Length == 0) { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } - - await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Get item image infos. - /// - /// Item id. - /// Item images returned. - /// Item not found. - /// The list of image infos on success, or if item not found. - [HttpGet("Items/{itemId}/Images")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetItemImageInfos([FromRoute, Required] Guid itemId) - { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } - - var list = new List(); - var itemImages = item.ImageInfos; - - if (itemImages.Length == 0) - { - // short-circuit - return list; - } - - await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes are correct - - foreach (var image in itemImages) - { - if (!item.AllowsMultipleImages(image.Type)) - { - var info = GetImageInfo(item, image, null); - - if (info is not null) - { - list.Add(info); - } - } - } - - foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages)) - { - var index = 0; - - // Prevent implicitly captured closure - var currentImageType = imageType; - - foreach (var image in itemImages.Where(i => i.Type == currentImageType)) - { - var info = GetImageInfo(item, image, index); - - if (info is not null) - { - list.Add(info); - } - - index++; - } - } - + // short-circuit return list; } - /// - /// Gets the item's image. - /// - /// Item id. - /// Image type. - /// The maximum image width to return. - /// The maximum image height to return. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. The of the returned image. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image index. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Items/{itemId}/Images/{imageType}")] - [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetItemImage( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] ImageType imageType, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery] string? tag, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] ImageFormat? format, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromQuery] int? imageIndex) + await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes are correct + + foreach (var image in itemImages) { - var item = _libraryManager.GetItemById(itemId); - if (item is null) + if (!item.AllowsMultipleImages(image.Type)) + { + var info = GetImageInfo(item, image, null); + + if (info is not null) + { + list.Add(info); + } + } + } + + foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages)) + { + var index = 0; + + // Prevent implicitly captured closure + var currentImageType = imageType; + + foreach (var image in itemImages.Where(i => i.Type == currentImageType)) + { + var info = GetImageInfo(item, image, index); + + if (info is not null) + { + list.Add(info); + } + + index++; + } + } + + return list; + } + + /// + /// Gets the item's image. + /// + /// Item id. + /// Image type. + /// The maximum image width to return. + /// The maximum image height to return. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. The of the returned image. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Items/{itemId}/Images/{imageType}")] + [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetItemImage( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery] string? tag, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] ImageFormat? format, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + itemId, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Gets the item's image. + /// + /// Item id. + /// Image type. + /// Image index. + /// The maximum image width to return. + /// The maximum image height to return. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. The of the returned image. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")] + [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetItemImageByIndex( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int imageIndex, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery] string? tag, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] ImageFormat? format, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + itemId, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Gets the item's image. + /// + /// Item id. + /// Image type. + /// The maximum image width to return. + /// The maximum image height to return. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Determines the output format of the image - original,gif,jpg,png. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] + [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetItemImage2( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int maxWidth, + [FromRoute, Required] int maxHeight, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromRoute, Required] string tag, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromRoute, Required] ImageFormat format, + [FromRoute, Required] double percentPlayed, + [FromRoute, Required] int unplayedCount, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromRoute, Required] int imageIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + itemId, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Get artist image by name. + /// + /// Artist name. + /// Image type. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetArtistImage( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromRoute, Required] int imageIndex) + { + var item = _libraryManager.GetArtist(name); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Get genre image by name. + /// + /// Genre name. + /// Image type. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Genres/{name}/Images/{imageType}")] + [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetGenreImage( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetGenre(name); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Get genre image by name. + /// + /// Genre name. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetGenreImageByIndex( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) + { + var item = _libraryManager.GetGenre(name); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Get music genre image by name. + /// + /// Music genre name. + /// Image type. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("MusicGenres/{name}/Images/{imageType}")] + [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetMusicGenreImage( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetMusicGenre(name); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Get music genre image by name. + /// + /// Music genre name. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetMusicGenreImageByIndex( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) + { + var item = _libraryManager.GetMusicGenre(name); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Get person image by name. + /// + /// Person name. + /// Image type. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Persons/{name}/Images/{imageType}")] + [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetPersonImage( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetPerson(name); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Get person image by name. + /// + /// Person name. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetPersonImageByIndex( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) + { + var item = _libraryManager.GetPerson(name); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Get studio image by name. + /// + /// Studio name. + /// Image type. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Studios/{name}/Images/{imageType}")] + [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetStudioImage( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetStudio(name); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Get studio image by name. + /// + /// Studio name. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetStudioImageByIndex( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) + { + var item = _libraryManager.GetStudio(name); + if (item is null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + item) + .ConfigureAwait(false); + } + + /// + /// Get user profile image. + /// + /// User id. + /// Image type. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image index. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Users/{userId}/Images/{imageType}")] + [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetUserImage( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery] int? imageIndex) + { + var user = _userManager.GetUserById(userId); + if (user?.ProfileImage is null) + { + return NotFound(); + } + + var info = new ItemImageInfo + { + Path = user.ProfileImage.Path, + Type = ImageType.Profile, + DateModified = user.ProfileImage.LastModified + }; + + if (width.HasValue) + { + info.Width = width.Value; + } + + if (height.HasValue) + { + info.Height = height.Value; + } + + return await GetImageInternal( + user.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + null, + info) + .ConfigureAwait(false); + } + + /// + /// Get user profile image. + /// + /// User id. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Width of box to fill. + /// Height of box to fill. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")] + [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetUserImageByIndex( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery, ParameterObsolete] bool? cropWhitespace, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) + { + var user = _userManager.GetUserById(userId); + if (user?.ProfileImage is null) + { + return NotFound(); + } + + var info = new ItemImageInfo + { + Path = user.ProfileImage.Path, + Type = ImageType.Profile, + DateModified = user.ProfileImage.LastModified + }; + + if (width.HasValue) + { + info.Width = width.Value; + } + + if (height.HasValue) + { + info.Height = height.Value; + } + + return await GetImageInternal( + user.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + fillWidth, + fillHeight, + blur, + backgroundColor, + foregroundLayer, + null, + info) + .ConfigureAwait(false); + } + + /// + /// Generates or gets the splashscreen. + /// + /// Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// The fixed image width to return. + /// The fixed image height to return. + /// Width of box to fill. + /// Height of box to fill. + /// Blur image. + /// Apply a background color for transparent images. + /// Apply a foreground layer on top of the image. + /// Quality setting, from 0-100. + /// Splashscreen returned successfully. + /// The splashscreen. + [HttpGet("Branding/Splashscreen")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesImageFile] + public async Task GetSplashscreen( + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery, Range(0, 100)] int quality = 90) + { + var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); + if (!brandingOptions.SplashscreenEnabled) + { + return NotFound(); + } + + string splashscreenPath; + + if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation) + && System.IO.File.Exists(brandingOptions.SplashscreenLocation)) + { + splashscreenPath = brandingOptions.SplashscreenLocation; + } + else + { + splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); + if (!System.IO.File.Exists(splashscreenPath)) { return NotFound(); } - - return await GetImageInternal( - itemId, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); } - /// - /// Gets the item's image. - /// - /// Item id. - /// Image type. - /// Image index. - /// The maximum image width to return. - /// The maximum image height to return. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. The of the returned image. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")] - [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetItemImageByIndex( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] ImageType imageType, - [FromRoute] int imageIndex, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery] string? tag, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] ImageFormat? format, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer) - { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } + var outputFormats = GetOutputFormats(format); - return await GetImageInternal( - itemId, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); + TimeSpan? cacheDuration = null; + if (!string.IsNullOrEmpty(tag)) + { + cacheDuration = TimeSpan.FromDays(365); } - /// - /// Gets the item's image. - /// - /// Item id. - /// Image type. - /// The maximum image width to return. - /// The maximum image height to return. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Determines the output format of the image - original,gif,jpg,png. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image index. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] - [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetItemImage2( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] ImageType imageType, - [FromRoute, Required] int maxWidth, - [FromRoute, Required] int maxHeight, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromRoute, Required] string tag, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromRoute, Required] ImageFormat format, - [FromRoute, Required] double percentPlayed, - [FromRoute, Required] int unplayedCount, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromRoute, Required] int imageIndex) + var options = new ImageProcessingOptions { - var item = _libraryManager.GetItemById(itemId); - if (item is null) + Image = new ItemImageInfo { - return NotFound(); - } + Path = splashscreenPath + }, + Height = height, + MaxHeight = maxHeight, + MaxWidth = maxWidth, + FillHeight = fillHeight, + FillWidth = fillWidth, + Quality = quality, + Width = width, + Blur = blur, + BackgroundColor = backgroundColor, + ForegroundLayer = foregroundLayer, + SupportedOutputFormats = outputFormats + }; - return await GetImageInternal( - itemId, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); - } + return await GetImageResult( + options, + cacheDuration, + ImmutableDictionary.Empty) + .ConfigureAwait(false); + } - /// - /// Get artist image by name. - /// - /// Artist name. - /// Image type. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image index. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")] - [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetArtistImage( - [FromRoute, Required] string name, - [FromRoute, Required] ImageType imageType, - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromRoute, Required] int imageIndex) + /// + /// Uploads a custom splashscreen. + /// The body is expected to the image contents base64 encoded. + /// + /// A indicating success. + /// Successfully uploaded new splashscreen. + /// Error reading MimeType from uploaded image. + /// User does not have permission to upload splashscreen.. + /// Error reading the image format. + [HttpPost("Branding/Splashscreen")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [AcceptsImageFile] + public async Task UploadCustomSplashscreen() + { + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + await using (memoryStream.ConfigureAwait(false)) { - var item = _libraryManager.GetArtist(name); - if (item is null) + var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType; + + if (!mimeType.HasValue) { - return NotFound(); + return BadRequest("Error reading mimetype from uploaded image"); } - return await GetImageInternal( - item.Id, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); - } - - /// - /// Get genre image by name. - /// - /// Genre name. - /// Image type. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image index. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Genres/{name}/Images/{imageType}")] - [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetGenreImage( - [FromRoute, Required] string name, - [FromRoute, Required] ImageType imageType, - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromQuery] int? imageIndex) - { - var item = _libraryManager.GetGenre(name); - if (item is null) + var extension = MimeTypes.ToExtension(mimeType.Value); + if (string.IsNullOrEmpty(extension)) { - return NotFound(); + return BadRequest("Error converting mimetype to an image extension"); } - return await GetImageInternal( - item.Id, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); - } - - /// - /// Get genre image by name. - /// - /// Genre name. - /// Image type. - /// Image index. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")] - [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetGenreImageByIndex( - [FromRoute, Required] string name, - [FromRoute, Required] ImageType imageType, - [FromRoute, Required] int imageIndex, - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer) - { - var item = _libraryManager.GetGenre(name); - if (item is null) - { - return NotFound(); - } - - return await GetImageInternal( - item.Id, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); - } - - /// - /// Get music genre image by name. - /// - /// Music genre name. - /// Image type. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image index. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("MusicGenres/{name}/Images/{imageType}")] - [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetMusicGenreImage( - [FromRoute, Required] string name, - [FromRoute, Required] ImageType imageType, - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromQuery] int? imageIndex) - { - var item = _libraryManager.GetMusicGenre(name); - if (item is null) - { - return NotFound(); - } - - return await GetImageInternal( - item.Id, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); - } - - /// - /// Get music genre image by name. - /// - /// Music genre name. - /// Image type. - /// Image index. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")] - [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetMusicGenreImageByIndex( - [FromRoute, Required] string name, - [FromRoute, Required] ImageType imageType, - [FromRoute, Required] int imageIndex, - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer) - { - var item = _libraryManager.GetMusicGenre(name); - if (item is null) - { - return NotFound(); - } - - return await GetImageInternal( - item.Id, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); - } - - /// - /// Get person image by name. - /// - /// Person name. - /// Image type. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image index. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Persons/{name}/Images/{imageType}")] - [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetPersonImage( - [FromRoute, Required] string name, - [FromRoute, Required] ImageType imageType, - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromQuery] int? imageIndex) - { - var item = _libraryManager.GetPerson(name); - if (item is null) - { - return NotFound(); - } - - return await GetImageInternal( - item.Id, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); - } - - /// - /// Get person image by name. - /// - /// Person name. - /// Image type. - /// Image index. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")] - [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetPersonImageByIndex( - [FromRoute, Required] string name, - [FromRoute, Required] ImageType imageType, - [FromRoute, Required] int imageIndex, - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer) - { - var item = _libraryManager.GetPerson(name); - if (item is null) - { - return NotFound(); - } - - return await GetImageInternal( - item.Id, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); - } - - /// - /// Get studio image by name. - /// - /// Studio name. - /// Image type. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image index. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Studios/{name}/Images/{imageType}")] - [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetStudioImage( - [FromRoute, Required] string name, - [FromRoute, Required] ImageType imageType, - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromQuery] int? imageIndex) - { - var item = _libraryManager.GetStudio(name); - if (item is null) - { - return NotFound(); - } - - return await GetImageInternal( - item.Id, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); - } - - /// - /// Get studio image by name. - /// - /// Studio name. - /// Image type. - /// Image index. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")] - [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetStudioImageByIndex( - [FromRoute, Required] string name, - [FromRoute, Required] ImageType imageType, - [FromRoute, Required] int imageIndex, - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer) - { - var item = _libraryManager.GetStudio(name); - if (item is null) - { - return NotFound(); - } - - return await GetImageInternal( - item.Id, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - item) - .ConfigureAwait(false); - } - - /// - /// Get user profile image. - /// - /// User id. - /// Image type. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image index. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Users/{userId}/Images/{imageType}")] - [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetUserImage( - [FromRoute, Required] Guid userId, - [FromRoute, Required] ImageType imageType, - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromQuery] int? imageIndex) - { - var user = _userManager.GetUserById(userId); - if (user?.ProfileImage is null) - { - return NotFound(); - } - - var info = new ItemImageInfo - { - Path = user.ProfileImage.Path, - Type = ImageType.Profile, - DateModified = user.ProfileImage.LastModified - }; - - if (width.HasValue) - { - info.Width = width.Value; - } - - if (height.HasValue) - { - info.Height = height.Value; - } - - return await GetImageInternal( - user.Id, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - null, - info) - .ConfigureAwait(false); - } - - /// - /// Get user profile image. - /// - /// User id. - /// Image type. - /// Image index. - /// Optional. Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// Optional. Percent to render for the percent played overlay. - /// Optional. Unplayed count overlay to render. - /// The fixed image width to return. - /// The fixed image height to return. - /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. - /// Width of box to fill. - /// Height of box to fill. - /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. - /// Optional. Blur image. - /// Optional. Apply a background color for transparent images. - /// Optional. Apply a foreground layer on top of the image. - /// Image stream returned. - /// Item not found. - /// - /// A containing the file stream on success, - /// or a if item not found. - /// - [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")] - [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - public async Task GetUserImageByIndex( - [FromRoute, Required] Guid userId, - [FromRoute, Required] ImageType imageType, - [FromRoute, Required] int imageIndex, - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] double? percentPlayed, - [FromQuery] int? unplayedCount, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? quality, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery, ParameterObsolete] bool? cropWhitespace, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer) - { - var user = _userManager.GetUserById(userId); - if (user?.ProfileImage is null) - { - return NotFound(); - } - - var info = new ItemImageInfo - { - Path = user.ProfileImage.Path, - Type = ImageType.Profile, - DateModified = user.ProfileImage.LastModified - }; - - if (width.HasValue) - { - info.Width = width.Value; - } - - if (height.HasValue) - { - info.Height = height.Value; - } - - return await GetImageInternal( - user.Id, - imageType, - imageIndex, - tag, - format, - maxWidth, - maxHeight, - percentPlayed, - unplayedCount, - width, - height, - quality, - fillWidth, - fillHeight, - blur, - backgroundColor, - foregroundLayer, - null, - info) - .ConfigureAwait(false); - } - - /// - /// Generates or gets the splashscreen. - /// - /// Supply the cache tag from the item object to receive strong caching headers. - /// Determines the output format of the image - original,gif,jpg,png. - /// The maximum image width to return. - /// The maximum image height to return. - /// The fixed image width to return. - /// The fixed image height to return. - /// Width of box to fill. - /// Height of box to fill. - /// Blur image. - /// Apply a background color for transparent images. - /// Apply a foreground layer on top of the image. - /// Quality setting, from 0-100. - /// Splashscreen returned successfully. - /// The splashscreen. - [HttpGet("Branding/Splashscreen")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesImageFile] - public async Task GetSplashscreen( - [FromQuery] string? tag, - [FromQuery] ImageFormat? format, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? fillWidth, - [FromQuery] int? fillHeight, - [FromQuery] int? blur, - [FromQuery] string? backgroundColor, - [FromQuery] string? foregroundLayer, - [FromQuery, Range(0, 100)] int quality = 90) - { + var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); - if (!brandingOptions.SplashscreenEnabled) + brandingOptions.SplashscreenLocation = filePath; + _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); + + var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await using (fs.ConfigureAwait(false)) { - return NotFound(); - } - - string splashscreenPath; - - if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation) - && System.IO.File.Exists(brandingOptions.SplashscreenLocation)) - { - splashscreenPath = brandingOptions.SplashscreenLocation; - } - else - { - splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); - if (!System.IO.File.Exists(splashscreenPath)) - { - return NotFound(); - } - } - - var outputFormats = GetOutputFormats(format); - - TimeSpan? cacheDuration = null; - if (!string.IsNullOrEmpty(tag)) - { - cacheDuration = TimeSpan.FromDays(365); - } - - var options = new ImageProcessingOptions - { - Image = new ItemImageInfo - { - Path = splashscreenPath - }, - Height = height, - MaxHeight = maxHeight, - MaxWidth = maxWidth, - FillHeight = fillHeight, - FillWidth = fillWidth, - Quality = quality, - Width = width, - Blur = blur, - BackgroundColor = backgroundColor, - ForegroundLayer = foregroundLayer, - SupportedOutputFormats = outputFormats - }; - - return await GetImageResult( - options, - cacheDuration, - ImmutableDictionary.Empty) - .ConfigureAwait(false); - } - - /// - /// Uploads a custom splashscreen. - /// The body is expected to the image contents base64 encoded. - /// - /// A indicating success. - /// Successfully uploaded new splashscreen. - /// Error reading MimeType from uploaded image. - /// User does not have permission to upload splashscreen.. - /// Error reading the image format. - [HttpPost("Branding/Splashscreen")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [AcceptsImageFile] - public async Task UploadCustomSplashscreen() - { - var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); - await using (memoryStream.ConfigureAwait(false)) - { - var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType; - - if (!mimeType.HasValue) - { - return BadRequest("Error reading mimetype from uploaded image"); - } - - var extension = MimeTypes.ToExtension(mimeType.Value); - if (string.IsNullOrEmpty(extension)) - { - return BadRequest("Error converting mimetype to an image extension"); - } - - var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); - var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); - brandingOptions.SplashscreenLocation = filePath; - _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); - - var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await using (fs.ConfigureAwait(false)) - { - await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); - } - - return NoContent(); - } - } - - /// - /// Delete a custom splashscreen. - /// - /// A indicating success. - /// Successfully deleted the custom splashscreen. - /// User does not have permission to delete splashscreen.. - [HttpDelete("Branding/Splashscreen")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult DeleteCustomSplashscreen() - { - var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); - if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation) - && System.IO.File.Exists(brandingOptions.SplashscreenLocation)) - { - System.IO.File.Delete(brandingOptions.SplashscreenLocation); - brandingOptions.SplashscreenLocation = null; - _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); + await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); } return NoContent(); } + } - private static async Task GetMemoryStream(Stream inputStream) + /// + /// Delete a custom splashscreen. + /// + /// A indicating success. + /// Successfully deleted the custom splashscreen. + /// User does not have permission to delete splashscreen.. + [HttpDelete("Branding/Splashscreen")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult DeleteCustomSplashscreen() + { + var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); + if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation) + && System.IO.File.Exists(brandingOptions.SplashscreenLocation)) { - using var reader = new StreamReader(inputStream); - var text = await reader.ReadToEndAsync().ConfigureAwait(false); - - var bytes = Convert.FromBase64String(text); - return new MemoryStream(bytes, 0, bytes.Length, false, true); + System.IO.File.Delete(brandingOptions.SplashscreenLocation); + brandingOptions.SplashscreenLocation = null; + _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); } - private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) + return NoContent(); + } + + private static async Task GetMemoryStream(Stream inputStream) + { + using var reader = new StreamReader(inputStream); + var text = await reader.ReadToEndAsync().ConfigureAwait(false); + + var bytes = Convert.FromBase64String(text); + return new MemoryStream(bytes, 0, bytes.Length, false, true); + } + + private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) + { + int? width = null; + int? height = null; + string? blurhash = null; + long length = 0; + + try { - int? width = null; - int? height = null; - string? blurhash = null; - long length = 0; - - try + if (info.IsLocalFile) { - if (info.IsLocalFile) + var fileInfo = _fileSystem.GetFileInfo(info.Path); + length = fileInfo.Length; + + blurhash = info.BlurHash; + width = info.Width; + height = info.Height; + + if (width <= 0 || height <= 0) { - var fileInfo = _fileSystem.GetFileInfo(info.Path); - length = fileInfo.Length; - - blurhash = info.BlurHash; - width = info.Width; - height = info.Height; - - if (width <= 0 || height <= 0) - { - width = null; - height = null; - } + width = null; + height = null; } } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting image information for {Item}", item.Name); - } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting image information for {Item}", item.Name); + } - try + try + { + return new ImageInfo { - return new ImageInfo - { - Path = info.Path, - ImageIndex = imageIndex, - ImageType = info.Type, - ImageTag = _imageProcessor.GetImageCacheTag(item, info), - Size = length, - BlurHash = blurhash, - Width = width, - Height = height - }; + Path = info.Path, + ImageIndex = imageIndex, + ImageType = info.Type, + ImageTag = _imageProcessor.GetImageCacheTag(item, info), + Size = length, + BlurHash = blurhash, + Width = width, + Height = height + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting image information for {Path}", info.Path); + return null; + } + } + + private async Task GetImageInternal( + Guid itemId, + ImageType imageType, + int? imageIndex, + string? tag, + ImageFormat? format, + int? maxWidth, + int? maxHeight, + double? percentPlayed, + int? unplayedCount, + int? width, + int? height, + int? quality, + int? fillWidth, + int? fillHeight, + int? blur, + string? backgroundColor, + string? foregroundLayer, + BaseItem? item, + ItemImageInfo? imageInfo = null) + { + if (percentPlayed.HasValue) + { + if (percentPlayed.Value <= 0) + { + percentPlayed = null; } - catch (Exception ex) + else if (percentPlayed.Value >= 100) { - _logger.LogError(ex, "Error getting image information for {Path}", info.Path); - return null; + percentPlayed = null; } } - private async Task GetImageInternal( - Guid itemId, - ImageType imageType, - int? imageIndex, - string? tag, - ImageFormat? format, - int? maxWidth, - int? maxHeight, - double? percentPlayed, - int? unplayedCount, - int? width, - int? height, - int? quality, - int? fillWidth, - int? fillHeight, - int? blur, - string? backgroundColor, - string? foregroundLayer, - BaseItem? item, - ItemImageInfo? imageInfo = null) + if (percentPlayed.HasValue) { - if (percentPlayed.HasValue) - { - if (percentPlayed.Value <= 0) - { - percentPlayed = null; - } - else if (percentPlayed.Value >= 100) - { - percentPlayed = null; - } - } + unplayedCount = null; + } - if (percentPlayed.HasValue) - { - unplayedCount = null; - } - - if (unplayedCount.HasValue - && unplayedCount.Value <= 0) - { - unplayedCount = null; - } + if (unplayedCount.HasValue + && unplayedCount.Value <= 0) + { + unplayedCount = null; + } + if (imageInfo is null) + { + imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0); if (imageInfo is null) { - imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0); - if (imageInfo is null) - { - return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item?.Name, imageType)); - } + return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item?.Name, imageType)); } + } - var outputFormats = GetOutputFormats(format); + var outputFormats = GetOutputFormats(format); - TimeSpan? cacheDuration = null; + TimeSpan? cacheDuration = null; - if (!string.IsNullOrEmpty(tag)) - { - cacheDuration = TimeSpan.FromDays(365); - } + if (!string.IsNullOrEmpty(tag)) + { + cacheDuration = TimeSpan.FromDays(365); + } - var responseHeaders = new Dictionary + var responseHeaders = new Dictionary { { "transferMode.dlna.org", "Interactive" }, { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" } }; - if (!imageInfo.IsLocalFile && item is not null) - { - imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false); - } - - var options = new ImageProcessingOptions - { - Height = height, - ImageIndex = imageIndex ?? 0, - Image = imageInfo, - Item = item, - ItemId = itemId, - MaxHeight = maxHeight, - MaxWidth = maxWidth, - FillHeight = fillHeight, - FillWidth = fillWidth, - Quality = quality ?? 100, - Width = width, - PercentPlayed = percentPlayed ?? 0, - UnplayedCount = unplayedCount, - Blur = blur, - BackgroundColor = backgroundColor, - ForegroundLayer = foregroundLayer, - SupportedOutputFormats = outputFormats - }; - - return await GetImageResult( - options, - cacheDuration, - responseHeaders).ConfigureAwait(false); + if (!imageInfo.IsLocalFile && item is not null) + { + imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false); } - private ImageFormat[] GetOutputFormats(ImageFormat? format) + var options = new ImageProcessingOptions { - if (format.HasValue) - { - return new[] { format.Value }; - } + Height = height, + ImageIndex = imageIndex ?? 0, + Image = imageInfo, + Item = item, + ItemId = itemId, + MaxHeight = maxHeight, + MaxWidth = maxWidth, + FillHeight = fillHeight, + FillWidth = fillWidth, + Quality = quality ?? 100, + Width = width, + PercentPlayed = percentPlayed ?? 0, + UnplayedCount = unplayedCount, + Blur = blur, + BackgroundColor = backgroundColor, + ForegroundLayer = foregroundLayer, + SupportedOutputFormats = outputFormats + }; - return GetClientSupportedFormats(); + return await GetImageResult( + options, + cacheDuration, + responseHeaders).ConfigureAwait(false); + } + + private ImageFormat[] GetOutputFormats(ImageFormat? format) + { + if (format.HasValue) + { + return new[] { format.Value }; } - private ImageFormat[] GetClientSupportedFormats() + return GetClientSupportedFormats(); + } + + private ImageFormat[] GetClientSupportedFormats() + { + var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); + for (var i = 0; i < supportedFormats.Length; i++) { - var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); - for (var i = 0; i < supportedFormats.Length; i++) + // Remove charsets etc. (anything after semi-colon) + var type = supportedFormats[i]; + int index = type.IndexOf(';', StringComparison.Ordinal); + if (index != -1) { - // Remove charsets etc. (anything after semi-colon) - var type = supportedFormats[i]; - int index = type.IndexOf(';', StringComparison.Ordinal); - if (index != -1) - { - supportedFormats[i] = type.Substring(0, index); - } + supportedFormats[i] = type.Substring(0, index); } - - var acceptParam = Request.Query[HeaderNames.Accept]; - - var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false); - - if (!supportsWebP) - { - var userAgent = Request.Headers[HeaderNames.UserAgent].ToString(); - if (userAgent.Contains("crosswalk", StringComparison.OrdinalIgnoreCase) - && userAgent.Contains("android", StringComparison.OrdinalIgnoreCase)) - { - supportsWebP = true; - } - } - - var formats = new List(4); - - if (supportsWebP) - { - formats.Add(ImageFormat.Webp); - } - - formats.Add(ImageFormat.Jpg); - formats.Add(ImageFormat.Png); - - if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true)) - { - formats.Add(ImageFormat.Gif); - } - - return formats.ToArray(); } - private bool SupportsFormat(IReadOnlyCollection requestAcceptTypes, string? acceptParam, ImageFormat format, bool acceptAll) + var acceptParam = Request.Query[HeaderNames.Accept]; + + var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false); + + if (!supportsWebP) { - if (requestAcceptTypes.Contains(format.GetMimeType())) + var userAgent = Request.Headers[HeaderNames.UserAgent].ToString(); + if (userAgent.Contains("crosswalk", StringComparison.OrdinalIgnoreCase) + && userAgent.Contains("android", StringComparison.OrdinalIgnoreCase)) { - return true; + supportsWebP = true; } - - if (acceptAll && requestAcceptTypes.Contains("*/*")) - { - return true; - } - - // Review if this should be jpeg, jpg or both for ImageFormat.Jpg - var normalized = format.ToString().ToLowerInvariant(); - return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase); } - private async Task GetImageResult( - ImageProcessingOptions imageProcessingOptions, - TimeSpan? cacheDuration, - IDictionary headers) + var formats = new List(4); + + if (supportsWebP) { - var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions).ConfigureAwait(false); + formats.Add(ImageFormat.Webp); + } - var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache"); - var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader); + formats.Add(ImageFormat.Jpg); + formats.Add(ImageFormat.Png); - // if the parsing of the IfModifiedSince header was not successful, disable caching - if (!parsingSuccessful) + if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true)) + { + formats.Add(ImageFormat.Gif); + } + + return formats.ToArray(); + } + + private bool SupportsFormat(IReadOnlyCollection requestAcceptTypes, string? acceptParam, ImageFormat format, bool acceptAll) + { + if (requestAcceptTypes.Contains(format.GetMimeType())) + { + return true; + } + + if (acceptAll && requestAcceptTypes.Contains("*/*")) + { + return true; + } + + // Review if this should be jpeg, jpg or both for ImageFormat.Jpg + var normalized = format.ToString().ToLowerInvariant(); + return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase); + } + + private async Task GetImageResult( + ImageProcessingOptions imageProcessingOptions, + TimeSpan? cacheDuration, + IDictionary headers) + { + var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions).ConfigureAwait(false); + + var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache"); + var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader); + + // if the parsing of the IfModifiedSince header was not successful, disable caching + if (!parsingSuccessful) + { + // disableCaching = true; + } + + foreach (var (key, value) in headers) + { + Response.Headers.Add(key, value); + } + + Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain; + Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); + Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept); + + if (disableCaching) + { + Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); + Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate"); + } + else + { + if (cacheDuration.HasValue) { - // disableCaching = true; - } - - foreach (var (key, value) in headers) - { - Response.Headers.Add(key, value); - } - - Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain; - Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture)); - Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept); - - if (disableCaching) - { - Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); - Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate"); + Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds); } else { - if (cacheDuration.HasValue) - { - Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds); - } - else - { - Response.Headers.Add(HeaderNames.CacheControl, "public"); - } - - 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) - { - if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow) - { - Response.StatusCode = StatusCodes.Status304NotModified; - return new ContentResult(); - } - } + Response.Headers.Add(HeaderNames.CacheControl, "public"); } - return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain); + 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) + { + if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow) + { + Response.StatusCode = StatusCodes.Status304NotModified; + return new ContentResult(); + } + } } + + return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain); } } diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 2e0d3cb99e..89592bade4 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -16,346 +16,345 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The instant mix controller. +/// +[Route("")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class InstantMixController : BaseJellyfinApiController { + private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; + private readonly ILibraryManager _libraryManager; + private readonly IMusicManager _musicManager; + /// - /// The instant mix controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class InstantMixController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public InstantMixController( + IUserManager userManager, + IDtoService dtoService, + IMusicManager musicManager, + ILibraryManager libraryManager) { - private readonly IUserManager _userManager; - private readonly IDtoService _dtoService; - private readonly ILibraryManager _libraryManager; - private readonly IMusicManager _musicManager; + _userManager = userManager; + _dtoService = dtoService; + _musicManager = musicManager; + _libraryManager = libraryManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public InstantMixController( - IUserManager userManager, - IDtoService dtoService, - IMusicManager musicManager, - ILibraryManager libraryManager) + /// + /// Creates an instant playlist based on a given song. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include image information in output. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Instant playlist returned. + /// A with the playlist items. + [HttpGet("Songs/{id}/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetInstantMixFromSong( + [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 is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); + return GetResult(items, user, limit, dtoOptions); + } + + /// + /// Creates an instant playlist based on a given album. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include image information in output. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Instant playlist returned. + /// A with the playlist items. + [HttpGet("Albums/{id}/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetInstantMixFromAlbum( + [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 album = _libraryManager.GetItemById(id); + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions); + return GetResult(items, user, limit, dtoOptions); + } + + /// + /// Creates an instant playlist based on a given playlist. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include image information in output. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Instant playlist returned. + /// A with the playlist items. + [HttpGet("Playlists/{id}/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetInstantMixFromPlaylist( + [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 playlist = (Playlist)_libraryManager.GetItemById(id); + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions); + return GetResult(items, user, limit, dtoOptions); + } + + /// + /// Creates an instant playlist based on a given genre. + /// + /// The genre name. + /// Optional. Filter by user id, and attach user data. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include image information in output. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Instant playlist returned. + /// A with the playlist items. + [HttpGet("MusicGenres/{name}/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetInstantMixFromMusicGenreByName( + [FromRoute, Required] string name, + [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 user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions); + return GetResult(items, user, limit, dtoOptions); + } + + /// + /// Creates an instant playlist based on a given artist. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include image information in output. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Instant playlist returned. + /// A with the playlist items. + [HttpGet("Artists/{id}/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetInstantMixFromArtists( + [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 is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); + return GetResult(items, user, limit, dtoOptions); + } + + /// + /// Creates an instant playlist based on a given item. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include image information in output. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Instant playlist returned. + /// A with the playlist items. + [HttpGet("Items/{id}/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetInstantMixFromItem( + [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 is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); + return GetResult(items, user, limit, dtoOptions); + } + + /// + /// Creates an instant playlist based on a given artist. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include image information in output. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Instant playlist returned. + /// A with the playlist items. + [HttpGet("Artists/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Obsolete("Use GetInstantMixFromArtists")] + public ActionResult> GetInstantMixFromArtists2( + [FromQuery, 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) + { + return GetInstantMixFromArtists( + id, + userId, + limit, + fields, + enableImages, + enableUserData, + imageTypeLimit, + enableImageTypes); + } + + /// + /// Creates an instant playlist based on a given genre. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include image information in output. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Instant playlist returned. + /// A with the playlist items. + [HttpGet("MusicGenres/InstantMix")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetInstantMixFromMusicGenreById( + [FromQuery, 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 is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); + return GetResult(items, user, limit, dtoOptions); + } + + private QueryResult GetResult(List items, User? user, int? limit, DtoOptions dtoOptions) + { + var list = items; + + var totalCount = list.Count; + + if (limit.HasValue && limit < list.Count) { - _userManager = userManager; - _dtoService = dtoService; - _musicManager = musicManager; - _libraryManager = libraryManager; + list = list.GetRange(0, limit.Value); } - /// - /// Creates an instant playlist based on a given song. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include image information in output. - /// Optional. Include user data. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Instant playlist returned. - /// A with the playlist items. - [HttpGet("Songs/{id}/InstantMix")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetInstantMixFromSong( - [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 is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); - return GetResult(items, user, limit, dtoOptions); - } + var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user); - /// - /// Creates an instant playlist based on a given album. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include image information in output. - /// Optional. Include user data. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Instant playlist returned. - /// A with the playlist items. - [HttpGet("Albums/{id}/InstantMix")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetInstantMixFromAlbum( - [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 album = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions); - return GetResult(items, user, limit, dtoOptions); - } + var result = new QueryResult( + 0, + totalCount, + returnList); - /// - /// Creates an instant playlist based on a given playlist. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include image information in output. - /// Optional. Include user data. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Instant playlist returned. - /// A with the playlist items. - [HttpGet("Playlists/{id}/InstantMix")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetInstantMixFromPlaylist( - [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 playlist = (Playlist)_libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions); - return GetResult(items, user, limit, dtoOptions); - } - - /// - /// Creates an instant playlist based on a given genre. - /// - /// The genre name. - /// Optional. Filter by user id, and attach user data. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include image information in output. - /// Optional. Include user data. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Instant playlist returned. - /// A with the playlist items. - [HttpGet("MusicGenres/{name}/InstantMix")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetInstantMixFromMusicGenreByName( - [FromRoute, Required] string name, - [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 user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions); - return GetResult(items, user, limit, dtoOptions); - } - - /// - /// Creates an instant playlist based on a given artist. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include image information in output. - /// Optional. Include user data. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Instant playlist returned. - /// A with the playlist items. - [HttpGet("Artists/{id}/InstantMix")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetInstantMixFromArtists( - [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 is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); - return GetResult(items, user, limit, dtoOptions); - } - - /// - /// Creates an instant playlist based on a given item. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include image information in output. - /// Optional. Include user data. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Instant playlist returned. - /// A with the playlist items. - [HttpGet("Items/{id}/InstantMix")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetInstantMixFromItem( - [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 is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); - return GetResult(items, user, limit, dtoOptions); - } - - /// - /// Creates an instant playlist based on a given artist. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include image information in output. - /// Optional. Include user data. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Instant playlist returned. - /// A with the playlist items. - [HttpGet("Artists/InstantMix")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Obsolete("Use GetInstantMixFromArtists")] - public ActionResult> GetInstantMixFromArtists2( - [FromQuery, 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) - { - return GetInstantMixFromArtists( - id, - userId, - limit, - fields, - enableImages, - enableUserData, - imageTypeLimit, - enableImageTypes); - } - - /// - /// Creates an instant playlist based on a given genre. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include image information in output. - /// Optional. Include user data. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Instant playlist returned. - /// A with the playlist items. - [HttpGet("MusicGenres/InstantMix")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetInstantMixFromMusicGenreById( - [FromQuery, 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 is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); - return GetResult(items, user, limit, dtoOptions); - } - - private QueryResult GetResult(List items, User? user, int? limit, DtoOptions dtoOptions) - { - var list = items; - - var totalCount = list.Count; - - if (limit.HasValue && limit < list.Count) - { - list = list.GetRange(0, limit.Value); - } - - var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user); - - var result = new QueryResult( - 0, - totalCount, - returnList); - - return result; - } + return result; } } diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index b6c5504db5..c8468e9f2d 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -18,257 +18,256 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Item lookup controller. +/// +[Route("")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class ItemLookupController : BaseJellyfinApiController { + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + /// - /// Item lookup controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class ItemLookupController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ItemLookupController( + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + ILogger logger) { - private readonly IProviderManager _providerManager; - private readonly IFileSystem _fileSystem; - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + _providerManager = providerManager; + _fileSystem = fileSystem; + _libraryManager = libraryManager; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public ItemLookupController( - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager, - ILogger logger) + /// + /// Get the item's external id info. + /// + /// Item id. + /// External id info retrieved. + /// Item not found. + /// List of external id info. + [HttpGet("Items/{itemId}/ExternalIdInfos")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetExternalIdInfos([FromRoute, Required] Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) { - _providerManager = providerManager; - _fileSystem = fileSystem; - _libraryManager = libraryManager; - _logger = logger; + return NotFound(); } - /// - /// Get the item's external id info. - /// - /// Item id. - /// External id info retrieved. - /// Item not found. - /// List of external id info. - [HttpGet("Items/{itemId}/ExternalIdInfos")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult> GetExternalIdInfos([FromRoute, Required] Guid itemId) - { - var item = _libraryManager.GetItemById(itemId); - if (item is null) + return Ok(_providerManager.GetExternalIdInfos(item)); + } + + /// + /// Get movie remote search. + /// + /// Remote search query. + /// Movie remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("Items/RemoteSearch/Movie")] + public async Task>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get trailer remote search. + /// + /// Remote search query. + /// Trailer remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("Items/RemoteSearch/Trailer")] + public async Task>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get music video remote search. + /// + /// Remote search query. + /// Music video remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("Items/RemoteSearch/MusicVideo")] + public async Task>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get series remote search. + /// + /// Remote search query. + /// Series remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("Items/RemoteSearch/Series")] + public async Task>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get box set remote search. + /// + /// Remote search query. + /// Box set remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("Items/RemoteSearch/BoxSet")] + public async Task>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get music artist remote search. + /// + /// Remote search query. + /// Music artist remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("Items/RemoteSearch/MusicArtist")] + public async Task>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get music album remote search. + /// + /// Remote search query. + /// Music album remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("Items/RemoteSearch/MusicAlbum")] + public async Task>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get person remote search. + /// + /// Remote search query. + /// Person remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("Items/RemoteSearch/Person")] + [Authorize(Policy = Policies.RequiresElevation)] + public async Task>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Get book remote search. + /// + /// Remote search query. + /// Book remote search executed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an containing the list of remote search results. + /// + [HttpPost("Items/RemoteSearch/Book")] + public async Task>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) + { + var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) + .ConfigureAwait(false); + return Ok(results); + } + + /// + /// Applies search criteria to an item and refreshes metadata. + /// + /// Item id. + /// The remote search result. + /// Optional. Whether or not to replace all images. Default: True. + /// Item metadata refreshed. + /// + /// A that represents the asynchronous operation to get the remote search results. + /// The task result contains an . + /// + [HttpPost("Items/RemoteSearch/Apply/{itemId}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task ApplySearchCriteria( + [FromRoute, Required] Guid itemId, + [FromBody, Required] RemoteSearchResult searchResult, + [FromQuery] bool replaceAllImages = true) + { + var item = _libraryManager.GetItemById(itemId); + _logger.LogInformation( + "Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}", + item.Id, + item.Name, + searchResult.ProviderIds); + + // Since the refresh process won't erase provider Ids, we need to set this explicitly now. + item.ProviderIds = searchResult.ProviderIds; + await _providerManager.RefreshFullItem( + item, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { - return NotFound(); - } + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + ReplaceAllMetadata = true, + ReplaceAllImages = replaceAllImages, + SearchResult = searchResult, + RemoveOldMetadata = true + }, + CancellationToken.None).ConfigureAwait(false); - return Ok(_providerManager.GetExternalIdInfos(item)); - } - - /// - /// Get movie remote search. - /// - /// Remote search query. - /// Movie remote search executed. - /// - /// A that represents the asynchronous operation to get the remote search results. - /// The task result contains an containing the list of remote search results. - /// - [HttpPost("Items/RemoteSearch/Movie")] - public async Task>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) - { - var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) - .ConfigureAwait(false); - return Ok(results); - } - - /// - /// Get trailer remote search. - /// - /// Remote search query. - /// Trailer remote search executed. - /// - /// A that represents the asynchronous operation to get the remote search results. - /// The task result contains an containing the list of remote search results. - /// - [HttpPost("Items/RemoteSearch/Trailer")] - public async Task>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) - { - var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) - .ConfigureAwait(false); - return Ok(results); - } - - /// - /// Get music video remote search. - /// - /// Remote search query. - /// Music video remote search executed. - /// - /// A that represents the asynchronous operation to get the remote search results. - /// The task result contains an containing the list of remote search results. - /// - [HttpPost("Items/RemoteSearch/MusicVideo")] - public async Task>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) - { - var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) - .ConfigureAwait(false); - return Ok(results); - } - - /// - /// Get series remote search. - /// - /// Remote search query. - /// Series remote search executed. - /// - /// A that represents the asynchronous operation to get the remote search results. - /// The task result contains an containing the list of remote search results. - /// - [HttpPost("Items/RemoteSearch/Series")] - public async Task>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) - { - var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) - .ConfigureAwait(false); - return Ok(results); - } - - /// - /// Get box set remote search. - /// - /// Remote search query. - /// Box set remote search executed. - /// - /// A that represents the asynchronous operation to get the remote search results. - /// The task result contains an containing the list of remote search results. - /// - [HttpPost("Items/RemoteSearch/BoxSet")] - public async Task>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) - { - var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) - .ConfigureAwait(false); - return Ok(results); - } - - /// - /// Get music artist remote search. - /// - /// Remote search query. - /// Music artist remote search executed. - /// - /// A that represents the asynchronous operation to get the remote search results. - /// The task result contains an containing the list of remote search results. - /// - [HttpPost("Items/RemoteSearch/MusicArtist")] - public async Task>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) - { - var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) - .ConfigureAwait(false); - return Ok(results); - } - - /// - /// Get music album remote search. - /// - /// Remote search query. - /// Music album remote search executed. - /// - /// A that represents the asynchronous operation to get the remote search results. - /// The task result contains an containing the list of remote search results. - /// - [HttpPost("Items/RemoteSearch/MusicAlbum")] - public async Task>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) - { - var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) - .ConfigureAwait(false); - return Ok(results); - } - - /// - /// Get person remote search. - /// - /// Remote search query. - /// Person remote search executed. - /// - /// A that represents the asynchronous operation to get the remote search results. - /// The task result contains an containing the list of remote search results. - /// - [HttpPost("Items/RemoteSearch/Person")] - [Authorize(Policy = Policies.RequiresElevation)] - public async Task>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) - { - var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) - .ConfigureAwait(false); - return Ok(results); - } - - /// - /// Get book remote search. - /// - /// Remote search query. - /// Book remote search executed. - /// - /// A that represents the asynchronous operation to get the remote search results. - /// The task result contains an containing the list of remote search results. - /// - [HttpPost("Items/RemoteSearch/Book")] - public async Task>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) - { - var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) - .ConfigureAwait(false); - return Ok(results); - } - - /// - /// Applies search criteria to an item and refreshes metadata. - /// - /// Item id. - /// The remote search result. - /// Optional. Whether or not to replace all images. Default: True. - /// Item metadata refreshed. - /// - /// A that represents the asynchronous operation to get the remote search results. - /// The task result contains an . - /// - [HttpPost("Items/RemoteSearch/Apply/{itemId}")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task ApplySearchCriteria( - [FromRoute, Required] Guid itemId, - [FromBody, Required] RemoteSearchResult searchResult, - [FromQuery] bool replaceAllImages = true) - { - var item = _libraryManager.GetItemById(itemId); - _logger.LogInformation( - "Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}", - item.Id, - item.Name, - searchResult.ProviderIds); - - // Since the refresh process won't erase provider Ids, we need to set this explicitly now. - item.ProviderIds = searchResult.ProviderIds; - await _providerManager.RefreshFullItem( - item, - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - ReplaceAllMetadata = true, - ReplaceAllImages = replaceAllImages, - SearchResult = searchResult, - RemoveOldMetadata = true - }, - CancellationToken.None).ConfigureAwait(false); - - return NoContent(); - } + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index 0dc3fbd05a..b8f6e91ad2 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -9,78 +9,77 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Item Refresh Controller. +/// +[Route("Items")] +[Authorize(Policy = Policies.RequiresElevation)] +public class ItemRefreshController : BaseJellyfinApiController { + private readonly ILibraryManager _libraryManager; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + /// - /// Item Refresh Controller. + /// Initializes a new instance of the class. /// - [Route("Items")] - [Authorize(Policy = Policies.RequiresElevation)] - public class ItemRefreshController : BaseJellyfinApiController + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public ItemRefreshController( + ILibraryManager libraryManager, + IProviderManager providerManager, + IFileSystem fileSystem) { - private readonly ILibraryManager _libraryManager; - private readonly IProviderManager _providerManager; - private readonly IFileSystem _fileSystem; + _libraryManager = libraryManager; + _providerManager = providerManager; + _fileSystem = fileSystem; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - public ItemRefreshController( - ILibraryManager libraryManager, - IProviderManager providerManager, - IFileSystem fileSystem) + /// + /// Refreshes metadata for an item. + /// + /// Item id. + /// (Optional) Specifies the metadata refresh mode. + /// (Optional) Specifies the image refresh mode. + /// (Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh. + /// (Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh. + /// Item metadata refresh queued. + /// Item to refresh not found. + /// An on success, or a if the item could not be found. + [HttpPost("{itemId}/Refresh")] + [Description("Refreshes metadata for an item.")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult RefreshItem( + [FromRoute, Required] Guid itemId, + [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None, + [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None, + [FromQuery] bool replaceAllMetadata = false, + [FromQuery] bool replaceAllImages = false) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) { - _libraryManager = libraryManager; - _providerManager = providerManager; - _fileSystem = fileSystem; + return NotFound(); } - /// - /// Refreshes metadata for an item. - /// - /// Item id. - /// (Optional) Specifies the metadata refresh mode. - /// (Optional) Specifies the image refresh mode. - /// (Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh. - /// (Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh. - /// Item metadata refresh queued. - /// Item to refresh not found. - /// An on success, or a if the item could not be found. - [HttpPost("{itemId}/Refresh")] - [Description("Refreshes metadata for an item.")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult RefreshItem( - [FromRoute, Required] Guid itemId, - [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None, - [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None, - [FromQuery] bool replaceAllMetadata = false, - [FromQuery] bool replaceAllImages = false) + var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } + MetadataRefreshMode = metadataRefreshMode, + ImageRefreshMode = imageRefreshMode, + ReplaceAllImages = replaceAllImages, + ReplaceAllMetadata = replaceAllMetadata, + ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh + || imageRefreshMode == MetadataRefreshMode.FullRefresh + || replaceAllImages + || replaceAllMetadata, + IsAutomated = false + }; - var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - MetadataRefreshMode = metadataRefreshMode, - ImageRefreshMode = imageRefreshMode, - ReplaceAllImages = replaceAllImages, - ReplaceAllMetadata = replaceAllMetadata, - ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh - || imageRefreshMode == MetadataRefreshMode.FullRefresh - || replaceAllImages - || replaceAllMetadata, - IsAutomated = false - }; - - _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High); - return NoContent(); - } + _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High); + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index af3d779f56..230fbfb2cd 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -20,332 +20,332 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Item update controller. +/// +[Route("")] +[Authorize(Policy = Policies.RequiresElevation)] +public class ItemUpdateController : BaseJellyfinApiController { + private readonly ILibraryManager _libraryManager; + private readonly IProviderManager _providerManager; + private readonly ILocalizationManager _localizationManager; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _serverConfigurationManager; + /// - /// Item update controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.RequiresElevation)] - public class ItemUpdateController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ItemUpdateController( + IFileSystem fileSystem, + ILibraryManager libraryManager, + IProviderManager providerManager, + ILocalizationManager localizationManager, + IServerConfigurationManager serverConfigurationManager) { - private readonly ILibraryManager _libraryManager; - private readonly IProviderManager _providerManager; - private readonly ILocalizationManager _localizationManager; - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _serverConfigurationManager; + _libraryManager = libraryManager; + _providerManager = providerManager; + _localizationManager = localizationManager; + _fileSystem = fileSystem; + _serverConfigurationManager = serverConfigurationManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public ItemUpdateController( - IFileSystem fileSystem, - ILibraryManager libraryManager, - IProviderManager providerManager, - ILocalizationManager localizationManager, - IServerConfigurationManager serverConfigurationManager) + /// + /// Updates an item. + /// + /// The item id. + /// The new item properties. + /// Item updated. + /// Item not found. + /// An on success, or a if the item could not be found. + [HttpPost("Items/{itemId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) { - _libraryManager = libraryManager; - _providerManager = providerManager; - _localizationManager = localizationManager; - _fileSystem = fileSystem; - _serverConfigurationManager = serverConfigurationManager; + return NotFound(); } - /// - /// Updates an item. - /// - /// The item id. - /// The new item properties. - /// Item updated. - /// Item not found. - /// An on success, or a if the item could not be found. - [HttpPost("Items/{itemId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request) + var newLockData = request.LockData ?? false; + var isLockedChanged = item.IsLocked != newLockData; + + var series = item as Series; + var displayOrderChanged = series is not null && !string.Equals( + series.DisplayOrder ?? string.Empty, + request.DisplayOrder ?? string.Empty, + StringComparison.OrdinalIgnoreCase); + + // Do this first so that metadata savers can pull the updates from the database. + if (request.People is not null) { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } - - var newLockData = request.LockData ?? false; - var isLockedChanged = item.IsLocked != newLockData; - - var series = item as Series; - var displayOrderChanged = series is not null && !string.Equals( - series.DisplayOrder ?? string.Empty, - request.DisplayOrder ?? string.Empty, - StringComparison.OrdinalIgnoreCase); - - // Do this first so that metadata savers can pull the updates from the database. - if (request.People is not null) - { - _libraryManager.UpdatePeople( - item, - request.People.Select(x => new PersonInfo - { - Name = x.Name, - Role = x.Role, - Type = x.Type - }).ToList()); - } - - UpdateItem(request, item); - - item.OnMetadataChanged(); - - await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - - if (isLockedChanged && item.IsFolder) - { - var folder = (Folder)item; - - foreach (var child in folder.GetRecursiveChildren()) + _libraryManager.UpdatePeople( + item, + request.People.Select(x => new PersonInfo { - child.IsLocked = newLockData; - await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - } - } - - if (displayOrderChanged) - { - _providerManager.QueueRefresh( - series!.Id, - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - ReplaceAllMetadata = true - }, - RefreshPriority.High); - } - - return NoContent(); + Name = x.Name, + Role = x.Role, + Type = x.Type + }).ToList()); } - /// - /// Gets metadata editor info for an item. - /// - /// The item id. - /// Item metadata editor returned. - /// Item not found. - /// An on success containing the metadata editor, or a if the item could not be found. - [HttpGet("Items/{itemId}/MetadataEditor")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetMetadataEditorInfo([FromRoute, Required] Guid itemId) + UpdateItem(request, item); + + item.OnMetadataChanged(); + + await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); + + if (isLockedChanged && item.IsFolder) { - var item = _libraryManager.GetItemById(itemId); + var folder = (Folder)item; - var info = new MetadataEditorInfo + foreach (var child in folder.GetRecursiveChildren()) { - ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(), - ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(), - Countries = _localizationManager.GetCountries().ToArray(), - Cultures = _localizationManager.GetCultures().ToArray() - }; - - if (!item.IsVirtualItem - && 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); - var configuredContentType = _libraryManager.GetConfiguredContentType(item); - - if (string.IsNullOrWhiteSpace(inheritedContentType) || - !string.IsNullOrWhiteSpace(configuredContentType)) - { - info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); - info.ContentType = configuredContentType; - - if (string.IsNullOrWhiteSpace(inheritedContentType) - || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) - { - info.ContentTypeOptions = info.ContentTypeOptions - .Where(i => string.IsNullOrWhiteSpace(i.Value) - || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - } - } + child.IsLocked = newLockData; + await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } - - return info; } - /// - /// Updates an item's content type. - /// - /// The item id. - /// The content type of the item. - /// Item content type updated. - /// Item not found. - /// An on success, or a if the item could not be found. - [HttpPost("Items/{itemId}/ContentType")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType) + if (displayOrderChanged) { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } - - var path = item.ContainingFolderPath; - - var types = _serverConfigurationManager.Configuration.ContentTypes - .Where(i => !string.IsNullOrWhiteSpace(i.Name)) - .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - if (!string.IsNullOrWhiteSpace(contentType)) - { - types.Add(new NameValuePair + _providerManager.QueueRefresh( + series!.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { - Name = path, - Value = contentType - }); - } - - _serverConfigurationManager.Configuration.ContentTypes = types.ToArray(); - _serverConfigurationManager.SaveConfiguration(); - return NoContent(); + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + ReplaceAllMetadata = true + }, + RefreshPriority.High); } - private void UpdateItem(BaseItemDto request, BaseItem item) + return NoContent(); + } + + /// + /// Gets metadata editor info for an item. + /// + /// The item id. + /// Item metadata editor returned. + /// Item not found. + /// An on success containing the metadata editor, or a if the item could not be found. + [HttpGet("Items/{itemId}/MetadataEditor")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetMetadataEditorInfo([FromRoute, Required] Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + + var info = new MetadataEditorInfo { - item.Name = request.Name; - item.ForcedSortName = request.ForcedSortName; + ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(), + ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(), + Countries = _localizationManager.GetCountries().ToArray(), + Cultures = _localizationManager.GetCultures().ToArray() + }; - item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle; + if (!item.IsVirtualItem + && 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); + var configuredContentType = _libraryManager.GetConfiguredContentType(item); - item.CriticRating = request.CriticRating; - - item.CommunityRating = request.CommunityRating; - item.IndexNumber = request.IndexNumber; - item.ParentIndexNumber = request.ParentIndexNumber; - item.Overview = request.Overview; - item.Genres = request.Genres; - - if (item is Episode episode) + if (string.IsNullOrWhiteSpace(inheritedContentType) || + !string.IsNullOrWhiteSpace(configuredContentType)) { - episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber; - episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber; - episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber; - } + info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); + info.ContentType = configuredContentType; - item.Tags = request.Tags; - - if (request.Taglines is not null) - { - item.Tagline = request.Taglines.FirstOrDefault(); - } - - if (request.Studios is not null) - { - item.Studios = request.Studios.Select(x => x.Name).ToArray(); - } - - if (request.DateCreated.HasValue) - { - item.DateCreated = NormalizeDateTime(request.DateCreated.Value); - } - - 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; - - if (request.ProductionLocations is not null) - { - item.ProductionLocations = request.ProductionLocations; - } - - item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode; - item.PreferredMetadataLanguage = request.PreferredMetadataLanguage; - - if (item is IHasDisplayOrder hasDisplayOrder) - { - hasDisplayOrder.DisplayOrder = request.DisplayOrder; - } - - if (item is IHasAspectRatio hasAspectRatio) - { - hasAspectRatio.AspectRatio = request.AspectRatio; - } - - item.IsLocked = request.LockData ?? false; - - if (request.LockedFields is not null) - { - item.LockedFields = request.LockedFields; - } - - // Only allow this for series. Runtimes for media comes from ffprobe. - if (item is Series) - { - item.RunTimeTicks = request.RunTimeTicks; - } - - foreach (var pair in request.ProviderIds.ToList()) - { - if (string.IsNullOrEmpty(pair.Value)) + if (string.IsNullOrWhiteSpace(inheritedContentType) + || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { - request.ProviderIds.Remove(pair.Key); - } - } - - item.ProviderIds = request.ProviderIds; - - if (item is Video video) - { - video.Video3DFormat = request.Video3DFormat; - } - - if (request.AlbumArtists is not null) - { - if (item is IHasAlbumArtist hasAlbumArtists) - { - hasAlbumArtists.AlbumArtists = request - .AlbumArtists - .Select(i => i.Name) + info.ContentTypeOptions = info.ContentTypeOptions + .Where(i => string.IsNullOrWhiteSpace(i.Value) + || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) .ToArray(); } } + } - if (request.ArtistItems is not null) + return info; + } + + /// + /// Updates an item's content type. + /// + /// The item id. + /// The content type of the item. + /// Item content type updated. + /// Item not found. + /// An on success, or a if the item could not be found. + [HttpPost("Items/{itemId}/ContentType")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + var path = item.ContainingFolderPath; + + var types = _serverConfigurationManager.Configuration.ContentTypes + .Where(i => !string.IsNullOrWhiteSpace(i.Name)) + .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (!string.IsNullOrWhiteSpace(contentType)) + { + types.Add(new NameValuePair { - if (item is IHasArtist hasArtists) - { - hasArtists.Artists = request - .ArtistItems - .Select(i => i.Name) - .ToArray(); - } + Name = path, + Value = contentType + }); + } + + _serverConfigurationManager.Configuration.ContentTypes = types.ToArray(); + _serverConfigurationManager.SaveConfiguration(); + return NoContent(); + } + + private void UpdateItem(BaseItemDto request, BaseItem item) + { + item.Name = request.Name; + item.ForcedSortName = request.ForcedSortName; + + item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle; + + item.CriticRating = request.CriticRating; + + item.CommunityRating = request.CommunityRating; + item.IndexNumber = request.IndexNumber; + item.ParentIndexNumber = request.ParentIndexNumber; + item.Overview = request.Overview; + item.Genres = request.Genres; + + if (item is Episode episode) + { + episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber; + episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber; + episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber; + } + + item.Tags = request.Tags; + + if (request.Taglines is not null) + { + item.Tagline = request.Taglines.FirstOrDefault(); + } + + if (request.Studios is not null) + { + item.Studios = request.Studios.Select(x => x.Name).ToArray(); + } + + if (request.DateCreated.HasValue) + { + item.DateCreated = NormalizeDateTime(request.DateCreated.Value); + } + + 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; + + if (request.ProductionLocations is not null) + { + item.ProductionLocations = request.ProductionLocations; + } + + item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode; + item.PreferredMetadataLanguage = request.PreferredMetadataLanguage; + + if (item is IHasDisplayOrder hasDisplayOrder) + { + hasDisplayOrder.DisplayOrder = request.DisplayOrder; + } + + if (item is IHasAspectRatio hasAspectRatio) + { + hasAspectRatio.AspectRatio = request.AspectRatio; + } + + item.IsLocked = request.LockData ?? false; + + if (request.LockedFields is not null) + { + item.LockedFields = request.LockedFields; + } + + // Only allow this for series. Runtimes for media comes from ffprobe. + if (item is Series) + { + item.RunTimeTicks = request.RunTimeTicks; + } + + foreach (var pair in request.ProviderIds.ToList()) + { + if (string.IsNullOrEmpty(pair.Value)) + { + request.ProviderIds.Remove(pair.Key); } + } - switch (item) + item.ProviderIds = request.ProviderIds; + + if (item is Video video) + { + video.Video3DFormat = request.Video3DFormat; + } + + if (request.AlbumArtists is not null) + { + if (item is IHasAlbumArtist hasAlbumArtists) { - case Audio song: - song.Album = request.Album; - break; - case MusicVideo musicVideo: - musicVideo.Album = request.Album; - break; - case Series series: + hasAlbumArtists.AlbumArtists = request + .AlbumArtists + .Select(i => i.Name) + .ToArray(); + } + } + + if (request.ArtistItems is not null) + { + if (item is IHasArtist hasArtists) + { + hasArtists.Artists = request + .ArtistItems + .Select(i => i.Name) + .ToArray(); + } + } + + switch (item) + { + case Audio song: + song.Album = request.Album; + break; + case MusicVideo musicVideo: + musicVideo.Album = request.Album; + break; + case Series series: { series.Status = GetSeriesStatus(request); @@ -357,93 +357,92 @@ namespace Jellyfin.Api.Controllers break; } - } - } - - private SeriesStatus? GetSeriesStatus(BaseItemDto item) - { - if (string.IsNullOrEmpty(item.Status)) - { - return null; - } - - return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true); - } - - private DateTime NormalizeDateTime(DateTime val) - { - return DateTime.SpecifyKind(val, DateTimeKind.Utc); - } - - private List GetContentTypeOptions(bool isForItem) - { - var list = new List(); - - if (isForItem) - { - list.Add(new NameValuePair - { - Name = "Inherit", - Value = string.Empty - }); - } - - list.Add(new NameValuePair - { - Name = "Movies", - Value = "movies" - }); - list.Add(new NameValuePair - { - Name = "Music", - Value = "music" - }); - list.Add(new NameValuePair - { - Name = "Shows", - Value = "tvshows" - }); - - if (!isForItem) - { - list.Add(new NameValuePair - { - Name = "Books", - Value = "books" - }); - } - - list.Add(new NameValuePair - { - Name = "HomeVideos", - Value = "homevideos" - }); - list.Add(new NameValuePair - { - Name = "MusicVideos", - Value = "musicvideos" - }); - list.Add(new NameValuePair - { - Name = "Photos", - Value = "photos" - }); - - if (!isForItem) - { - list.Add(new NameValuePair - { - Name = "MixedContent", - Value = string.Empty - }); - } - - foreach (var val in list) - { - val.Name = _localizationManager.GetLocalizedString(val.Name); - } - - return list; } } + + private SeriesStatus? GetSeriesStatus(BaseItemDto item) + { + if (string.IsNullOrEmpty(item.Status)) + { + return null; + } + + return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true); + } + + private DateTime NormalizeDateTime(DateTime val) + { + return DateTime.SpecifyKind(val, DateTimeKind.Utc); + } + + private List GetContentTypeOptions(bool isForItem) + { + var list = new List(); + + if (isForItem) + { + list.Add(new NameValuePair + { + Name = "Inherit", + Value = string.Empty + }); + } + + list.Add(new NameValuePair + { + Name = "Movies", + Value = "movies" + }); + list.Add(new NameValuePair + { + Name = "Music", + Value = "music" + }); + list.Add(new NameValuePair + { + Name = "Shows", + Value = "tvshows" + }); + + if (!isForItem) + { + list.Add(new NameValuePair + { + Name = "Books", + Value = "books" + }); + } + + list.Add(new NameValuePair + { + Name = "HomeVideos", + Value = "homevideos" + }); + list.Add(new NameValuePair + { + Name = "MusicVideos", + Value = "musicvideos" + }); + list.Add(new NameValuePair + { + Name = "Photos", + Value = "photos" + }); + + if (!isForItem) + { + list.Add(new NameValuePair + { + Name = "MixedContent", + Value = string.Empty + }); + } + + foreach (var val in list) + { + val.Name = _localizationManager.GetLocalizedString(val.Name); + } + + return list; + } } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 717ddc32b3..44080353dc 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -20,854 +20,853 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The items controller. +/// +[Route("")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class ItemsController : BaseJellyfinApiController { + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + private readonly IDtoService _dtoService; + private readonly ILogger _logger; + private readonly ISessionManager _sessionManager; + /// - /// The items controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class ItemsController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ItemsController( + IUserManager userManager, + ILibraryManager libraryManager, + ILocalizationManager localization, + IDtoService dtoService, + ILogger logger, + ISessionManager sessionManager) { - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localization; - private readonly IDtoService _dtoService; - private readonly ILogger _logger; - private readonly ISessionManager _sessionManager; + _userManager = userManager; + _libraryManager = libraryManager; + _localization = localization; + _dtoService = dtoService; + _logger = logger; + _sessionManager = sessionManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public ItemsController( - IUserManager userManager, - ILibraryManager libraryManager, - ILocalizationManager localization, - IDtoService dtoService, - ILogger logger, - ISessionManager sessionManager) + /// + /// Gets items based on a query. + /// + /// The user id supplied as query parameter; this is required when not using an API key. + /// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc). + /// Optional filter by items with theme songs. + /// Optional filter by items with theme videos. + /// Optional filter by items with subtitles. + /// Optional filter by items with special features. + /// Optional filter by items with trailers. + /// Optional. Return items that are siblings of a supplied item. + /// Optional filter by parent index number. + /// Optional filter by items that have or do not have a parental rating. + /// Optional filter by items that are HD or not. + /// Optional filter by items that are 4K or not. + /// Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited. + /// Optional filter by items that are missing episodes or not. + /// Optional filter by items that are unaired episodes or not. + /// Optional filter by minimum community rating. + /// Optional filter by minimum critic rating. + /// Optional. The minimum premiere date. Format = ISO. + /// Optional. The minimum last saved date. Format = ISO. + /// Optional. The minimum last saved date for the current user. Format = ISO. + /// Optional. The maximum premiere date. Format = ISO. + /// Optional filter by items that have an overview or not. + /// Optional filter by items that have an IMDb id or not. + /// Optional filter by items that have a TMDb id or not. + /// Optional filter by items that have a TVDb id or not. + /// Optional filter for live tv movies. + /// Optional filter for live tv series. + /// Optional filter for live tv news. + /// Optional filter for live tv kids. + /// Optional filter for live tv sports. + /// Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// When searching within folders, this determines whether or not the search will be recursive. true/false. + /// Optional. Filter based on a search term. + /// Sort Order - Ascending, Descending. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional filter by items that are marked as favorite, or not. + /// Optional filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited. + /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. + /// Optional filter by items that are played, or not. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person id. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered to include only those containing the specified artist id. + /// Optional. If specified, results will be filtered to include only those containing the specified album artist id. + /// Optional. If specified, results will be filtered to include only those containing the specified contributing artist id. + /// Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited. + /// Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited. + /// Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited. + /// Optional filter by minimum official rating (PG, PG-13, TV-MA, etc). + /// Optional filter by items that are locked. + /// Optional filter by items that are placeholders. + /// Optional filter by items that have official ratings. + /// Whether or not to hide items behind their boxsets. + /// Optional. Filter by the minimum width of the item. + /// Optional. Filter by the minimum height of the item. + /// Optional. Filter by the maximum width of the item. + /// Optional. Filter by the maximum height of the item. + /// Optional filter by items that are 3D, or not. + /// Optional filter by Series Status. Allows multiple, comma delimited. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. + /// Optional. Enable the total record count. + /// Optional, include image information in output. + /// A with the items. + [HttpGet("Items")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetItems( + [FromQuery] Guid? userId, + [FromQuery] string? maxOfficialRating, + [FromQuery] bool? hasThemeSong, + [FromQuery] bool? hasThemeVideo, + [FromQuery] bool? hasSubtitles, + [FromQuery] bool? hasSpecialFeature, + [FromQuery] bool? hasTrailer, + [FromQuery] Guid? adjacentTo, + [FromQuery] int? parentIndexNumber, + [FromQuery] bool? hasParentalRating, + [FromQuery] bool? isHd, + [FromQuery] bool? is4K, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, + [FromQuery] bool? isMissing, + [FromQuery] bool? isUnaired, + [FromQuery] double? minCommunityRating, + [FromQuery] double? minCriticRating, + [FromQuery] DateTime? minPremiereDate, + [FromQuery] DateTime? minDateLastSaved, + [FromQuery] DateTime? minDateLastSavedForUser, + [FromQuery] DateTime? maxPremiereDate, + [FromQuery] bool? hasOverview, + [FromQuery] bool? hasImdbId, + [FromQuery] bool? hasTmdbId, + [FromQuery] bool? hasTvdbId, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] bool? recursive, + [FromQuery] string? searchTerm, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery] bool? isFavorite, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery] bool? isPlayed, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] string? person, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, + [FromQuery] string? minOfficialRating, + [FromQuery] bool? isLocked, + [FromQuery] bool? isPlaceHolder, + [FromQuery] bool? hasOfficialRating, + [FromQuery] bool? collapseBoxSetItems, + [FromQuery] int? minWidth, + [FromQuery] int? minHeight, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] bool? is3D, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus, + [FromQuery] string? nameStartsWithOrGreater, + [FromQuery] string? nameStartsWith, + [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery] bool enableTotalRecordCount = true, + [FromQuery] bool? enableImages = true) + { + var isApiKey = User.GetIsApiKey(); + // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method + var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default) + ? _userManager.GetUserById(userId.Value) + : null; + + // beyond this point, we're either using an api key or we have a valid user + if (!isApiKey && user is null) { - _userManager = userManager; - _libraryManager = libraryManager; - _localization = localization; - _dtoService = dtoService; - _logger = logger; - _sessionManager = sessionManager; + return BadRequest("userId is required"); } - /// - /// Gets items based on a query. - /// - /// The user id supplied as query parameter; this is required when not using an API key. - /// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc). - /// Optional filter by items with theme songs. - /// Optional filter by items with theme videos. - /// Optional filter by items with subtitles. - /// Optional filter by items with special features. - /// Optional filter by items with trailers. - /// Optional. Return items that are siblings of a supplied item. - /// Optional filter by parent index number. - /// Optional filter by items that have or do not have a parental rating. - /// Optional filter by items that are HD or not. - /// Optional filter by items that are 4K or not. - /// Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited. - /// Optional filter by items that are missing episodes or not. - /// Optional filter by items that are unaired episodes or not. - /// Optional filter by minimum community rating. - /// Optional filter by minimum critic rating. - /// Optional. The minimum premiere date. Format = ISO. - /// Optional. The minimum last saved date. Format = ISO. - /// Optional. The minimum last saved date for the current user. Format = ISO. - /// Optional. The maximum premiere date. Format = ISO. - /// Optional filter by items that have an overview or not. - /// Optional filter by items that have an IMDb id or not. - /// Optional filter by items that have a TMDb id or not. - /// Optional filter by items that have a TVDb id or not. - /// Optional filter for live tv movies. - /// Optional filter for live tv series. - /// Optional filter for live tv news. - /// Optional filter for live tv kids. - /// Optional filter for live tv sports. - /// Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// When searching within folders, this determines whether or not the search will be recursive. true/false. - /// Optional. Filter based on a search term. - /// Sort Order - Ascending, Descending. - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. - /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited. - /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. - /// Optional filter by items that are marked as favorite, or not. - /// Optional filter by MediaType. Allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited. - /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. - /// Optional filter by items that are played, or not. - /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. - /// Optional, include user data. - /// Optional, the max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. If specified, results will be filtered to include only those containing the specified person. - /// Optional. If specified, results will be filtered to include only those containing the specified person id. - /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. - /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered to include only those containing the specified artist id. - /// Optional. If specified, results will be filtered to include only those containing the specified album artist id. - /// Optional. If specified, results will be filtered to include only those containing the specified contributing artist id. - /// Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited. - /// Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited. - /// Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited. - /// Optional filter by minimum official rating (PG, PG-13, TV-MA, etc). - /// Optional filter by items that are locked. - /// Optional filter by items that are placeholders. - /// Optional filter by items that have official ratings. - /// Whether or not to hide items behind their boxsets. - /// Optional. Filter by the minimum width of the item. - /// Optional. Filter by the minimum height of the item. - /// Optional. Filter by the maximum width of the item. - /// Optional. Filter by the maximum height of the item. - /// Optional filter by items that are 3D, or not. - /// Optional filter by Series Status. Allows multiple, comma delimited. - /// Optional filter by items whose name is sorted equally or greater than a given input string. - /// Optional filter by items whose name is sorted equally than a given input string. - /// Optional filter by items whose name is equally or lesser than a given input string. - /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. - /// Optional. Enable the total record count. - /// Optional, include image information in output. - /// A with the items. - [HttpGet("Items")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetItems( - [FromQuery] Guid? userId, - [FromQuery] string? maxOfficialRating, - [FromQuery] bool? hasThemeSong, - [FromQuery] bool? hasThemeVideo, - [FromQuery] bool? hasSubtitles, - [FromQuery] bool? hasSpecialFeature, - [FromQuery] bool? hasTrailer, - [FromQuery] Guid? adjacentTo, - [FromQuery] int? parentIndexNumber, - [FromQuery] bool? hasParentalRating, - [FromQuery] bool? isHd, - [FromQuery] bool? is4K, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, - [FromQuery] bool? isMissing, - [FromQuery] bool? isUnaired, - [FromQuery] double? minCommunityRating, - [FromQuery] double? minCriticRating, - [FromQuery] DateTime? minPremiereDate, - [FromQuery] DateTime? minDateLastSaved, - [FromQuery] DateTime? minDateLastSavedForUser, - [FromQuery] DateTime? maxPremiereDate, - [FromQuery] bool? hasOverview, - [FromQuery] bool? hasImdbId, - [FromQuery] bool? hasTmdbId, - [FromQuery] bool? hasTvdbId, - [FromQuery] bool? isMovie, - [FromQuery] bool? isSeries, - [FromQuery] bool? isNews, - [FromQuery] bool? isKids, - [FromQuery] bool? isSports, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] bool? recursive, - [FromQuery] string? searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, - [FromQuery] bool? isPlayed, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, - [FromQuery] bool? enableUserData, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? person, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, - [FromQuery] string? minOfficialRating, - [FromQuery] bool? isLocked, - [FromQuery] bool? isPlaceHolder, - [FromQuery] bool? hasOfficialRating, - [FromQuery] bool? collapseBoxSetItems, - [FromQuery] int? minWidth, - [FromQuery] int? minHeight, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] bool? is3D, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus, - [FromQuery] string? nameStartsWithOrGreater, - [FromQuery] string? nameStartsWith, - [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, - [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool? enableImages = true) + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + if (includeItemTypes.Length == 1 + && (includeItemTypes[0] == BaseItemKind.Playlist + || includeItemTypes[0] == BaseItemKind.BoxSet)) { - var isApiKey = User.GetIsApiKey(); - // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method - var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default) - ? _userManager.GetUserById(userId.Value) - : null; - - // beyond this point, we're either using an api key or we have a valid user - if (!isApiKey && user is null) - { - return BadRequest("userId is required"); - } - - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - - if (includeItemTypes.Length == 1 - && (includeItemTypes[0] == BaseItemKind.Playlist - || includeItemTypes[0] == BaseItemKind.BoxSet)) - { - parentId = null; - } - - var item = _libraryManager.GetParentItem(parentId, userId); - QueryResult result; - - if (item is not Folder folder) - { - folder = _libraryManager.GetUserRootFolder(); - } - - string? collectionType = null; - if (folder is IHasCollectionType hasCollectionType) - { - collectionType = hasCollectionType.CollectionType; - } - - if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) - { - recursive = true; - includeItemTypes = new[] { BaseItemKind.Playlist }; - } - - if (item is not UserRootFolder - // api keys can always access all folders - && !isApiKey - // check the item is visible for the user - && !item.IsVisible(user)) - { - _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user!.Username, item.Name); - return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); - } - - if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder) - { - var query = new InternalItemsQuery(user) - { - IsPlayed = isPlayed, - MediaTypes = mediaTypes, - IncludeItemTypes = includeItemTypes, - ExcludeItemTypes = excludeItemTypes, - Recursive = recursive ?? false, - OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), - IsFavorite = isFavorite, - Limit = limit, - StartIndex = startIndex, - IsMissing = isMissing, - IsUnaired = isUnaired, - CollapseBoxSetItems = collapseBoxSetItems, - NameLessThan = nameLessThan, - NameStartsWith = nameStartsWith, - NameStartsWithOrGreater = nameStartsWithOrGreater, - HasImdbId = hasImdbId, - IsPlaceHolder = isPlaceHolder, - IsLocked = isLocked, - MinWidth = minWidth, - MinHeight = minHeight, - MaxWidth = maxWidth, - MaxHeight = maxHeight, - Is3D = is3D, - HasTvdbId = hasTvdbId, - HasTmdbId = hasTmdbId, - IsMovie = isMovie, - IsSeries = isSeries, - IsNews = isNews, - IsKids = isKids, - IsSports = isSports, - HasOverview = hasOverview, - HasOfficialRating = hasOfficialRating, - HasParentalRating = hasParentalRating, - HasSpecialFeature = hasSpecialFeature, - HasSubtitles = hasSubtitles, - HasThemeSong = hasThemeSong, - HasThemeVideo = hasThemeVideo, - HasTrailer = hasTrailer, - IsHD = isHd, - Is4K = is4K, - Tags = tags, - OfficialRatings = officialRatings, - Genres = genres, - ArtistIds = artistIds, - AlbumArtistIds = albumArtistIds, - ContributingArtistIds = contributingArtistIds, - GenreIds = genreIds, - StudioIds = studioIds, - Person = person, - PersonIds = personIds, - PersonTypes = personTypes, - Years = years, - ImageTypes = imageTypes, - VideoTypes = videoTypes, - AdjacentTo = adjacentTo, - ItemIds = ids, - MinCommunityRating = minCommunityRating, - MinCriticRating = minCriticRating, - ParentId = parentId ?? Guid.Empty, - ParentIndexNumber = parentIndexNumber, - EnableTotalRecordCount = enableTotalRecordCount, - ExcludeItemIds = excludeItemIds, - DtoOptions = dtoOptions, - SearchTerm = searchTerm, - MinDateLastSaved = minDateLastSaved?.ToUniversalTime(), - MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(), - MinPremiereDate = minPremiereDate?.ToUniversalTime(), - MaxPremiereDate = maxPremiereDate?.ToUniversalTime(), - }; - - if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm)) - { - query.CollapseBoxSetItems = false; - } - - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - } - } - - // Filter by Series Status - if (seriesStatus.Length != 0) - { - query.SeriesStatuses = seriesStatus; - } - - // ExcludeLocationTypes - if (excludeLocationTypes.Any(t => t == LocationType.Virtual)) - { - query.IsVirtualItem = false; - } - - if (locationTypes.Length > 0 && locationTypes.Length < 4) - { - query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual); - } - - // Min official rating - if (!string.IsNullOrWhiteSpace(minOfficialRating)) - { - query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating); - } - - // Max official rating - if (!string.IsNullOrWhiteSpace(maxOfficialRating)) - { - query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating); - } - - // Artists - if (artists.Length != 0) - { - query.ArtistIds = artists.Select(i => - { - try - { - return _libraryManager.GetArtist(i, new DtoOptions(false)); - } - catch - { - return null; - } - }).Where(i => i is not null).Select(i => i!.Id).ToArray(); - } - - // ExcludeArtistIds - if (excludeArtistIds.Length != 0) - { - query.ExcludeArtistIds = excludeArtistIds; - } - - if (albumIds.Length != 0) - { - query.AlbumIds = albumIds; - } - - // Albums - if (albums.Length != 0) - { - query.AlbumIds = albums.SelectMany(i => - { - return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = i, Limit = 1 }); - }).ToArray(); - } - - // Studios - if (studios.Length != 0) - { - query.StudioIds = studios.Select(i => - { - try - { - return _libraryManager.GetStudio(i); - } - catch - { - return null; - } - }).Where(i => i is not null).Select(i => i!.Id).ToArray(); - } - - // Apply default sorting if none requested - if (query.OrderBy.Count == 0) - { - // Albums by artist - if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum) - { - query.OrderBy = new[] { (ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Ascending) }; - } - } - - result = folder.GetItems(query); - } - else - { - var itemsArray = folder.GetChildren(user, true); - result = new QueryResult(itemsArray); - } - - return new QueryResult( - startIndex, - result.TotalRecordCount, - _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user)); + parentId = null; } - /// - /// Gets items based on a query. - /// - /// The user id supplied as query parameter. - /// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc). - /// Optional filter by items with theme songs. - /// Optional filter by items with theme videos. - /// Optional filter by items with subtitles. - /// Optional filter by items with special features. - /// Optional filter by items with trailers. - /// Optional. Return items that are siblings of a supplied item. - /// Optional filter by parent index number. - /// Optional filter by items that have or do not have a parental rating. - /// Optional filter by items that are HD or not. - /// Optional filter by items that are 4K or not. - /// Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited. - /// Optional filter by items that are missing episodes or not. - /// Optional filter by items that are unaired episodes or not. - /// Optional filter by minimum community rating. - /// Optional filter by minimum critic rating. - /// Optional. The minimum premiere date. Format = ISO. - /// Optional. The minimum last saved date. Format = ISO. - /// Optional. The minimum last saved date for the current user. Format = ISO. - /// Optional. The maximum premiere date. Format = ISO. - /// Optional filter by items that have an overview or not. - /// Optional filter by items that have an IMDb id or not. - /// Optional filter by items that have a TMDb id or not. - /// Optional filter by items that have a TVDb id or not. - /// Optional filter for live tv movies. - /// Optional filter for live tv series. - /// Optional filter for live tv news. - /// Optional filter for live tv kids. - /// Optional filter for live tv sports. - /// Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// When searching within folders, this determines whether or not the search will be recursive. true/false. - /// Optional. Filter based on a search term. - /// Sort Order - Ascending, Descending. - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. - /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited. - /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. - /// Optional filter by items that are marked as favorite, or not. - /// Optional filter by MediaType. Allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited. - /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. - /// Optional filter by items that are played, or not. - /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. - /// Optional, include user data. - /// Optional, the max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. If specified, results will be filtered to include only those containing the specified person. - /// Optional. If specified, results will be filtered to include only those containing the specified person id. - /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. - /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered to include only those containing the specified artist id. - /// Optional. If specified, results will be filtered to include only those containing the specified album artist id. - /// Optional. If specified, results will be filtered to include only those containing the specified contributing artist id. - /// Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited. - /// Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited. - /// Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited. - /// Optional filter by minimum official rating (PG, PG-13, TV-MA, etc). - /// Optional filter by items that are locked. - /// Optional filter by items that are placeholders. - /// Optional filter by items that have official ratings. - /// Whether or not to hide items behind their boxsets. - /// Optional. Filter by the minimum width of the item. - /// Optional. Filter by the minimum height of the item. - /// Optional. Filter by the maximum width of the item. - /// Optional. Filter by the maximum height of the item. - /// Optional filter by items that are 3D, or not. - /// Optional filter by Series Status. Allows multiple, comma delimited. - /// Optional filter by items whose name is sorted equally or greater than a given input string. - /// Optional filter by items whose name is sorted equally than a given input string. - /// Optional filter by items whose name is equally or lesser than a given input string. - /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. - /// Optional. Enable the total record count. - /// Optional, include image information in output. - /// A with the items. - [HttpGet("Users/{userId}/Items")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetItemsByUserId( - [FromRoute] Guid userId, - [FromQuery] string? maxOfficialRating, - [FromQuery] bool? hasThemeSong, - [FromQuery] bool? hasThemeVideo, - [FromQuery] bool? hasSubtitles, - [FromQuery] bool? hasSpecialFeature, - [FromQuery] bool? hasTrailer, - [FromQuery] Guid? adjacentTo, - [FromQuery] int? parentIndexNumber, - [FromQuery] bool? hasParentalRating, - [FromQuery] bool? isHd, - [FromQuery] bool? is4K, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, - [FromQuery] bool? isMissing, - [FromQuery] bool? isUnaired, - [FromQuery] double? minCommunityRating, - [FromQuery] double? minCriticRating, - [FromQuery] DateTime? minPremiereDate, - [FromQuery] DateTime? minDateLastSaved, - [FromQuery] DateTime? minDateLastSavedForUser, - [FromQuery] DateTime? maxPremiereDate, - [FromQuery] bool? hasOverview, - [FromQuery] bool? hasImdbId, - [FromQuery] bool? hasTmdbId, - [FromQuery] bool? hasTvdbId, - [FromQuery] bool? isMovie, - [FromQuery] bool? isSeries, - [FromQuery] bool? isNews, - [FromQuery] bool? isKids, - [FromQuery] bool? isSports, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] bool? recursive, - [FromQuery] string? searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, - [FromQuery] bool? isPlayed, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, - [FromQuery] bool? enableUserData, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? person, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, - [FromQuery] string? minOfficialRating, - [FromQuery] bool? isLocked, - [FromQuery] bool? isPlaceHolder, - [FromQuery] bool? hasOfficialRating, - [FromQuery] bool? collapseBoxSetItems, - [FromQuery] int? minWidth, - [FromQuery] int? minHeight, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] bool? is3D, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus, - [FromQuery] string? nameStartsWithOrGreater, - [FromQuery] string? nameStartsWith, - [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, - [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool? enableImages = true) + var item = _libraryManager.GetParentItem(parentId, userId); + QueryResult result; + + if (item is not Folder folder) { - return GetItems( - userId, - maxOfficialRating, - hasThemeSong, - hasThemeVideo, - hasSubtitles, - hasSpecialFeature, - hasTrailer, - adjacentTo, - parentIndexNumber, - hasParentalRating, - isHd, - is4K, - locationTypes, - excludeLocationTypes, - isMissing, - isUnaired, - minCommunityRating, - minCriticRating, - minPremiereDate, - minDateLastSaved, - minDateLastSavedForUser, - maxPremiereDate, - hasOverview, - hasImdbId, - hasTmdbId, - hasTvdbId, - isMovie, - isSeries, - isNews, - isKids, - isSports, - excludeItemIds, - startIndex, - limit, - recursive, - searchTerm, - sortOrder, - parentId, - fields, - excludeItemTypes, - includeItemTypes, - filters, - isFavorite, - mediaTypes, - imageTypes, - sortBy, - isPlayed, - genres, - officialRatings, - tags, - years, - enableUserData, - imageTypeLimit, - enableImageTypes, - person, - personIds, - personTypes, - studios, - artists, - excludeArtistIds, - artistIds, - albumArtistIds, - contributingArtistIds, - albums, - albumIds, - ids, - videoTypes, - minOfficialRating, - isLocked, - isPlaceHolder, - hasOfficialRating, - collapseBoxSetItems, - minWidth, - minHeight, - maxWidth, - maxHeight, - is3D, - seriesStatus, - nameStartsWithOrGreater, - nameStartsWith, - nameLessThan, - studioIds, - genreIds, - enableTotalRecordCount, - enableImages); + folder = _libraryManager.GetUserRootFolder(); } - /// - /// Gets items based on a query. - /// - /// The user id. - /// The start index. - /// The item limit. - /// The search term. - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. - /// Optional. Filter by MediaType. Allows multiple, comma delimited. - /// Optional. Include user data. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited. - /// Optional. Enable the total record count. - /// Optional. Include image information in output. - /// Optional. Whether to exclude the currently active sessions. - /// Items returned. - /// A with the items that are resumable. - [HttpGet("Users/{userId}/Items/Resume")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetResumeItems( - [FromRoute, Required] Guid userId, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] string? searchTerm, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery] bool? enableUserData, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool? enableImages = true, - [FromQuery] bool excludeActiveSessions = false) + string? collectionType = null; + if (folder is IHasCollectionType hasCollectionType) { - var user = _userManager.GetUserById(userId); - var parentIdGuid = parentId ?? Guid.Empty; - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + collectionType = hasCollectionType.CollectionType; + } - var ancestorIds = Array.Empty(); + if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) + { + recursive = true; + includeItemTypes = new[] { BaseItemKind.Playlist }; + } - var excludeFolderIds = user.GetPreferenceValues(PreferenceKind.LatestItemExcludes); - if (parentIdGuid.Equals(default) && excludeFolderIds.Length > 0) + if (item is not UserRootFolder + // api keys can always access all folders + && !isApiKey + // check the item is visible for the user + && !item.IsVisible(user)) + { + _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user!.Username, item.Name); + return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); + } + + if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder) + { + var query = new InternalItemsQuery(user) { - ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true) - .Where(i => i is Folder) - .Where(i => !excludeFolderIds.Contains(i.Id)) - .Select(i => i.Id) - .ToArray(); - } - - var excludeItemIds = Array.Empty(); - if (excludeActiveSessions) - { - excludeItemIds = _sessionManager.Sessions - .Where(s => s.UserId.Equals(userId) && s.NowPlayingItem is not null) - .Select(s => s.NowPlayingItem.Id) - .ToArray(); - } - - var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user) - { - OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) }, - IsResumable = true, - StartIndex = startIndex, - Limit = limit, - ParentId = parentIdGuid, - Recursive = true, - DtoOptions = dtoOptions, + IsPlayed = isPlayed, MediaTypes = mediaTypes, - IsVirtualItem = false, - CollapseBoxSetItems = false, - EnableTotalRecordCount = enableTotalRecordCount, - AncestorIds = ancestorIds, IncludeItemTypes = includeItemTypes, ExcludeItemTypes = excludeItemTypes, + Recursive = recursive ?? false, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), + IsFavorite = isFavorite, + Limit = limit, + StartIndex = startIndex, + IsMissing = isMissing, + IsUnaired = isUnaired, + CollapseBoxSetItems = collapseBoxSetItems, + NameLessThan = nameLessThan, + NameStartsWith = nameStartsWith, + NameStartsWithOrGreater = nameStartsWithOrGreater, + HasImdbId = hasImdbId, + IsPlaceHolder = isPlaceHolder, + IsLocked = isLocked, + MinWidth = minWidth, + MinHeight = minHeight, + MaxWidth = maxWidth, + MaxHeight = maxHeight, + Is3D = is3D, + HasTvdbId = hasTvdbId, + HasTmdbId = hasTmdbId, + IsMovie = isMovie, + IsSeries = isSeries, + IsNews = isNews, + IsKids = isKids, + IsSports = isSports, + HasOverview = hasOverview, + HasOfficialRating = hasOfficialRating, + HasParentalRating = hasParentalRating, + HasSpecialFeature = hasSpecialFeature, + HasSubtitles = hasSubtitles, + HasThemeSong = hasThemeSong, + HasThemeVideo = hasThemeVideo, + HasTrailer = hasTrailer, + IsHD = isHd, + Is4K = is4K, + Tags = tags, + OfficialRatings = officialRatings, + Genres = genres, + ArtistIds = artistIds, + AlbumArtistIds = albumArtistIds, + ContributingArtistIds = contributingArtistIds, + GenreIds = genreIds, + StudioIds = studioIds, + Person = person, + PersonIds = personIds, + PersonTypes = personTypes, + Years = years, + ImageTypes = imageTypes, + VideoTypes = videoTypes, + AdjacentTo = adjacentTo, + ItemIds = ids, + MinCommunityRating = minCommunityRating, + MinCriticRating = minCriticRating, + ParentId = parentId ?? Guid.Empty, + ParentIndexNumber = parentIndexNumber, + EnableTotalRecordCount = enableTotalRecordCount, + ExcludeItemIds = excludeItemIds, + DtoOptions = dtoOptions, SearchTerm = searchTerm, - ExcludeItemIds = excludeItemIds - }); + MinDateLastSaved = minDateLastSaved?.ToUniversalTime(), + MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(), + MinPremiereDate = minPremiereDate?.ToUniversalTime(), + MaxPremiereDate = maxPremiereDate?.ToUniversalTime(), + }; - var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user); + if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm)) + { + query.CollapseBoxSetItems = false; + } - return new QueryResult( - startIndex, - itemsResult.TotalRecordCount, - returnItems); + foreach (var filter in filters) + { + switch (filter) + { + case ItemFilter.Dislikes: + query.IsLiked = false; + break; + case ItemFilter.IsFavorite: + query.IsFavorite = true; + break; + case ItemFilter.IsFavoriteOrLikes: + query.IsFavoriteOrLiked = true; + break; + case ItemFilter.IsFolder: + query.IsFolder = true; + break; + case ItemFilter.IsNotFolder: + query.IsFolder = false; + break; + case ItemFilter.IsPlayed: + query.IsPlayed = true; + break; + case ItemFilter.IsResumable: + query.IsResumable = true; + break; + case ItemFilter.IsUnplayed: + query.IsPlayed = false; + break; + case ItemFilter.Likes: + query.IsLiked = true; + break; + } + } + + // Filter by Series Status + if (seriesStatus.Length != 0) + { + query.SeriesStatuses = seriesStatus; + } + + // ExcludeLocationTypes + if (excludeLocationTypes.Any(t => t == LocationType.Virtual)) + { + query.IsVirtualItem = false; + } + + if (locationTypes.Length > 0 && locationTypes.Length < 4) + { + query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual); + } + + // Min official rating + if (!string.IsNullOrWhiteSpace(minOfficialRating)) + { + query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating); + } + + // Max official rating + if (!string.IsNullOrWhiteSpace(maxOfficialRating)) + { + query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating); + } + + // Artists + if (artists.Length != 0) + { + query.ArtistIds = artists.Select(i => + { + try + { + return _libraryManager.GetArtist(i, new DtoOptions(false)); + } + catch + { + return null; + } + }).Where(i => i is not null).Select(i => i!.Id).ToArray(); + } + + // ExcludeArtistIds + if (excludeArtistIds.Length != 0) + { + query.ExcludeArtistIds = excludeArtistIds; + } + + if (albumIds.Length != 0) + { + query.AlbumIds = albumIds; + } + + // Albums + if (albums.Length != 0) + { + query.AlbumIds = albums.SelectMany(i => + { + return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = i, Limit = 1 }); + }).ToArray(); + } + + // Studios + if (studios.Length != 0) + { + query.StudioIds = studios.Select(i => + { + try + { + return _libraryManager.GetStudio(i); + } + catch + { + return null; + } + }).Where(i => i is not null).Select(i => i!.Id).ToArray(); + } + + // Apply default sorting if none requested + if (query.OrderBy.Count == 0) + { + // Albums by artist + if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum) + { + query.OrderBy = new[] { (ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Ascending) }; + } + } + + result = folder.GetItems(query); } + else + { + var itemsArray = folder.GetChildren(user, true); + result = new QueryResult(itemsArray); + } + + return new QueryResult( + startIndex, + result.TotalRecordCount, + _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user)); + } + + /// + /// Gets items based on a query. + /// + /// The user id supplied as query parameter. + /// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc). + /// Optional filter by items with theme songs. + /// Optional filter by items with theme videos. + /// Optional filter by items with subtitles. + /// Optional filter by items with special features. + /// Optional filter by items with trailers. + /// Optional. Return items that are siblings of a supplied item. + /// Optional filter by parent index number. + /// Optional filter by items that have or do not have a parental rating. + /// Optional filter by items that are HD or not. + /// Optional filter by items that are 4K or not. + /// Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited. + /// Optional filter by items that are missing episodes or not. + /// Optional filter by items that are unaired episodes or not. + /// Optional filter by minimum community rating. + /// Optional filter by minimum critic rating. + /// Optional. The minimum premiere date. Format = ISO. + /// Optional. The minimum last saved date. Format = ISO. + /// Optional. The minimum last saved date for the current user. Format = ISO. + /// Optional. The maximum premiere date. Format = ISO. + /// Optional filter by items that have an overview or not. + /// Optional filter by items that have an IMDb id or not. + /// Optional filter by items that have a TMDb id or not. + /// Optional filter by items that have a TVDb id or not. + /// Optional filter for live tv movies. + /// Optional filter for live tv series. + /// Optional filter for live tv news. + /// Optional filter for live tv kids. + /// Optional filter for live tv sports. + /// Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// When searching within folders, this determines whether or not the search will be recursive. true/false. + /// Optional. Filter based on a search term. + /// Sort Order - Ascending, Descending. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional filter by items that are marked as favorite, or not. + /// Optional filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited. + /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. + /// Optional filter by items that are played, or not. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person id. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered to include only those containing the specified artist id. + /// Optional. If specified, results will be filtered to include only those containing the specified album artist id. + /// Optional. If specified, results will be filtered to include only those containing the specified contributing artist id. + /// Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited. + /// Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited. + /// Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited. + /// Optional filter by minimum official rating (PG, PG-13, TV-MA, etc). + /// Optional filter by items that are locked. + /// Optional filter by items that are placeholders. + /// Optional filter by items that have official ratings. + /// Whether or not to hide items behind their boxsets. + /// Optional. Filter by the minimum width of the item. + /// Optional. Filter by the minimum height of the item. + /// Optional. Filter by the maximum width of the item. + /// Optional. Filter by the maximum height of the item. + /// Optional filter by items that are 3D, or not. + /// Optional filter by Series Status. Allows multiple, comma delimited. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. + /// Optional. Enable the total record count. + /// Optional, include image information in output. + /// A with the items. + [HttpGet("Users/{userId}/Items")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetItemsByUserId( + [FromRoute] Guid userId, + [FromQuery] string? maxOfficialRating, + [FromQuery] bool? hasThemeSong, + [FromQuery] bool? hasThemeVideo, + [FromQuery] bool? hasSubtitles, + [FromQuery] bool? hasSpecialFeature, + [FromQuery] bool? hasTrailer, + [FromQuery] Guid? adjacentTo, + [FromQuery] int? parentIndexNumber, + [FromQuery] bool? hasParentalRating, + [FromQuery] bool? isHd, + [FromQuery] bool? is4K, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, + [FromQuery] bool? isMissing, + [FromQuery] bool? isUnaired, + [FromQuery] double? minCommunityRating, + [FromQuery] double? minCriticRating, + [FromQuery] DateTime? minPremiereDate, + [FromQuery] DateTime? minDateLastSaved, + [FromQuery] DateTime? minDateLastSavedForUser, + [FromQuery] DateTime? maxPremiereDate, + [FromQuery] bool? hasOverview, + [FromQuery] bool? hasImdbId, + [FromQuery] bool? hasTmdbId, + [FromQuery] bool? hasTvdbId, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] bool? recursive, + [FromQuery] string? searchTerm, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery] bool? isFavorite, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery] bool? isPlayed, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] string? person, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, + [FromQuery] string? minOfficialRating, + [FromQuery] bool? isLocked, + [FromQuery] bool? isPlaceHolder, + [FromQuery] bool? hasOfficialRating, + [FromQuery] bool? collapseBoxSetItems, + [FromQuery] int? minWidth, + [FromQuery] int? minHeight, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] bool? is3D, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus, + [FromQuery] string? nameStartsWithOrGreater, + [FromQuery] string? nameStartsWith, + [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery] bool enableTotalRecordCount = true, + [FromQuery] bool? enableImages = true) + { + return GetItems( + userId, + maxOfficialRating, + hasThemeSong, + hasThemeVideo, + hasSubtitles, + hasSpecialFeature, + hasTrailer, + adjacentTo, + parentIndexNumber, + hasParentalRating, + isHd, + is4K, + locationTypes, + excludeLocationTypes, + isMissing, + isUnaired, + minCommunityRating, + minCriticRating, + minPremiereDate, + minDateLastSaved, + minDateLastSavedForUser, + maxPremiereDate, + hasOverview, + hasImdbId, + hasTmdbId, + hasTvdbId, + isMovie, + isSeries, + isNews, + isKids, + isSports, + excludeItemIds, + startIndex, + limit, + recursive, + searchTerm, + sortOrder, + parentId, + fields, + excludeItemTypes, + includeItemTypes, + filters, + isFavorite, + mediaTypes, + imageTypes, + sortBy, + isPlayed, + genres, + officialRatings, + tags, + years, + enableUserData, + imageTypeLimit, + enableImageTypes, + person, + personIds, + personTypes, + studios, + artists, + excludeArtistIds, + artistIds, + albumArtistIds, + contributingArtistIds, + albums, + albumIds, + ids, + videoTypes, + minOfficialRating, + isLocked, + isPlaceHolder, + hasOfficialRating, + collapseBoxSetItems, + minWidth, + minHeight, + maxWidth, + maxHeight, + is3D, + seriesStatus, + nameStartsWithOrGreater, + nameStartsWith, + nameLessThan, + studioIds, + genreIds, + enableTotalRecordCount, + enableImages); + } + + /// + /// Gets items based on a query. + /// + /// The user id. + /// The start index. + /// The item limit. + /// The search term. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. Filter by MediaType. Allows multiple, comma delimited. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited. + /// Optional. Enable the total record count. + /// Optional. Include image information in output. + /// Optional. Whether to exclude the currently active sessions. + /// Items returned. + /// A with the items that are resumable. + [HttpGet("Users/{userId}/Items/Resume")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetResumeItems( + [FromRoute, Required] Guid userId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string? searchTerm, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery] bool enableTotalRecordCount = true, + [FromQuery] bool? enableImages = true, + [FromQuery] bool excludeActiveSessions = false) + { + var user = _userManager.GetUserById(userId); + var parentIdGuid = parentId ?? Guid.Empty; + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + var ancestorIds = Array.Empty(); + + var excludeFolderIds = user.GetPreferenceValues(PreferenceKind.LatestItemExcludes); + if (parentIdGuid.Equals(default) && excludeFolderIds.Length > 0) + { + ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true) + .Where(i => i is Folder) + .Where(i => !excludeFolderIds.Contains(i.Id)) + .Select(i => i.Id) + .ToArray(); + } + + var excludeItemIds = Array.Empty(); + if (excludeActiveSessions) + { + excludeItemIds = _sessionManager.Sessions + .Where(s => s.UserId.Equals(userId) && s.NowPlayingItem is not null) + .Select(s => s.NowPlayingItem.Id) + .ToArray(); + } + + var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user) + { + OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) }, + IsResumable = true, + StartIndex = startIndex, + Limit = limit, + ParentId = parentIdGuid, + Recursive = true, + DtoOptions = dtoOptions, + MediaTypes = mediaTypes, + IsVirtualItem = false, + CollapseBoxSetItems = false, + EnableTotalRecordCount = enableTotalRecordCount, + AncestorIds = ancestorIds, + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, + SearchTerm = searchTerm, + ExcludeItemIds = excludeItemIds + }); + + var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user); + + return new QueryResult( + startIndex, + itemsResult.TotalRecordCount, + returnItems); } } diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 196d509fbc..916aaf6bae 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -37,936 +37,935 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Library Controller. +/// +[Route("")] +public class LibraryController : BaseJellyfinApiController { + private readonly IProviderManager _providerManager; + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; + private readonly IActivityManager _activityManager; + private readonly ILocalizationManager _localization; + private readonly ILibraryMonitor _libraryMonitor; + private readonly ILogger _logger; + private readonly IServerConfigurationManager _serverConfigurationManager; + /// - /// Library Controller. + /// Initializes a new instance of the class. /// - [Route("")] - public class LibraryController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public LibraryController( + IProviderManager providerManager, + ILibraryManager libraryManager, + IUserManager userManager, + IDtoService dtoService, + IActivityManager activityManager, + ILocalizationManager localization, + ILibraryMonitor libraryMonitor, + ILogger logger, + IServerConfigurationManager serverConfigurationManager) { - private readonly IProviderManager _providerManager; - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDtoService _dtoService; - private readonly IActivityManager _activityManager; - private readonly ILocalizationManager _localization; - private readonly ILibraryMonitor _libraryMonitor; - private readonly ILogger _logger; - private readonly IServerConfigurationManager _serverConfigurationManager; + _providerManager = providerManager; + _libraryManager = libraryManager; + _userManager = userManager; + _dtoService = dtoService; + _activityManager = activityManager; + _localization = localization; + _libraryMonitor = libraryMonitor; + _logger = logger; + _serverConfigurationManager = serverConfigurationManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public LibraryController( - IProviderManager providerManager, - ILibraryManager libraryManager, - IUserManager userManager, - IDtoService dtoService, - IActivityManager activityManager, - ILocalizationManager localization, - ILibraryMonitor libraryMonitor, - ILogger logger, - IServerConfigurationManager serverConfigurationManager) + /// + /// Get the original file of an item. + /// + /// The item id. + /// File stream returned. + /// Item not found. + /// A with the original file. + [HttpGet("Items/{itemId}/File")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesFile("video/*", "audio/*")] + public ActionResult GetFile([FromRoute, Required] Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) { - _providerManager = providerManager; - _libraryManager = libraryManager; - _userManager = userManager; - _dtoService = dtoService; - _activityManager = activityManager; - _localization = localization; - _libraryMonitor = libraryMonitor; - _logger = logger; - _serverConfigurationManager = serverConfigurationManager; + return NotFound(); } - /// - /// Get the original file of an item. - /// - /// The item id. - /// File stream returned. - /// Item not found. - /// A with the original file. - [HttpGet("Items/{itemId}/File")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesFile("video/*", "audio/*")] - public ActionResult GetFile([FromRoute, Required] Guid itemId) - { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } + return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true); + } - return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true); + /// + /// Gets critic review for an item. + /// + /// Critic reviews returned. + /// The list of critic reviews. + [HttpGet("Items/{itemId}/CriticReviews")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [Obsolete("This endpoint is obsolete.")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetCriticReviews() + { + return new QueryResult(); + } + + /// + /// Get theme songs for an item. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. Determines whether or not parent items should be searched for theme media. + /// Theme songs returned. + /// Item not found. + /// The item theme songs. + [HttpGet("Items/{itemId}/ThemeSongs")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetThemeSongs( + [FromRoute, Required] Guid itemId, + [FromQuery] Guid? userId, + [FromQuery] bool inheritFromParent = false) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) + : _libraryManager.GetItemById(itemId); + + if (item is null) + { + return NotFound("Item not found."); } - /// - /// Gets critic review for an item. - /// - /// Critic reviews returned. - /// The list of critic reviews. - [HttpGet("Items/{itemId}/CriticReviews")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [Obsolete("This endpoint is obsolete.")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetCriticReviews() + IEnumerable themeItems; + + while (true) { - return new QueryResult(); + themeItems = item.GetThemeSongs(); + + if (themeItems.Any() || !inheritFromParent) + { + break; + } + + var parent = item.GetParent(); + if (parent is null) + { + break; + } + + item = parent; } - /// - /// Get theme songs for an item. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Optional. Determines whether or not parent items should be searched for theme media. - /// Theme songs returned. - /// Item not found. - /// The item theme songs. - [HttpGet("Items/{itemId}/ThemeSongs")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetThemeSongs( - [FromRoute, Required] Guid itemId, - [FromQuery] Guid? userId, - [FromQuery] bool inheritFromParent = false) + var dtoOptions = new DtoOptions().AddClientFields(User); + var items = themeItems + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) + .ToArray(); + + return new ThemeMediaResult { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); + Items = items, + TotalRecordCount = items.Length, + OwnerId = item.Id + }; + } - var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) - ? _libraryManager.RootFolder - : _libraryManager.GetUserRootFolder()) - : _libraryManager.GetItemById(itemId); + /// + /// Get theme videos for an item. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. Determines whether or not parent items should be searched for theme media. + /// Theme videos returned. + /// Item not found. + /// The item theme videos. + [HttpGet("Items/{itemId}/ThemeVideos")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetThemeVideos( + [FromRoute, Required] Guid itemId, + [FromQuery] Guid? userId, + [FromQuery] bool inheritFromParent = false) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); - if (item is null) - { - return NotFound("Item not found."); - } + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) + : _libraryManager.GetItemById(itemId); - IEnumerable themeItems; - - while (true) - { - themeItems = item.GetThemeSongs(); - - if (themeItems.Any() || !inheritFromParent) - { - break; - } - - var parent = item.GetParent(); - if (parent is null) - { - break; - } - - item = parent; - } - - var dtoOptions = new DtoOptions().AddClientFields(User); - var items = themeItems - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) - .ToArray(); - - return new ThemeMediaResult - { - Items = items, - TotalRecordCount = items.Length, - OwnerId = item.Id - }; + if (item is null) + { + return NotFound("Item not found."); } - /// - /// Get theme videos for an item. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Optional. Determines whether or not parent items should be searched for theme media. - /// Theme videos returned. - /// Item not found. - /// The item theme videos. - [HttpGet("Items/{itemId}/ThemeVideos")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetThemeVideos( - [FromRoute, Required] Guid itemId, - [FromQuery] Guid? userId, - [FromQuery] bool inheritFromParent = false) + IEnumerable themeItems; + + while (true) { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); + themeItems = item.GetThemeVideos(); - var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) - ? _libraryManager.RootFolder - : _libraryManager.GetUserRootFolder()) - : _libraryManager.GetItemById(itemId); - - if (item is null) + if (themeItems.Any() || !inheritFromParent) { - return NotFound("Item not found."); + break; } - IEnumerable themeItems; - - while (true) + var parent = item.GetParent(); + if (parent is null) { - themeItems = item.GetThemeVideos(); - - if (themeItems.Any() || !inheritFromParent) - { - break; - } - - var parent = item.GetParent(); - if (parent is null) - { - break; - } - - item = parent; + break; } - var dtoOptions = new DtoOptions().AddClientFields(User); - var items = themeItems - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) - .ToArray(); - - return new ThemeMediaResult - { - Items = items, - TotalRecordCount = items.Length, - OwnerId = item.Id - }; + item = parent; } - /// - /// Get theme songs and videos for an item. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Optional. Determines whether or not parent items should be searched for theme media. - /// Theme songs and videos returned. - /// Item not found. - /// The item theme videos. - [HttpGet("Items/{itemId}/ThemeMedia")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetThemeMedia( - [FromRoute, Required] Guid itemId, - [FromQuery] Guid? userId, - [FromQuery] bool inheritFromParent = false) + var dtoOptions = new DtoOptions().AddClientFields(User); + var items = themeItems + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) + .ToArray(); + + return new ThemeMediaResult { - var themeSongs = GetThemeSongs( - itemId, - userId, - inheritFromParent); + Items = items, + TotalRecordCount = items.Length, + OwnerId = item.Id + }; + } - var themeVideos = GetThemeVideos( - itemId, - userId, - inheritFromParent); + /// + /// Get theme songs and videos for an item. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Optional. Determines whether or not parent items should be searched for theme media. + /// Theme songs and videos returned. + /// Item not found. + /// The item theme videos. + [HttpGet("Items/{itemId}/ThemeMedia")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetThemeMedia( + [FromRoute, Required] Guid itemId, + [FromQuery] Guid? userId, + [FromQuery] bool inheritFromParent = false) + { + var themeSongs = GetThemeSongs( + itemId, + userId, + inheritFromParent); - return new AllThemeMediaResult - { - ThemeSongsResult = themeSongs?.Value, - ThemeVideosResult = themeVideos?.Value, - SoundtrackSongsResult = new ThemeMediaResult() - }; + var themeVideos = GetThemeVideos( + itemId, + userId, + inheritFromParent); + + return new AllThemeMediaResult + { + ThemeSongsResult = themeSongs?.Value, + ThemeVideosResult = themeVideos?.Value, + SoundtrackSongsResult = new ThemeMediaResult() + }; + } + + /// + /// Starts a library scan. + /// + /// Library scan started. + /// A . + [HttpPost("Library/Refresh")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task RefreshLibrary() + { + try + { + await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing library"); } - /// - /// Starts a library scan. - /// - /// Library scan started. - /// A . - [HttpPost("Library/Refresh")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task RefreshLibrary() - { - try - { - await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing library"); - } + return NoContent(); + } + /// + /// Deletes an item from the library and filesystem. + /// + /// The item id. + /// Item deleted. + /// Unauthorized access. + /// A . + [HttpDelete("Items/{itemId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public ActionResult DeleteItem(Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + var user = _userManager.GetUserById(User.GetUserId()); + + if (!item.CanDelete(user)) + { + return Unauthorized("Unauthorized access"); + } + + _libraryManager.DeleteItem( + item, + new DeleteOptions { DeleteFileLocation = true }, + true); + + return NoContent(); + } + + /// + /// Deletes items from the library and filesystem. + /// + /// The item ids. + /// Items deleted. + /// Unauthorized access. + /// A . + [HttpDelete("Items")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + { + if (ids.Length == 0) + { return NoContent(); } - /// - /// Deletes an item from the library and filesystem. - /// - /// The item id. - /// Item deleted. - /// Unauthorized access. - /// A . - [HttpDelete("Items/{itemId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public ActionResult DeleteItem(Guid itemId) + foreach (var i in ids) { - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(i); var user = _userManager.GetUserById(User.GetUserId()); if (!item.CanDelete(user)) { - return Unauthorized("Unauthorized access"); + if (ids.Length > 1) + { + return Unauthorized("Unauthorized access"); + } + + continue; } _libraryManager.DeleteItem( item, new DeleteOptions { DeleteFileLocation = true }, true); - - return NoContent(); } - /// - /// Deletes items from the library and filesystem. - /// - /// The item ids. - /// Items deleted. - /// Unauthorized access. - /// A . - [HttpDelete("Items")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + return NoContent(); + } + + /// + /// Get item counts. + /// + /// Optional. Get counts from a specific user's library. + /// Optional. Get counts of favorite items. + /// Item counts returned. + /// Item counts. + [HttpGet("Items/Counts")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetItemCounts( + [FromQuery] Guid? userId, + [FromQuery] bool? isFavorite) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var counts = new ItemCounts { - if (ids.Length == 0) + AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite), + EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite), + MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite), + SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite), + SongCount = GetCount(BaseItemKind.Audio, user, isFavorite), + MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite), + BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite), + BookCount = GetCount(BaseItemKind.Book, user, isFavorite) + }; + + return counts; + } + + /// + /// Gets all parents of an item. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Item parents returned. + /// Item not found. + /// Item parents. + [HttpGet("Items/{itemId}/Ancestors")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) + { + var item = _libraryManager.GetItemById(itemId); + + if (item is null) + { + return NotFound("Item not found"); + } + + var baseItemDtos = new List(); + + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var dtoOptions = new DtoOptions().AddClientFields(User); + BaseItem? parent = item.GetParent(); + + while (parent is not null) + { + if (user is not null) { - return NoContent(); + parent = TranslateParentItem(parent, user); } - foreach (var i in ids) - { - var item = _libraryManager.GetItemById(i); - var user = _userManager.GetUserById(User.GetUserId()); + baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); - if (!item.CanDelete(user)) + parent = parent?.GetParent(); + } + + return baseItemDtos; + } + + /// + /// Gets a list of physical paths from virtual folders. + /// + /// Physical paths returned. + /// List of physical paths. + [HttpGet("Library/PhysicalPaths")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetPhysicalPaths() + { + return Ok(_libraryManager.RootFolder.Children + .SelectMany(c => c.PhysicalLocations)); + } + + /// + /// Gets all user media folders. + /// + /// Optional. Filter by folders that are marked hidden, or not. + /// Media folders returned. + /// List of user media folders. + [HttpGet("Library/MediaFolders")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetMediaFolders([FromQuery] bool? isHidden) + { + var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList(); + + if (isHidden.HasValue) + { + var val = isHidden.Value; + + items = items.Where(i => i.IsHidden == val).ToList(); + } + + var dtoOptions = new DtoOptions().AddClientFields(User); + var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions); + return new QueryResult(resultArray); + } + + /// + /// Reports that new episodes of a series have been added by an external source. + /// + /// The tvdbId. + /// Report success. + /// A . + [HttpPost("Library/Series/Added", Name = "PostAddedSeries")] + [HttpPost("Library/Series/Updated")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId) + { + var series = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { BaseItemKind.Series }, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + }).Where(i => string.Equals(tvdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray(); + + foreach (var item in series) + { + _libraryMonitor.ReportFileSystemChanged(item.Path); + } + + return NoContent(); + } + + /// + /// Reports that new movies have been added by an external source. + /// + /// The tmdbId. + /// The imdbId. + /// Report success. + /// A . + [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")] + [HttpPost("Library/Movies/Updated")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId) + { + var movies = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { BaseItemKind.Movie }, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + }); + + if (!string.IsNullOrWhiteSpace(imdbId)) + { + movies = movies.Where(i => string.Equals(imdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList(); + } + else if (!string.IsNullOrWhiteSpace(tmdbId)) + { + movies = movies.Where(i => string.Equals(tmdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList(); + } + else + { + movies = new List(); + } + + foreach (var item in movies) + { + _libraryMonitor.ReportFileSystemChanged(item.Path); + } + + return NoContent(); + } + + /// + /// Reports that new movies have been added by an external source. + /// + /// The update paths. + /// Report success. + /// A . + [HttpPost("Library/Media/Updated")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto) + { + foreach (var item in dto.Updates) + { + _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null.")); + } + + return NoContent(); + } + + /// + /// Downloads item media. + /// + /// The item id. + /// Media downloaded. + /// Item not found. + /// A containing the media stream. + /// User can't download or item can't be downloaded. + [HttpGet("Items/{itemId}/Download")] + [Authorize(Policy = Policies.Download)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesFile("video/*", "audio/*")] + public async Task GetDownload([FromRoute, Required] Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + var user = _userManager.GetUserById(User.GetUserId()); + + if (user is not null) + { + if (!item.CanDownload(user)) + { + throw new ArgumentException("Item does not support downloading"); + } + } + else + { + if (!item.CanDownload()) + { + throw new ArgumentException("Item does not support downloading"); + } + } + + if (user is not null) + { + await LogDownloadAsync(item, user).ConfigureAwait(false); + } + + // Quotes are valid in linux. They'll possibly cause issues here. + var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal); + + return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true); + } + + /// + /// Gets similar items. + /// + /// The item id. + /// Exclude artist ids. + /// Optional. Filter by user id, and attach user data. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. + /// Similar items returned. + /// A containing the similar items. + [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")] + [HttpGet("Items/{itemId}/Similar")] + [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")] + [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")] + [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")] + [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetSimilarItems( + [FromRoute, Required] Guid itemId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, + [FromQuery] Guid? userId, + [FromQuery] int? limit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) + { + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) + : _libraryManager.GetItemById(itemId); + + if (item is Episode || (item is IItemByName && item is not MusicArtist)) + { + return new QueryResult(); + } + + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User); + + var program = item as IHasProgramAttributes; + bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer; + bool? isSeries = item is Series || (program is not null && program.IsSeries); + + var includeItemTypes = new List(); + if (isMovie.Value) + { + includeItemTypes.Add(BaseItemKind.Movie); + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + includeItemTypes.Add(BaseItemKind.Trailer); + includeItemTypes.Add(BaseItemKind.LiveTvProgram); + } + } + else if (isSeries.Value) + { + includeItemTypes.Add(BaseItemKind.Series); + } + else + { + // For non series and movie types these columns are typically null + // isSeries = null; + isMovie = null; + includeItemTypes.Add(item.GetBaseItemKind()); + } + + var query = new InternalItemsQuery(user) + { + Genres = item.Genres, + Limit = limit, + IncludeItemTypes = includeItemTypes.ToArray(), + SimilarTo = item, + DtoOptions = dtoOptions, + EnableTotalRecordCount = !isMovie ?? true, + EnableGroupByMetadataKey = isMovie ?? false, + MinSimilarityScore = 2 // A remnant from album/artist scoring + }; + + // ExcludeArtistIds + if (excludeArtistIds.Length != 0) + { + query.ExcludeArtistIds = excludeArtistIds; + } + + List itemsResult = _libraryManager.GetItemList(query); + + var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user); + + return new QueryResult( + query.StartIndex, + itemsResult.Count, + returnList); + } + + /// + /// Gets the library options info. + /// + /// Library content type. + /// Whether this is a new library. + /// Library options info returned. + /// Library options info. + [HttpGet("Libraries/AvailableOptions")] + [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetLibraryOptionsInfo( + [FromQuery] string? libraryContentType, + [FromQuery] bool isNewLibrary = false) + { + var result = new LibraryOptionsResultDto(); + + var types = GetRepresentativeItemTypes(libraryContentType); + var typesList = types.ToList(); + + var plugins = _providerManager.GetAllMetadataPlugins() + .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase)) + .OrderBy(i => typesList.IndexOf(i.ItemType)) + .ToList(); + + result.MetadataSavers = plugins + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary) + }) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + result.MetadataReaders = plugins + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = true + }) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + result.SubtitleFetchers = plugins + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher)) + .Select(i => new LibraryOptionInfoDto + { + Name = i.Name, + DefaultEnabled = true + }) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + var typeOptions = new List(); + + foreach (var type in types) + { + TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions); + + typeOptions.Add(new LibraryTypeOptionsDto + { + Type = type, + + MetadataFetchers = plugins + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher)) + .Select(i => new LibraryOptionInfoDto { - if (ids.Length > 1) - { - return Unauthorized("Unauthorized access"); - } + Name = i.Name, + DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) + }) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(), - continue; - } - - _libraryManager.DeleteItem( - item, - new DeleteOptions { DeleteFileLocation = true }, - true); - } - - return NoContent(); - } - - /// - /// Get item counts. - /// - /// Optional. Get counts from a specific user's library. - /// Optional. Get counts of favorite items. - /// Item counts returned. - /// Item counts. - [HttpGet("Items/Counts")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetItemCounts( - [FromQuery] Guid? userId, - [FromQuery] bool? isFavorite) - { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var counts = new ItemCounts - { - AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite), - EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite), - MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite), - SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite), - SongCount = GetCount(BaseItemKind.Audio, user, isFavorite), - MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite), - BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite), - BookCount = GetCount(BaseItemKind.Book, user, isFavorite) - }; - - return counts; - } - - /// - /// Gets all parents of an item. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Item parents returned. - /// Item not found. - /// Item parents. - [HttpGet("Items/{itemId}/Ancestors")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) - { - var item = _libraryManager.GetItemById(itemId); - - if (item is null) - { - return NotFound("Item not found"); - } - - var baseItemDtos = new List(); - - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var dtoOptions = new DtoOptions().AddClientFields(User); - BaseItem? parent = item.GetParent(); - - while (parent is not null) - { - if (user is not null) + ImageFetchers = plugins + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher)) + .Select(i => new LibraryOptionInfoDto { - parent = TranslateParentItem(parent, user); - } + Name = i.Name, + DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) + }) + .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(), - baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); + SupportedImageTypes = plugins + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .SelectMany(i => i.SupportedImageTypes ?? Array.Empty()) + .Distinct() + .ToArray(), - parent = parent?.GetParent(); - } - - return baseItemDtos; - } - - /// - /// Gets a list of physical paths from virtual folders. - /// - /// Physical paths returned. - /// List of physical paths. - [HttpGet("Library/PhysicalPaths")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetPhysicalPaths() - { - return Ok(_libraryManager.RootFolder.Children - .SelectMany(c => c.PhysicalLocations)); - } - - /// - /// Gets all user media folders. - /// - /// Optional. Filter by folders that are marked hidden, or not. - /// Media folders returned. - /// List of user media folders. - [HttpGet("Library/MediaFolders")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetMediaFolders([FromQuery] bool? isHidden) - { - var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList(); - - if (isHidden.HasValue) - { - var val = isHidden.Value; - - items = items.Where(i => i.IsHidden == val).ToList(); - } - - var dtoOptions = new DtoOptions().AddClientFields(User); - var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions); - return new QueryResult(resultArray); - } - - /// - /// Reports that new episodes of a series have been added by an external source. - /// - /// The tvdbId. - /// Report success. - /// A . - [HttpPost("Library/Series/Added", Name = "PostAddedSeries")] - [HttpPost("Library/Series/Updated")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId) - { - var series = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.Series }, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - } - }).Where(i => string.Equals(tvdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray(); - - foreach (var item in series) - { - _libraryMonitor.ReportFileSystemChanged(item.Path); - } - - return NoContent(); - } - - /// - /// Reports that new movies have been added by an external source. - /// - /// The tmdbId. - /// The imdbId. - /// Report success. - /// A . - [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")] - [HttpPost("Library/Movies/Updated")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId) - { - var movies = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.Movie }, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - } + DefaultImageOptions = defaultImageOptions ?? Array.Empty() }); - - if (!string.IsNullOrWhiteSpace(imdbId)) - { - movies = movies.Where(i => string.Equals(imdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList(); - } - else if (!string.IsNullOrWhiteSpace(tmdbId)) - { - movies = movies.Where(i => string.Equals(tmdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList(); - } - else - { - movies = new List(); - } - - foreach (var item in movies) - { - _libraryMonitor.ReportFileSystemChanged(item.Path); - } - - return NoContent(); } - /// - /// Reports that new movies have been added by an external source. - /// - /// The update paths. - /// Report success. - /// A . - [HttpPost("Library/Media/Updated")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto) - { - foreach (var item in dto.Updates) - { - _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null.")); - } + result.TypeOptions = typeOptions.ToArray(); - return NoContent(); + return result; + } + + private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite) + { + var query = new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { itemKind }, + Limit = 0, + Recursive = true, + IsVirtualItem = false, + IsFavorite = isFavorite, + DtoOptions = new DtoOptions(false) + { + EnableImages = false + } + }; + + return _libraryManager.GetItemsResult(query).TotalRecordCount; + } + + private BaseItem? TranslateParentItem(BaseItem item, User user) + { + return item.GetParent() is AggregateFolder + ? _libraryManager.GetUserRootFolder().GetChildren(user, true) + .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path)) + : item; + } + + private async Task LogDownloadAsync(BaseItem item, User user) + { + try + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name), + "UserDownloadingContent", + User.GetUserId()) + { + ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()), + }).ConfigureAwait(false); } - - /// - /// Downloads item media. - /// - /// The item id. - /// Media downloaded. - /// Item not found. - /// A containing the media stream. - /// User can't download or item can't be downloaded. - [HttpGet("Items/{itemId}/Download")] - [Authorize(Policy = Policies.Download)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesFile("video/*", "audio/*")] - public async Task GetDownload([FromRoute, Required] Guid itemId) + catch { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } - - var user = _userManager.GetUserById(User.GetUserId()); - - if (user is not null) - { - if (!item.CanDownload(user)) - { - throw new ArgumentException("Item does not support downloading"); - } - } - else - { - if (!item.CanDownload()) - { - throw new ArgumentException("Item does not support downloading"); - } - } - - if (user is not null) - { - await LogDownloadAsync(item, user).ConfigureAwait(false); - } - - // Quotes are valid in linux. They'll possibly cause issues here. - var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal); - - return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true); - } - - /// - /// Gets similar items. - /// - /// The item id. - /// Exclude artist ids. - /// Optional. Filter by user id, and attach user data. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. - /// Similar items returned. - /// A containing the similar items. - [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")] - [HttpGet("Items/{itemId}/Similar")] - [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")] - [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")] - [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")] - [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSimilarItems( - [FromRoute, Required] Guid itemId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, - [FromQuery] Guid? userId, - [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) - { - var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) - ? _libraryManager.RootFolder - : _libraryManager.GetUserRootFolder()) - : _libraryManager.GetItemById(itemId); - - if (item is Episode || (item is IItemByName && item is not MusicArtist)) - { - return new QueryResult(); - } - - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User); - - var program = item as IHasProgramAttributes; - bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer; - bool? isSeries = item is Series || (program is not null && program.IsSeries); - - var includeItemTypes = new List(); - if (isMovie.Value) - { - includeItemTypes.Add(BaseItemKind.Movie); - if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) - { - includeItemTypes.Add(BaseItemKind.Trailer); - includeItemTypes.Add(BaseItemKind.LiveTvProgram); - } - } - else if (isSeries.Value) - { - includeItemTypes.Add(BaseItemKind.Series); - } - else - { - // For non series and movie types these columns are typically null - // isSeries = null; - isMovie = null; - includeItemTypes.Add(item.GetBaseItemKind()); - } - - var query = new InternalItemsQuery(user) - { - Genres = item.Genres, - Limit = limit, - IncludeItemTypes = includeItemTypes.ToArray(), - SimilarTo = item, - DtoOptions = dtoOptions, - EnableTotalRecordCount = !isMovie ?? true, - EnableGroupByMetadataKey = isMovie ?? false, - MinSimilarityScore = 2 // A remnant from album/artist scoring - }; - - // ExcludeArtistIds - if (excludeArtistIds.Length != 0) - { - query.ExcludeArtistIds = excludeArtistIds; - } - - List itemsResult = _libraryManager.GetItemList(query); - - var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user); - - return new QueryResult( - query.StartIndex, - itemsResult.Count, - returnList); - } - - /// - /// Gets the library options info. - /// - /// Library content type. - /// Whether this is a new library. - /// Library options info returned. - /// Library options info. - [HttpGet("Libraries/AvailableOptions")] - [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetLibraryOptionsInfo( - [FromQuery] string? libraryContentType, - [FromQuery] bool isNewLibrary = false) - { - var result = new LibraryOptionsResultDto(); - - var types = GetRepresentativeItemTypes(libraryContentType); - var typesList = types.ToList(); - - var plugins = _providerManager.GetAllMetadataPlugins() - .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase)) - .OrderBy(i => typesList.IndexOf(i.ItemType)) - .ToList(); - - result.MetadataSavers = plugins - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver)) - .Select(i => new LibraryOptionInfoDto - { - Name = i.Name, - DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary) - }) - .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .ToArray(); - - result.MetadataReaders = plugins - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider)) - .Select(i => new LibraryOptionInfoDto - { - Name = i.Name, - DefaultEnabled = true - }) - .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .ToArray(); - - result.SubtitleFetchers = plugins - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher)) - .Select(i => new LibraryOptionInfoDto - { - Name = i.Name, - DefaultEnabled = true - }) - .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .ToArray(); - - var typeOptions = new List(); - - foreach (var type in types) - { - TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions); - - typeOptions.Add(new LibraryTypeOptionsDto - { - Type = type, - - MetadataFetchers = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher)) - .Select(i => new LibraryOptionInfoDto - { - Name = i.Name, - DefaultEnabled = IsMetadataFetcherEnabledByDefault(i.Name, type, isNewLibrary) - }) - .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .ToArray(), - - ImageFetchers = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher)) - .Select(i => new LibraryOptionInfoDto - { - Name = i.Name, - DefaultEnabled = IsImageFetcherEnabledByDefault(i.Name, type, isNewLibrary) - }) - .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .ToArray(), - - SupportedImageTypes = plugins - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .SelectMany(i => i.SupportedImageTypes ?? Array.Empty()) - .Distinct() - .ToArray(), - - DefaultImageOptions = defaultImageOptions ?? Array.Empty() - }); - } - - result.TypeOptions = typeOptions.ToArray(); - - return result; - } - - private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite) - { - var query = new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { itemKind }, - Limit = 0, - Recursive = true, - IsVirtualItem = false, - IsFavorite = isFavorite, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - } - }; - - return _libraryManager.GetItemsResult(query).TotalRecordCount; - } - - private BaseItem? TranslateParentItem(BaseItem item, User user) - { - return item.GetParent() is AggregateFolder - ? _libraryManager.GetUserRootFolder().GetChildren(user, true) - .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path)) - : item; - } - - private async Task LogDownloadAsync(BaseItem item, User user) - { - try - { - await _activityManager.CreateAsync(new ActivityLog( - string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name), - "UserDownloadingContent", - User.GetUserId()) - { - ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()), - }).ConfigureAwait(false); - } - catch - { - // Logged at lower levels - } - } - - private static string[] GetRepresentativeItemTypes(string? contentType) - { - return contentType switch - { - CollectionType.BoxSets => new[] { "BoxSet" }, - CollectionType.Playlists => new[] { "Playlist" }, - CollectionType.Movies => new[] { "Movie" }, - CollectionType.TvShows => new[] { "Series", "Season", "Episode" }, - CollectionType.Books => new[] { "Book" }, - CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" }, - CollectionType.HomeVideos => new[] { "Video", "Photo" }, - CollectionType.Photos => new[] { "Video", "Photo" }, - CollectionType.MusicVideos => new[] { "MusicVideo" }, - _ => new[] { "Series", "Season", "Episode", "Movie" } - }; - } - - private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary) - { - if (isNewLibrary) - { - return false; - } - - var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions - .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase)); - } - - private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary) - { - if (isNewLibrary) - { - if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) - { - return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) - || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) - || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); - } - - return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); - } - - var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - return metadataOptions.Length == 0 - || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); - } - - private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) - { - if (isNewLibrary) - { - if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) - { - return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase) - && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) - && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) - && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase); - } - - return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase); - } - - var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - if (metadataOptions.Length == 0) - { - return true; - } - - return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); + // Logged at lower levels } } + + private static string[] GetRepresentativeItemTypes(string? contentType) + { + return contentType switch + { + CollectionType.BoxSets => new[] { "BoxSet" }, + CollectionType.Playlists => new[] { "Playlist" }, + CollectionType.Movies => new[] { "Movie" }, + CollectionType.TvShows => new[] { "Series", "Season", "Episode" }, + CollectionType.Books => new[] { "Book" }, + CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" }, + CollectionType.HomeVideos => new[] { "Video", "Photo" }, + CollectionType.Photos => new[] { "Video", "Photo" }, + CollectionType.MusicVideos => new[] { "MusicVideo" }, + _ => new[] { "Series", "Season", "Episode", "Movie" } + }; + } + + private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary) + { + if (isNewLibrary) + { + return false; + } + + var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions + .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase)); + } + + private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary) + { + if (isNewLibrary) + { + if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) + { + return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); + } + + return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); + } + + var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + return metadataOptions.Length == 0 + || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); + } + + private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) + { + if (isNewLibrary) + { + if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) + { + return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase); + } + + return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase); + } + + var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions + .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + if (metadataOptions.Length == 0) + { + return true; + } + + return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); + } } diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 1c23940556..b012ff42eb 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -20,308 +20,307 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The library structure controller. +/// +[Route("Library/VirtualFolders")] +[Authorize(Policy = Policies.FirstTimeSetupOrElevated)] +public class LibraryStructureController : BaseJellyfinApiController { + private readonly IServerApplicationPaths _appPaths; + private readonly ILibraryManager _libraryManager; + private readonly ILibraryMonitor _libraryMonitor; + /// - /// The library structure controller. + /// Initializes a new instance of the class. /// - [Route("Library/VirtualFolders")] - [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] - public class LibraryStructureController : BaseJellyfinApiController + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public LibraryStructureController( + IServerConfigurationManager serverConfigurationManager, + ILibraryManager libraryManager, + ILibraryMonitor libraryMonitor) { - private readonly IServerApplicationPaths _appPaths; - private readonly ILibraryManager _libraryManager; - private readonly ILibraryMonitor _libraryMonitor; + _appPaths = serverConfigurationManager.ApplicationPaths; + _libraryManager = libraryManager; + _libraryMonitor = libraryMonitor; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - public LibraryStructureController( - IServerConfigurationManager serverConfigurationManager, - ILibraryManager libraryManager, - ILibraryMonitor libraryMonitor) + /// + /// Gets all virtual folders. + /// + /// Virtual folders retrieved. + /// An with the virtual folders. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetVirtualFolders() + { + return _libraryManager.GetVirtualFolders(true); + } + + /// + /// Adds a virtual folder. + /// + /// The name of the virtual folder. + /// The type of the collection. + /// The paths of the virtual folder. + /// The library options. + /// Whether to refresh the library. + /// Folder added. + /// A . + [HttpPost] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task AddVirtualFolder( + [FromQuery] string? name, + [FromQuery] CollectionTypeOptions? collectionType, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths, + [FromBody] AddVirtualFolderDto? libraryOptionsDto, + [FromQuery] bool refreshLibrary = false) + { + var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions(); + + if (paths is not null && paths.Length > 0) { - _appPaths = serverConfigurationManager.ApplicationPaths; - _libraryManager = libraryManager; - _libraryMonitor = libraryMonitor; + libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray(); } - /// - /// Gets all virtual folders. - /// - /// Virtual folders retrieved. - /// An with the virtual folders. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetVirtualFolders() + await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Removes a virtual folder. + /// + /// The name of the folder. + /// Whether to refresh the library. + /// Folder removed. + /// A . + [HttpDelete] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task RemoveVirtualFolder( + [FromQuery] string? name, + [FromQuery] bool refreshLibrary = false) + { + await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Renames a virtual folder. + /// + /// The name of the virtual folder. + /// The new name. + /// Whether to refresh the library. + /// Folder renamed. + /// Library doesn't exist. + /// Library already exists. + /// A on success, a if the library doesn't exist, a if the new name is already taken. + /// The new name may not be null. + [HttpPost("Name")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public ActionResult RenameVirtualFolder( + [FromQuery] string? name, + [FromQuery] string? newName, + [FromQuery] bool refreshLibrary = false) + { + if (string.IsNullOrWhiteSpace(name)) { - return _libraryManager.GetVirtualFolders(true); + throw new ArgumentNullException(nameof(name)); } - /// - /// Adds a virtual folder. - /// - /// The name of the virtual folder. - /// The type of the collection. - /// The paths of the virtual folder. - /// The library options. - /// Whether to refresh the library. - /// Folder added. - /// A . - [HttpPost] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task AddVirtualFolder( - [FromQuery] string? name, - [FromQuery] CollectionTypeOptions? collectionType, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths, - [FromBody] AddVirtualFolderDto? libraryOptionsDto, - [FromQuery] bool refreshLibrary = false) + if (string.IsNullOrWhiteSpace(newName)) { - var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions(); - - if (paths is not null && paths.Length > 0) - { - libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray(); - } - - await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); - - return NoContent(); + throw new ArgumentNullException(nameof(newName)); } - /// - /// Removes a virtual folder. - /// - /// The name of the folder. - /// Whether to refresh the library. - /// Folder removed. - /// A . - [HttpDelete] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task RemoveVirtualFolder( - [FromQuery] string? name, - [FromQuery] bool refreshLibrary = false) + var rootFolderPath = _appPaths.DefaultUserViewsPath; + + var currentPath = Path.Combine(rootFolderPath, name); + var newPath = Path.Combine(rootFolderPath, newName); + + if (!Directory.Exists(currentPath)) { - await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); - return NoContent(); + return NotFound("The media collection does not exist."); } - /// - /// Renames a virtual folder. - /// - /// The name of the virtual folder. - /// The new name. - /// Whether to refresh the library. - /// Folder renamed. - /// Library doesn't exist. - /// Library already exists. - /// A on success, a if the library doesn't exist, a if the new name is already taken. - /// The new name may not be null. - [HttpPost("Name")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public ActionResult RenameVirtualFolder( - [FromQuery] string? name, - [FromQuery] string? newName, - [FromQuery] bool refreshLibrary = false) + if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath)) { - if (string.IsNullOrWhiteSpace(name)) + return Conflict($"The media library already exists at {newPath}."); + } + + _libraryMonitor.Stop(); + + try + { + // Changing capitalization. Handle windows case insensitivity + if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase)) { - throw new ArgumentNullException(nameof(name)); + var tempPath = Path.Combine( + rootFolderPath, + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); + Directory.Move(currentPath, tempPath); + currentPath = tempPath; } - if (string.IsNullOrWhiteSpace(newName)) + Directory.Move(currentPath, newPath); + } + finally + { + CollectionFolder.OnCollectionFolderChange(); + + Task.Run(async () => { - throw new ArgumentNullException(nameof(newName)); - } - - var rootFolderPath = _appPaths.DefaultUserViewsPath; - - var currentPath = Path.Combine(rootFolderPath, name); - var newPath = Path.Combine(rootFolderPath, newName); - - if (!Directory.Exists(currentPath)) - { - return NotFound("The media collection does not exist."); - } - - if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath)) - { - return Conflict($"The media library already exists at {newPath}."); - } - - _libraryMonitor.Stop(); - - try - { - // Changing capitalization. Handle windows case insensitivity - if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase)) + // No need to start if scanning the library because it will handle it + if (refreshLibrary) { - var tempPath = Path.Combine( - rootFolderPath, - Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); - Directory.Move(currentPath, tempPath); - currentPath = tempPath; + await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); } - - Directory.Move(currentPath, newPath); - } - finally - { - CollectionFolder.OnCollectionFolderChange(); - - Task.Run(async () => + else { - // No need to start if scanning the library because it will handle it - if (refreshLibrary) - { - await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - // Have to block here to allow exceptions to bubble - await Task.Delay(1000).ConfigureAwait(false); - _libraryMonitor.Start(); - } - }); - } - - return NoContent(); + // Need to add a delay here or directory watchers may still pick up the changes + // Have to block here to allow exceptions to bubble + await Task.Delay(1000).ConfigureAwait(false); + _libraryMonitor.Start(); + } + }); } - /// - /// Add a media path to a library. - /// - /// The media path dto. - /// Whether to refresh the library. - /// A . - /// Media path added. - /// The name of the library may not be empty. - [HttpPost("Paths")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult AddMediaPath( - [FromBody, Required] MediaPathDto mediaPathDto, - [FromQuery] bool refreshLibrary = false) + return NoContent(); + } + + /// + /// Add a media path to a library. + /// + /// The media path dto. + /// Whether to refresh the library. + /// A . + /// Media path added. + /// The name of the library may not be empty. + [HttpPost("Paths")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult AddMediaPath( + [FromBody, Required] MediaPathDto mediaPathDto, + [FromQuery] bool refreshLibrary = false) + { + _libraryMonitor.Stop(); + + try { - _libraryMonitor.Stop(); + var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null.")); - try + _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath); + } + finally + { + Task.Run(async () => { - var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null.")); - - _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath); - } - finally - { - Task.Run(async () => + // No need to start if scanning the library because it will handle it + if (refreshLibrary) { - // No need to start if scanning the library because it will handle it - if (refreshLibrary) - { - await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - // Have to block here to allow exceptions to bubble - await Task.Delay(1000).ConfigureAwait(false); - _libraryMonitor.Start(); - } - }); - } - - return NoContent(); - } - - /// - /// Updates a media path. - /// - /// The name of the library and path infos. - /// A . - /// Media path updated. - /// The name of the library may not be empty. - [HttpPost("Paths/Update")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto) - { - if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name)) - { - throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty"); - } - - _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo); - return NoContent(); - } - - /// - /// Remove a media path. - /// - /// The name of the library. - /// The path to remove. - /// Whether to refresh the library. - /// A . - /// Media path removed. - /// The name of the library may not be empty. - [HttpDelete("Paths")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RemoveMediaPath( - [FromQuery] string? name, - [FromQuery] string? path, - [FromQuery] bool refreshLibrary = false) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - _libraryMonitor.Stop(); - - try - { - _libraryManager.RemoveMediaPath(name, path); - } - finally - { - Task.Run(async () => + await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); + } + else { - // No need to start if scanning the library because it will handle it - if (refreshLibrary) - { - await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - // Have to block here to allow exceptions to bubble - await Task.Delay(1000).ConfigureAwait(false); - _libraryMonitor.Start(); - } - }); - } - - return NoContent(); + // Need to add a delay here or directory watchers may still pick up the changes + // Have to block here to allow exceptions to bubble + await Task.Delay(1000).ConfigureAwait(false); + _libraryMonitor.Start(); + } + }); } - /// - /// Update library options. - /// - /// The library name and options. - /// Library updated. - /// A . - [HttpPost("LibraryOptions")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateLibraryOptions( - [FromBody] UpdateLibraryOptionsDto request) + return NoContent(); + } + + /// + /// Updates a media path. + /// + /// The name of the library and path infos. + /// A . + /// Media path updated. + /// The name of the library may not be empty. + [HttpPost("Paths/Update")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto) + { + if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name)) { - var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id); - - collectionFolder.UpdateLibraryOptions(request.LibraryOptions); - return NoContent(); + throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty"); } + + _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo); + return NoContent(); + } + + /// + /// Remove a media path. + /// + /// The name of the library. + /// The path to remove. + /// Whether to refresh the library. + /// A . + /// Media path removed. + /// The name of the library may not be empty. + [HttpDelete("Paths")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RemoveMediaPath( + [FromQuery] string? name, + [FromQuery] string? path, + [FromQuery] bool refreshLibrary = false) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + _libraryMonitor.Stop(); + + try + { + _libraryManager.RemoveMediaPath(name, path); + } + finally + { + Task.Run(async () => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + await _libraryManager.ValidateMediaLibrary(new SimpleProgress(), CancellationToken.None).ConfigureAwait(false); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + // Have to block here to allow exceptions to bubble + await Task.Delay(1000).ConfigureAwait(false); + _libraryMonitor.Start(); + } + }); + } + + return NoContent(); + } + + /// + /// Update library options. + /// + /// The library name and options. + /// Library updated. + /// A . + [HttpPost("LibraryOptions")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateLibraryOptions( + [FromBody] UpdateLibraryOptionsDto request) + { + var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id); + + collectionFolder.UpdateLibraryOptions(request.LibraryOptions); + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 5228e0babf..21b4243464 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -35,1200 +35,1199 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Live tv controller. +/// +public class LiveTvController : BaseJellyfinApiController { + private readonly ILiveTvManager _liveTvManager; + private readonly IUserManager _userManager; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IConfigurationManager _configurationManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly ISessionManager _sessionManager; + /// - /// Live tv controller. + /// Initializes a new instance of the class. /// - public class LiveTvController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the class. + /// Instance of the interface. + public LiveTvController( + ILiveTvManager liveTvManager, + IUserManager userManager, + IHttpClientFactory httpClientFactory, + ILibraryManager libraryManager, + IDtoService dtoService, + IMediaSourceManager mediaSourceManager, + IConfigurationManager configurationManager, + TranscodingJobHelper transcodingJobHelper, + ISessionManager sessionManager) { - private readonly ILiveTvManager _liveTvManager; - private readonly IUserManager _userManager; - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILibraryManager _libraryManager; - private readonly IDtoService _dtoService; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IConfigurationManager _configurationManager; - private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly ISessionManager _sessionManager; + _liveTvManager = liveTvManager; + _userManager = userManager; + _httpClientFactory = httpClientFactory; + _libraryManager = libraryManager; + _dtoService = dtoService; + _mediaSourceManager = mediaSourceManager; + _configurationManager = configurationManager; + _transcodingJobHelper = transcodingJobHelper; + _sessionManager = sessionManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the class. - /// Instance of the interface. - public LiveTvController( - ILiveTvManager liveTvManager, - IUserManager userManager, - IHttpClientFactory httpClientFactory, - ILibraryManager libraryManager, - IDtoService dtoService, - IMediaSourceManager mediaSourceManager, - IConfigurationManager configurationManager, - TranscodingJobHelper transcodingJobHelper, - ISessionManager sessionManager) - { - _liveTvManager = liveTvManager; - _userManager = userManager; - _httpClientFactory = httpClientFactory; - _libraryManager = libraryManager; - _dtoService = dtoService; - _mediaSourceManager = mediaSourceManager; - _configurationManager = configurationManager; - _transcodingJobHelper = transcodingJobHelper; - _sessionManager = sessionManager; - } + /// + /// Gets available live tv services. + /// + /// Available live tv services returned. + /// + /// An containing the available live tv services. + /// + [HttpGet("Info")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult GetLiveTvInfo() + { + return _liveTvManager.GetLiveTvInfo(CancellationToken.None); + } - /// - /// Gets available live tv services. - /// - /// Available live tv services returned. - /// - /// An containing the available live tv services. - /// - [HttpGet("Info")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public ActionResult GetLiveTvInfo() - { - return _liveTvManager.GetLiveTvInfo(CancellationToken.None); - } + /// + /// Gets available live tv channels. + /// + /// Optional. Filter by channel type. + /// Optional. Filter by user and attach user data. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. Filter for movies. + /// Optional. Filter for series. + /// Optional. Filter for news. + /// Optional. Filter for kids. + /// Optional. Filter for sports. + /// Optional. The maximum number of records to return. + /// Optional. Filter by channels that are favorites, or not. + /// Optional. Filter by channels that are liked, or not. + /// Optional. Filter by channels that are disliked, or not. + /// Optional. Include image information in output. + /// Optional. The max number of images to return, per image type. + /// "Optional. The image types to include in the output. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include user data. + /// Optional. Key to sort by. + /// Optional. Sort order. + /// Optional. Incorporate favorite and like status into channel sorting. + /// Optional. Adds current program info to each channel. + /// Available live tv channels returned. + /// + /// An containing the resulting available live tv channels. + /// + [HttpGet("Channels")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult> GetLiveTvChannels( + [FromQuery] ChannelType? type, + [FromQuery] Guid? userId, + [FromQuery] int? startIndex, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, + [FromQuery] int? limit, + [FromQuery] bool? isFavorite, + [FromQuery] bool? isLiked, + [FromQuery] bool? isDisliked, + [FromQuery] bool? enableImages, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] bool? enableUserData, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery] SortOrder? sortOrder, + [FromQuery] bool enableFavoriteSorting = false, + [FromQuery] bool addCurrentProgram = true) + { + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - /// - /// Gets available live tv channels. - /// - /// Optional. Filter by channel type. - /// Optional. Filter by user and attach user data. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. Filter for movies. - /// Optional. Filter for series. - /// Optional. Filter for news. - /// Optional. Filter for kids. - /// Optional. Filter for sports. - /// Optional. The maximum number of records to return. - /// Optional. Filter by channels that are favorites, or not. - /// Optional. Filter by channels that are liked, or not. - /// Optional. Filter by channels that are disliked, or not. - /// Optional. Include image information in output. - /// Optional. The max number of images to return, per image type. - /// "Optional. The image types to include in the output. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include user data. - /// Optional. Key to sort by. - /// Optional. Sort order. - /// Optional. Incorporate favorite and like status into channel sorting. - /// Optional. Adds current program info to each channel. - /// Available live tv channels returned. - /// - /// An containing the resulting available live tv channels. - /// - [HttpGet("Channels")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public ActionResult> GetLiveTvChannels( - [FromQuery] ChannelType? type, - [FromQuery] Guid? userId, - [FromQuery] int? startIndex, - [FromQuery] bool? isMovie, - [FromQuery] bool? isSeries, - [FromQuery] bool? isNews, - [FromQuery] bool? isKids, - [FromQuery] bool? isSports, - [FromQuery] int? limit, - [FromQuery] bool? isFavorite, - [FromQuery] bool? isLiked, - [FromQuery] bool? isDisliked, - [FromQuery] bool? enableImages, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] bool? enableUserData, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, - [FromQuery] SortOrder? sortOrder, - [FromQuery] bool enableFavoriteSorting = false, - [FromQuery] bool addCurrentProgram = true) - { - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - - var channelResult = _liveTvManager.GetInternalChannels( - new LiveTvChannelQuery - { - ChannelType = type, - UserId = userId ?? Guid.Empty, - StartIndex = startIndex, - Limit = limit, - IsFavorite = isFavorite, - IsLiked = isLiked, - IsDisliked = isDisliked, - EnableFavoriteSorting = enableFavoriteSorting, - IsMovie = isMovie, - IsSeries = isSeries, - IsNews = isNews, - IsKids = isKids, - IsSports = isSports, - SortBy = sortBy, - SortOrder = sortOrder ?? SortOrder.Ascending, - AddCurrentProgram = addCurrentProgram - }, - dtoOptions, - CancellationToken.None); - - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var fieldsList = dtoOptions.Fields.ToList(); - fieldsList.Remove(ItemFields.CanDelete); - fieldsList.Remove(ItemFields.CanDownload); - fieldsList.Remove(ItemFields.DisplayPreferencesId); - fieldsList.Remove(ItemFields.Etag); - dtoOptions.Fields = fieldsList.ToArray(); - dtoOptions.AddCurrentProgram = addCurrentProgram; - - var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, dtoOptions, user); - return new QueryResult( - startIndex, - channelResult.TotalRecordCount, - returnArray); - } - - /// - /// Gets a live tv channel. - /// - /// Channel id. - /// Optional. Attach user data. - /// Live tv channel returned. - /// An containing the live tv channel. - [HttpGet("Channels/{channelId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public ActionResult GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) - { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var item = channelId.Equals(default) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.GetItemById(channelId); - - var dtoOptions = new DtoOptions() - .AddClientFields(User); - return _dtoService.GetBaseItemDto(item, dtoOptions, user); - } - - /// - /// Gets live tv recordings. - /// - /// Optional. Filter by channel id. - /// Optional. Filter by user and attach user data. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Filter by recording status. - /// Optional. Filter by recordings that are in progress, or not. - /// Optional. Filter by recordings belonging to a series timer. - /// Optional. Include image information in output. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include user data. - /// Optional. Filter for movies. - /// Optional. Filter for series. - /// Optional. Filter for kids. - /// Optional. Filter for sports. - /// Optional. Filter for news. - /// Optional. Filter for is library item. - /// Optional. Return total record count. - /// Live tv recordings returned. - /// An containing the live tv recordings. - [HttpGet("Recordings")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public ActionResult> GetRecordings( - [FromQuery] string? channelId, - [FromQuery] Guid? userId, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] RecordingStatus? status, - [FromQuery] bool? isInProgress, - [FromQuery] string? seriesTimerId, - [FromQuery] bool? enableImages, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] bool? enableUserData, - [FromQuery] bool? isMovie, - [FromQuery] bool? isSeries, - [FromQuery] bool? isKids, - [FromQuery] bool? isSports, - [FromQuery] bool? isNews, - [FromQuery] bool? isLibraryItem, - [FromQuery] bool enableTotalRecordCount = true) - { - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - - return _liveTvManager.GetRecordings( - new RecordingQuery - { - ChannelId = channelId, - UserId = userId ?? Guid.Empty, - StartIndex = startIndex, - Limit = limit, - Status = status, - SeriesTimerId = seriesTimerId, - IsInProgress = isInProgress, - EnableTotalRecordCount = enableTotalRecordCount, - IsMovie = isMovie, - IsNews = isNews, - IsSeries = isSeries, - IsKids = isKids, - IsSports = isSports, - IsLibraryItem = isLibraryItem, - Fields = fields, - ImageTypeLimit = imageTypeLimit, - EnableImages = enableImages - }, - dtoOptions); - } - - /// - /// Gets live tv recording series. - /// - /// Optional. Filter by channel id. - /// Optional. Filter by user and attach user data. - /// Optional. Filter by recording group. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Filter by recording status. - /// Optional. Filter by recordings that are in progress, or not. - /// Optional. Filter by recordings belonging to a series timer. - /// Optional. Include image information in output. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include user data. - /// Optional. Return total record count. - /// Live tv recordings returned. - /// An containing the live tv recordings. - [HttpGet("Recordings/Series")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - [Obsolete("This endpoint is obsolete.")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "groupId", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "status", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isInProgress", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesTimerId", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "fields", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")] - public ActionResult> GetRecordingsSeries( - [FromQuery] string? channelId, - [FromQuery] Guid? userId, - [FromQuery] string? groupId, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] RecordingStatus? status, - [FromQuery] bool? isInProgress, - [FromQuery] string? seriesTimerId, - [FromQuery] bool? enableImages, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] bool? enableUserData, - [FromQuery] bool enableTotalRecordCount = true) - { - return new QueryResult(); - } - - /// - /// Gets live tv recording groups. - /// - /// Optional. Filter by user and attach user data. - /// Recording groups returned. - /// An containing the recording groups. - [HttpGet("Recordings/Groups")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - [Obsolete("This endpoint is obsolete.")] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] - public ActionResult> GetRecordingGroups([FromQuery] Guid? userId) - { - return new QueryResult(); - } - - /// - /// Gets recording folders. - /// - /// Optional. Filter by user and attach user data. - /// Recording folders returned. - /// An containing the recording folders. - [HttpGet("Recordings/Folders")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public ActionResult> GetRecordingFolders([FromQuery] Guid? userId) - { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var folders = _liveTvManager.GetRecordingFolders(user); - - var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user); - - return new QueryResult(returnArray); - } - - /// - /// Gets a live tv recording. - /// - /// Recording id. - /// Optional. Attach user data. - /// Recording returned. - /// An containing the live tv recording. - [HttpGet("Recordings/{recordingId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public ActionResult GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) - { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); - - var dtoOptions = new DtoOptions() - .AddClientFields(User); - - return _dtoService.GetBaseItemDto(item, dtoOptions, user); - } - - /// - /// Resets a tv tuner. - /// - /// Tuner id. - /// Tuner reset. - /// A . - [HttpPost("Tuners/{tunerId}/Reset")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public async Task ResetTuner([FromRoute, Required] string tunerId) - { - await AssertUserCanManageLiveTv().ConfigureAwait(false); - await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Gets a timer. - /// - /// Timer id. - /// Timer returned. - /// - /// A containing an which contains the timer. - /// - [HttpGet("Timers/{timerId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public async Task> GetTimer([FromRoute, Required] string timerId) - { - return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false); - } - - /// - /// Gets the default values for a new timer. - /// - /// Optional. To attach default values based on a program. - /// Default values returned. - /// - /// A containing an which contains the default values for a timer. - /// - [HttpGet("Timers/Defaults")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public async Task> GetDefaultTimer([FromQuery] string? programId) - { - return string.IsNullOrEmpty(programId) - ? await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false) - : await _liveTvManager.GetNewTimerDefaults(programId, CancellationToken.None).ConfigureAwait(false); - } - - /// - /// Gets the live tv timers. - /// - /// Optional. Filter by channel id. - /// Optional. Filter by timers belonging to a series timer. - /// Optional. Filter by timers that are active. - /// Optional. Filter by timers that are scheduled. - /// - /// A containing an which contains the live tv timers. - /// - [HttpGet("Timers")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public async Task>> GetTimers( - [FromQuery] string? channelId, - [FromQuery] string? seriesTimerId, - [FromQuery] bool? isActive, - [FromQuery] bool? isScheduled) - { - return await _liveTvManager.GetTimers( - new TimerQuery - { - ChannelId = channelId, - SeriesTimerId = seriesTimerId, - IsActive = isActive, - IsScheduled = isScheduled - }, - CancellationToken.None).ConfigureAwait(false); - } - - /// - /// Gets available live tv epgs. - /// - /// The channels to return guide information for. - /// Optional. Filter by user id. - /// Optional. The minimum premiere start date. - /// Optional. Filter by programs that have completed airing, or not. - /// Optional. Filter by programs that are currently airing, or not. - /// Optional. The maximum premiere start date. - /// Optional. The minimum premiere end date. - /// Optional. The maximum premiere end date. - /// Optional. Filter for movies. - /// Optional. Filter for series. - /// Optional. Filter for news. - /// Optional. Filter for kids. - /// Optional. Filter for sports. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Specify one or more sort orders, comma delimited. Options: Name, StartDate. - /// Sort Order - Ascending,Descending. - /// The genres to return guide information for. - /// The genre ids to return guide information for. - /// Optional. Include image information in output. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. Include user data. - /// Optional. Filter by series timer id. - /// Optional. Filter by library series id. - /// Optional. Specify additional fields of information to return in the output. - /// Retrieve total record count. - /// Live tv epgs returned. - /// - /// A containing a which contains the live tv epgs. - /// - [HttpGet("Programs")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public async Task>> GetLiveTvPrograms( - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds, - [FromQuery] Guid? userId, - [FromQuery] DateTime? minStartDate, - [FromQuery] bool? hasAired, - [FromQuery] bool? isAiring, - [FromQuery] DateTime? maxStartDate, - [FromQuery] DateTime? minEndDate, - [FromQuery] DateTime? maxEndDate, - [FromQuery] bool? isMovie, - [FromQuery] bool? isSeries, - [FromQuery] bool? isNews, - [FromQuery] bool? isKids, - [FromQuery] bool? isSports, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, - [FromQuery] bool? enableImages, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] bool? enableUserData, - [FromQuery] string? seriesTimerId, - [FromQuery] Guid? librarySeriesId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] bool enableTotalRecordCount = true) - { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var query = new InternalItemsQuery(user) + var channelResult = _liveTvManager.GetInternalChannels( + new LiveTvChannelQuery { - ChannelIds = channelIds, - HasAired = hasAired, - IsAiring = isAiring, - EnableTotalRecordCount = enableTotalRecordCount, - MinStartDate = minStartDate, - MinEndDate = minEndDate, - MaxStartDate = maxStartDate, - MaxEndDate = maxEndDate, + ChannelType = type, + UserId = userId ?? Guid.Empty, StartIndex = startIndex, Limit = limit, - OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), - IsNews = isNews, + IsFavorite = isFavorite, + IsLiked = isLiked, + IsDisliked = isDisliked, + EnableFavoriteSorting = enableFavoriteSorting, IsMovie = isMovie, IsSeries = isSeries, + IsNews = isNews, IsKids = isKids, IsSports = isSports, - SeriesTimerId = seriesTimerId, - Genres = genres, - GenreIds = genreIds - }; + SortBy = sortBy, + SortOrder = sortOrder ?? SortOrder.Ascending, + AddCurrentProgram = addCurrentProgram + }, + dtoOptions, + CancellationToken.None); - if (librarySeriesId.HasValue && !librarySeriesId.Equals(default)) + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var fieldsList = dtoOptions.Fields.ToList(); + fieldsList.Remove(ItemFields.CanDelete); + fieldsList.Remove(ItemFields.CanDownload); + fieldsList.Remove(ItemFields.DisplayPreferencesId); + fieldsList.Remove(ItemFields.Etag); + dtoOptions.Fields = fieldsList.ToArray(); + dtoOptions.AddCurrentProgram = addCurrentProgram; + + var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, dtoOptions, user); + return new QueryResult( + startIndex, + channelResult.TotalRecordCount, + returnArray); + } + + /// + /// Gets a live tv channel. + /// + /// Channel id. + /// Optional. Attach user data. + /// Live tv channel returned. + /// An containing the live tv channel. + [HttpGet("Channels/{channelId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var item = channelId.Equals(default) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(channelId); + + var dtoOptions = new DtoOptions() + .AddClientFields(User); + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + /// + /// Gets live tv recordings. + /// + /// Optional. Filter by channel id. + /// Optional. Filter by user and attach user data. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Filter by recording status. + /// Optional. Filter by recordings that are in progress, or not. + /// Optional. Filter by recordings belonging to a series timer. + /// Optional. Include image information in output. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include user data. + /// Optional. Filter for movies. + /// Optional. Filter for series. + /// Optional. Filter for kids. + /// Optional. Filter for sports. + /// Optional. Filter for news. + /// Optional. Filter for is library item. + /// Optional. Return total record count. + /// Live tv recordings returned. + /// An containing the live tv recordings. + [HttpGet("Recordings")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult> GetRecordings( + [FromQuery] string? channelId, + [FromQuery] Guid? userId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] RecordingStatus? status, + [FromQuery] bool? isInProgress, + [FromQuery] string? seriesTimerId, + [FromQuery] bool? enableImages, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] bool? enableUserData, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, + [FromQuery] bool? isNews, + [FromQuery] bool? isLibraryItem, + [FromQuery] bool enableTotalRecordCount = true) + { + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + return _liveTvManager.GetRecordings( + new RecordingQuery { - query.IsSeries = true; - - if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series) - { - query.Name = series.Name; - } - } - - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); - } - - /// - /// Gets available live tv epgs. - /// - /// Request body. - /// Live tv epgs returned. - /// - /// A containing a which contains the live tv epgs. - /// - [HttpPost("Programs")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.DefaultAuthorization)] - public async Task>> GetPrograms([FromBody] GetProgramsDto body) - { - var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId); - - var query = new InternalItemsQuery(user) - { - ChannelIds = body.ChannelIds, - HasAired = body.HasAired, - IsAiring = body.IsAiring, - EnableTotalRecordCount = body.EnableTotalRecordCount, - MinStartDate = body.MinStartDate, - MinEndDate = body.MinEndDate, - MaxStartDate = body.MaxStartDate, - MaxEndDate = body.MaxEndDate, - StartIndex = body.StartIndex, - Limit = body.Limit, - OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder), - IsNews = body.IsNews, - IsMovie = body.IsMovie, - IsSeries = body.IsSeries, - IsKids = body.IsKids, - IsSports = body.IsSports, - SeriesTimerId = body.SeriesTimerId, - Genres = body.Genres, - GenreIds = body.GenreIds - }; - - if (!body.LibrarySeriesId.Equals(default)) - { - query.IsSeries = true; - - if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series) - { - query.Name = series.Name; - } - } - - var dtoOptions = new DtoOptions { Fields = body.Fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes); - return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); - } - - /// - /// Gets recommended live tv epgs. - /// - /// Optional. filter by user id. - /// Optional. The maximum number of records to return. - /// Optional. Filter by programs that are currently airing, or not. - /// Optional. Filter by programs that have completed airing, or not. - /// Optional. Filter for series. - /// Optional. Filter for movies. - /// Optional. Filter for news. - /// Optional. Filter for kids. - /// Optional. Filter for sports. - /// Optional. Include image information in output. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// The genres to return guide information for. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. include user data. - /// Retrieve total record count. - /// Recommended epgs returned. - /// A containing the queryresult of recommended epgs. - [HttpGet("Programs/Recommended")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> GetRecommendedPrograms( - [FromQuery] Guid? userId, - [FromQuery] int? limit, - [FromQuery] bool? isAiring, - [FromQuery] bool? hasAired, - [FromQuery] bool? isSeries, - [FromQuery] bool? isMovie, - [FromQuery] bool? isNews, - [FromQuery] bool? isKids, - [FromQuery] bool? isSports, - [FromQuery] bool? enableImages, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] bool? enableUserData, - [FromQuery] bool enableTotalRecordCount = true) - { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var query = new InternalItemsQuery(user) - { - IsAiring = isAiring, + ChannelId = channelId, + UserId = userId ?? Guid.Empty, + StartIndex = startIndex, Limit = limit, - HasAired = hasAired, - IsSeries = isSeries, - IsMovie = isMovie, - IsKids = isKids, - IsNews = isNews, - IsSports = isSports, + Status = status, + SeriesTimerId = seriesTimerId, + IsInProgress = isInProgress, EnableTotalRecordCount = enableTotalRecordCount, - GenreIds = genreIds - }; + IsMovie = isMovie, + IsNews = isNews, + IsSeries = isSeries, + IsKids = isKids, + IsSports = isSports, + IsLibraryItem = isLibraryItem, + Fields = fields, + ImageTypeLimit = imageTypeLimit, + EnableImages = enableImages + }, + dtoOptions); + } - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); - } + /// + /// Gets live tv recording series. + /// + /// Optional. Filter by channel id. + /// Optional. Filter by user and attach user data. + /// Optional. Filter by recording group. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Filter by recording status. + /// Optional. Filter by recordings that are in progress, or not. + /// Optional. Filter by recordings belonging to a series timer. + /// Optional. Include image information in output. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include user data. + /// Optional. Return total record count. + /// Live tv recordings returned. + /// An containing the live tv recordings. + [HttpGet("Recordings/Series")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + [Obsolete("This endpoint is obsolete.")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "groupId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "status", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isInProgress", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesTimerId", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "fields", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")] + public ActionResult> GetRecordingsSeries( + [FromQuery] string? channelId, + [FromQuery] Guid? userId, + [FromQuery] string? groupId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] RecordingStatus? status, + [FromQuery] bool? isInProgress, + [FromQuery] string? seriesTimerId, + [FromQuery] bool? enableImages, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] bool? enableUserData, + [FromQuery] bool enableTotalRecordCount = true) + { + return new QueryResult(); + } - /// - /// Gets a live tv program. - /// - /// Program id. - /// Optional. Attach user data. - /// Program returned. - /// An containing the livetv program. - [HttpGet("Programs/{programId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetProgram( - [FromRoute, Required] string programId, - [FromQuery] Guid? userId) - { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); + /// + /// Gets live tv recording groups. + /// + /// Optional. Filter by user and attach user data. + /// Recording groups returned. + /// An containing the recording groups. + [HttpGet("Recordings/Groups")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + [Obsolete("This endpoint is obsolete.")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")] + public ActionResult> GetRecordingGroups([FromQuery] Guid? userId) + { + return new QueryResult(); + } - return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false); - } + /// + /// Gets recording folders. + /// + /// Optional. Filter by user and attach user data. + /// Recording folders returned. + /// An containing the recording folders. + [HttpGet("Recordings/Folders")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult> GetRecordingFolders([FromQuery] Guid? userId) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var folders = _liveTvManager.GetRecordingFolders(user); - /// - /// Deletes a live tv recording. - /// - /// Recording id. - /// Recording deleted. - /// Item not found. - /// A on success, or a if item not found. - [HttpDelete("Recordings/{recordingId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteRecording([FromRoute, Required] Guid recordingId) - { - await AssertUserCanManageLiveTv().ConfigureAwait(false); + var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user); - var item = _libraryManager.GetItemById(recordingId); - if (item is null) + return new QueryResult(returnArray); + } + + /// + /// Gets a live tv recording. + /// + /// Recording id. + /// Optional. Attach user data. + /// Recording returned. + /// An containing the live tv recording. + [HttpGet("Recordings/{recordingId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public ActionResult GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); + + var dtoOptions = new DtoOptions() + .AddClientFields(User); + + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + /// + /// Resets a tv tuner. + /// + /// Tuner id. + /// Tuner reset. + /// A . + [HttpPost("Tuners/{tunerId}/Reset")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public async Task ResetTuner([FromRoute, Required] string tunerId) + { + await AssertUserCanManageLiveTv().ConfigureAwait(false); + await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Gets a timer. + /// + /// Timer id. + /// Timer returned. + /// + /// A containing an which contains the timer. + /// + [HttpGet("Timers/{timerId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public async Task> GetTimer([FromRoute, Required] string timerId) + { + return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Gets the default values for a new timer. + /// + /// Optional. To attach default values based on a program. + /// Default values returned. + /// + /// A containing an which contains the default values for a timer. + /// + [HttpGet("Timers/Defaults")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public async Task> GetDefaultTimer([FromQuery] string? programId) + { + return string.IsNullOrEmpty(programId) + ? await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false) + : await _liveTvManager.GetNewTimerDefaults(programId, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Gets the live tv timers. + /// + /// Optional. Filter by channel id. + /// Optional. Filter by timers belonging to a series timer. + /// Optional. Filter by timers that are active. + /// Optional. Filter by timers that are scheduled. + /// + /// A containing an which contains the live tv timers. + /// + [HttpGet("Timers")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public async Task>> GetTimers( + [FromQuery] string? channelId, + [FromQuery] string? seriesTimerId, + [FromQuery] bool? isActive, + [FromQuery] bool? isScheduled) + { + return await _liveTvManager.GetTimers( + new TimerQuery { - return NotFound(); + ChannelId = channelId, + SeriesTimerId = seriesTimerId, + IsActive = isActive, + IsScheduled = isScheduled + }, + CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Gets available live tv epgs. + /// + /// The channels to return guide information for. + /// Optional. Filter by user id. + /// Optional. The minimum premiere start date. + /// Optional. Filter by programs that have completed airing, or not. + /// Optional. Filter by programs that are currently airing, or not. + /// Optional. The maximum premiere start date. + /// Optional. The minimum premiere end date. + /// Optional. The maximum premiere end date. + /// Optional. Filter for movies. + /// Optional. Filter for series. + /// Optional. Filter for news. + /// Optional. Filter for kids. + /// Optional. Filter for sports. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Specify one or more sort orders, comma delimited. Options: Name, StartDate. + /// Sort Order - Ascending,Descending. + /// The genres to return guide information for. + /// The genre ids to return guide information for. + /// Optional. Include image information in output. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. Include user data. + /// Optional. Filter by series timer id. + /// Optional. Filter by library series id. + /// Optional. Specify additional fields of information to return in the output. + /// Retrieve total record count. + /// Live tv epgs returned. + /// + /// A containing a which contains the live tv epgs. + /// + [HttpGet("Programs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public async Task>> GetLiveTvPrograms( + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds, + [FromQuery] Guid? userId, + [FromQuery] DateTime? minStartDate, + [FromQuery] bool? hasAired, + [FromQuery] bool? isAiring, + [FromQuery] DateTime? maxStartDate, + [FromQuery] DateTime? minEndDate, + [FromQuery] DateTime? maxEndDate, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery] bool? enableImages, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] bool? enableUserData, + [FromQuery] string? seriesTimerId, + [FromQuery] Guid? librarySeriesId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] bool enableTotalRecordCount = true) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var query = new InternalItemsQuery(user) + { + ChannelIds = channelIds, + HasAired = hasAired, + IsAiring = isAiring, + EnableTotalRecordCount = enableTotalRecordCount, + MinStartDate = minStartDate, + MinEndDate = minEndDate, + MaxStartDate = maxStartDate, + MaxEndDate = maxEndDate, + StartIndex = startIndex, + Limit = limit, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), + IsNews = isNews, + IsMovie = isMovie, + IsSeries = isSeries, + IsKids = isKids, + IsSports = isSports, + SeriesTimerId = seriesTimerId, + Genres = genres, + GenreIds = genreIds + }; + + if (librarySeriesId.HasValue && !librarySeriesId.Equals(default)) + { + query.IsSeries = true; + + if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series) + { + query.Name = series.Name; } + } - _libraryManager.DeleteItem(item, new DeleteOptions + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Gets available live tv epgs. + /// + /// Request body. + /// Live tv epgs returned. + /// + /// A containing a which contains the live tv epgs. + /// + [HttpPost("Programs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.DefaultAuthorization)] + public async Task>> GetPrograms([FromBody] GetProgramsDto body) + { + var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId); + + var query = new InternalItemsQuery(user) + { + ChannelIds = body.ChannelIds, + HasAired = body.HasAired, + IsAiring = body.IsAiring, + EnableTotalRecordCount = body.EnableTotalRecordCount, + MinStartDate = body.MinStartDate, + MinEndDate = body.MinEndDate, + MaxStartDate = body.MaxStartDate, + MaxEndDate = body.MaxEndDate, + StartIndex = body.StartIndex, + Limit = body.Limit, + OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder), + IsNews = body.IsNews, + IsMovie = body.IsMovie, + IsSeries = body.IsSeries, + IsKids = body.IsKids, + IsSports = body.IsSports, + SeriesTimerId = body.SeriesTimerId, + Genres = body.Genres, + GenreIds = body.GenreIds + }; + + if (!body.LibrarySeriesId.Equals(default)) + { + query.IsSeries = true; + + if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series) { - DeleteFileLocation = false - }); - - return NoContent(); - } - - /// - /// Cancels a live tv timer. - /// - /// Timer id. - /// Timer deleted. - /// A . - [HttpDelete("Timers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task CancelTimer([FromRoute, Required] string timerId) - { - await AssertUserCanManageLiveTv().ConfigureAwait(false); - await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Updates a live tv timer. - /// - /// Timer id. - /// New timer info. - /// Timer updated. - /// A . - [HttpPost("Timers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] - public async Task UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo) - { - await AssertUserCanManageLiveTv().ConfigureAwait(false); - await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Creates a live tv timer. - /// - /// New timer info. - /// Timer created. - /// A . - [HttpPost("Timers")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task CreateTimer([FromBody] TimerInfoDto timerInfo) - { - await AssertUserCanManageLiveTv().ConfigureAwait(false); - await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Gets a live tv series timer. - /// - /// Timer id. - /// Series timer returned. - /// Series timer not found. - /// A on success, or a if timer not found. - [HttpGet("SeriesTimers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetSeriesTimer([FromRoute, Required] string timerId) - { - var timer = await _liveTvManager.GetSeriesTimer(timerId, CancellationToken.None).ConfigureAwait(false); - if (timer is null) - { - return NotFound(); + query.Name = series.Name; } - - return timer; } - /// - /// Gets live tv series timers. - /// - /// Optional. Sort by SortName or Priority. - /// Optional. Sort in Ascending or Descending order. - /// Timers returned. - /// An of live tv series timers. - [HttpGet("SeriesTimers")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder) + var dtoOptions = new DtoOptions { Fields = body.Fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes); + return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Gets recommended live tv epgs. + /// + /// Optional. filter by user id. + /// Optional. The maximum number of records to return. + /// Optional. Filter by programs that are currently airing, or not. + /// Optional. Filter by programs that have completed airing, or not. + /// Optional. Filter for series. + /// Optional. Filter for movies. + /// Optional. Filter for news. + /// Optional. Filter for kids. + /// Optional. Filter for sports. + /// Optional. Include image information in output. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// The genres to return guide information for. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. include user data. + /// Retrieve total record count. + /// Recommended epgs returned. + /// A containing the queryresult of recommended epgs. + [HttpGet("Programs/Recommended")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetRecommendedPrograms( + [FromQuery] Guid? userId, + [FromQuery] int? limit, + [FromQuery] bool? isAiring, + [FromQuery] bool? hasAired, + [FromQuery] bool? isSeries, + [FromQuery] bool? isMovie, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, + [FromQuery] bool? enableImages, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] bool? enableUserData, + [FromQuery] bool enableTotalRecordCount = true) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var query = new InternalItemsQuery(user) { - return await _liveTvManager.GetSeriesTimers( - new SeriesTimerQuery - { - SortOrder = sortOrder ?? SortOrder.Ascending, - SortBy = sortBy - }, - CancellationToken.None).ConfigureAwait(false); - } + IsAiring = isAiring, + Limit = limit, + HasAired = hasAired, + IsSeries = isSeries, + IsMovie = isMovie, + IsKids = isKids, + IsNews = isNews, + IsSports = isSports, + EnableTotalRecordCount = enableTotalRecordCount, + GenreIds = genreIds + }; - /// - /// Cancels a live tv series timer. - /// - /// Timer id. - /// Timer cancelled. - /// A . - [HttpDelete("SeriesTimers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task CancelSeriesTimer([FromRoute, Required] string timerId) - { - await AssertUserCanManageLiveTv().ConfigureAwait(false); - await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false); - return NoContent(); - } + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); + } - /// - /// Updates a live tv series timer. - /// - /// Timer id. - /// New series timer info. - /// Series timer updated. - /// A . - [HttpPost("SeriesTimers/{timerId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] - public async Task UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo) - { - await AssertUserCanManageLiveTv().ConfigureAwait(false); - await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); - return NoContent(); - } + /// + /// Gets a live tv program. + /// + /// Program id. + /// Optional. Attach user data. + /// Program returned. + /// An containing the livetv program. + [HttpGet("Programs/{programId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetProgram( + [FromRoute, Required] string programId, + [FromQuery] Guid? userId) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); - /// - /// Creates a live tv series timer. - /// - /// New series timer info. - /// Series timer info created. - /// A . - [HttpPost("SeriesTimers")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo) - { - await AssertUserCanManageLiveTv().ConfigureAwait(false); - await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); - return NoContent(); - } + return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false); + } - /// - /// Get recording group. - /// - /// Group id. - /// A . - [HttpGet("Recordings/Groups/{groupId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [Obsolete("This endpoint is obsolete.")] - public ActionResult GetRecordingGroup([FromRoute, Required] Guid groupId) + /// + /// Deletes a live tv recording. + /// + /// Recording id. + /// Recording deleted. + /// Item not found. + /// A on success, or a if item not found. + [HttpDelete("Recordings/{recordingId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteRecording([FromRoute, Required] Guid recordingId) + { + await AssertUserCanManageLiveTv().ConfigureAwait(false); + + var item = _libraryManager.GetItemById(recordingId); + if (item is null) { return NotFound(); } - /// - /// Get guid info. - /// - /// Guid info returned. - /// An containing the guide info. - [HttpGet("GuideInfo")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetGuideInfo() + _libraryManager.DeleteItem(item, new DeleteOptions { - return _liveTvManager.GetGuideInfo(); + DeleteFileLocation = false + }); + + return NoContent(); + } + + /// + /// Cancels a live tv timer. + /// + /// Timer id. + /// Timer deleted. + /// A . + [HttpDelete("Timers/{timerId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task CancelTimer([FromRoute, Required] string timerId) + { + await AssertUserCanManageLiveTv().ConfigureAwait(false); + await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Updates a live tv timer. + /// + /// Timer id. + /// New timer info. + /// Timer updated. + /// A . + [HttpPost("Timers/{timerId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] + public async Task UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo) + { + await AssertUserCanManageLiveTv().ConfigureAwait(false); + await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Creates a live tv timer. + /// + /// New timer info. + /// Timer created. + /// A . + [HttpPost("Timers")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task CreateTimer([FromBody] TimerInfoDto timerInfo) + { + await AssertUserCanManageLiveTv().ConfigureAwait(false); + await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Gets a live tv series timer. + /// + /// Timer id. + /// Series timer returned. + /// Series timer not found. + /// A on success, or a if timer not found. + [HttpGet("SeriesTimers/{timerId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetSeriesTimer([FromRoute, Required] string timerId) + { + var timer = await _liveTvManager.GetSeriesTimer(timerId, CancellationToken.None).ConfigureAwait(false); + if (timer is null) + { + return NotFound(); } - /// - /// Adds a tuner host. - /// - /// New tuner host. - /// Created tuner host returned. - /// A containing the created tuner host. - [HttpPost("TunerHosts")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) - { - return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false); - } + return timer; + } - /// - /// Deletes a tuner host. - /// - /// Tuner host id. - /// Tuner host deleted. - /// A . - [HttpDelete("TunerHosts")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult DeleteTunerHost([FromQuery] string? id) - { - var config = _configurationManager.GetConfiguration("livetv"); - config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray(); - _configurationManager.SaveConfiguration("livetv", config); - return NoContent(); - } - - /// - /// Gets default listings provider info. - /// - /// Default listings provider info returned. - /// An containing the default listings provider info. - [HttpGet("ListingProviders/Default")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetDefaultListingProvider() - { - return new ListingsProviderInfo(); - } - - /// - /// Adds a listings provider. - /// - /// Password. - /// New listings info. - /// Validate listings. - /// Validate login. - /// Created listings provider returned. - /// A containing the created listings provider. - [HttpPost("ListingProviders")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")] - public async Task> AddListingProvider( - [FromQuery] string? pw, - [FromBody] ListingsProviderInfo listingsProviderInfo, - [FromQuery] bool validateListings = false, - [FromQuery] bool validateLogin = false) - { - if (!string.IsNullOrEmpty(pw)) + /// + /// Gets live tv series timers. + /// + /// Optional. Sort by SortName or Priority. + /// Optional. Sort in Ascending or Descending order. + /// Timers returned. + /// An of live tv series timers. + [HttpGet("SeriesTimers")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder) + { + return await _liveTvManager.GetSeriesTimers( + new SeriesTimerQuery { - // TODO: remove ToLower when Convert.ToHexString supports lowercase - // Schedules Direct requires the hex to be lowercase - listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant(); - } + SortOrder = sortOrder ?? SortOrder.Ascending, + SortBy = sortBy + }, + CancellationToken.None).ConfigureAwait(false); + } - return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false); + /// + /// Cancels a live tv series timer. + /// + /// Timer id. + /// Timer cancelled. + /// A . + [HttpDelete("SeriesTimers/{timerId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task CancelSeriesTimer([FromRoute, Required] string timerId) + { + await AssertUserCanManageLiveTv().ConfigureAwait(false); + await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Updates a live tv series timer. + /// + /// Timer id. + /// New series timer info. + /// Series timer updated. + /// A . + [HttpPost("SeriesTimers/{timerId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")] + public async Task UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo) + { + await AssertUserCanManageLiveTv().ConfigureAwait(false); + await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Creates a live tv series timer. + /// + /// New series timer info. + /// Series timer info created. + /// A . + [HttpPost("SeriesTimers")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo) + { + await AssertUserCanManageLiveTv().ConfigureAwait(false); + await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Get recording group. + /// + /// Group id. + /// A . + [HttpGet("Recordings/Groups/{groupId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Obsolete("This endpoint is obsolete.")] + public ActionResult GetRecordingGroup([FromRoute, Required] Guid groupId) + { + return NotFound(); + } + + /// + /// Get guid info. + /// + /// Guid info returned. + /// An containing the guide info. + [HttpGet("GuideInfo")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetGuideInfo() + { + return _liveTvManager.GetGuideInfo(); + } + + /// + /// Adds a tuner host. + /// + /// New tuner host. + /// Created tuner host returned. + /// A containing the created tuner host. + [HttpPost("TunerHosts")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) + { + return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false); + } + + /// + /// Deletes a tuner host. + /// + /// Tuner host id. + /// Tuner host deleted. + /// A . + [HttpDelete("TunerHosts")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult DeleteTunerHost([FromQuery] string? id) + { + var config = _configurationManager.GetConfiguration("livetv"); + config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray(); + _configurationManager.SaveConfiguration("livetv", config); + return NoContent(); + } + + /// + /// Gets default listings provider info. + /// + /// Default listings provider info returned. + /// An containing the default listings provider info. + [HttpGet("ListingProviders/Default")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDefaultListingProvider() + { + return new ListingsProviderInfo(); + } + + /// + /// Adds a listings provider. + /// + /// Password. + /// New listings info. + /// Validate listings. + /// Validate login. + /// Created listings provider returned. + /// A containing the created listings provider. + [HttpPost("ListingProviders")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")] + public async Task> AddListingProvider( + [FromQuery] string? pw, + [FromBody] ListingsProviderInfo listingsProviderInfo, + [FromQuery] bool validateListings = false, + [FromQuery] bool validateLogin = false) + { + if (!string.IsNullOrEmpty(pw)) + { + // TODO: remove ToLower when Convert.ToHexString supports lowercase + // Schedules Direct requires the hex to be lowercase + listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant(); } - /// - /// Delete listing provider. - /// - /// Listing provider id. - /// Listing provider deleted. - /// A . - [HttpDelete("ListingProviders")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult DeleteListingProvider([FromQuery] string? id) + return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false); + } + + /// + /// Delete listing provider. + /// + /// Listing provider id. + /// Listing provider deleted. + /// A . + [HttpDelete("ListingProviders")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult DeleteListingProvider([FromQuery] string? id) + { + _liveTvManager.DeleteListingsProvider(id); + return NoContent(); + } + + /// + /// Gets available lineups. + /// + /// Provider id. + /// Provider type. + /// Location. + /// Country. + /// Available lineups returned. + /// A containing the available lineups. + [HttpGet("ListingProviders/Lineups")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetLineups( + [FromQuery] string? id, + [FromQuery] string? type, + [FromQuery] string? location, + [FromQuery] string? country) + { + return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false); + } + + /// + /// Gets available countries. + /// + /// Available countries returned. + /// A containing the available countries. + [HttpGet("ListingProviders/SchedulesDirect/Countries")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile(MediaTypeNames.Application.Json)] + public async Task GetSchedulesDirectCountries() + { + var client = _httpClientFactory.CreateClient(NamedClient.Default); + // https://json.schedulesdirect.org/20141201/available/countries + // Can't dispose the response as it's required up the call chain. + var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries")) + .ConfigureAwait(false); + + return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json); + } + + /// + /// Get channel mapping options. + /// + /// Provider id. + /// Channel mapping options returned. + /// An containing the channel mapping options. + [HttpGet("ChannelMappingOptions")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetChannelMappingOptions([FromQuery] string? providerId) + { + var config = _configurationManager.GetConfiguration("livetv"); + + var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase)); + + var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name; + + var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None) + .ConfigureAwait(false); + + var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None) + .ConfigureAwait(false); + + var mappings = listingsProviderInfo.ChannelMappings; + + return new ChannelMappingOptionsDto { - _liveTvManager.DeleteListingsProvider(id); - return NoContent(); - } - - /// - /// Gets available lineups. - /// - /// Provider id. - /// Provider type. - /// Location. - /// Country. - /// Available lineups returned. - /// A containing the available lineups. - [HttpGet("ListingProviders/Lineups")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> GetLineups( - [FromQuery] string? id, - [FromQuery] string? type, - [FromQuery] string? location, - [FromQuery] string? country) - { - return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false); - } - - /// - /// Gets available countries. - /// - /// Available countries returned. - /// A containing the available countries. - [HttpGet("ListingProviders/SchedulesDirect/Countries")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesFile(MediaTypeNames.Application.Json)] - public async Task GetSchedulesDirectCountries() - { - var client = _httpClientFactory.CreateClient(NamedClient.Default); - // https://json.schedulesdirect.org/20141201/available/countries - // Can't dispose the response as it's required up the call chain. - var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries")) - .ConfigureAwait(false); - - return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json); - } - - /// - /// Get channel mapping options. - /// - /// Provider id. - /// Channel mapping options returned. - /// An containing the channel mapping options. - [HttpGet("ChannelMappingOptions")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetChannelMappingOptions([FromQuery] string? providerId) - { - var config = _configurationManager.GetConfiguration("livetv"); - - var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase)); - - var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name; - - var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None) - .ConfigureAwait(false); - - var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None) - .ConfigureAwait(false); - - var mappings = listingsProviderInfo.ChannelMappings; - - return new ChannelMappingOptionsDto + TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(), + ProviderChannels = providerChannels.Select(i => new NameIdPair { - TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(), - ProviderChannels = providerChannels.Select(i => new NameIdPair - { - Name = i.Name, - Id = i.Id - }).ToList(), - Mappings = mappings, - ProviderName = listingsProviderName - }; + Name = i.Name, + Id = i.Id + }).ToList(), + Mappings = mappings, + ProviderName = listingsProviderName + }; + } + + /// + /// Set channel mappings. + /// + /// The set channel mapping dto. + /// Created channel mapping returned. + /// An containing the created channel mapping. + [HttpPost("ChannelMappings")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto) + { + return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false); + } + + /// + /// Get tuner host types. + /// + /// Tuner host types returned. + /// An containing the tuner host types. + [HttpGet("TunerHosts/Types")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetTunerHostTypes() + { + return _liveTvManager.GetTunerHostTypes(); + } + + /// + /// Discover tuners. + /// + /// Only discover new tuners. + /// Tuners returned. + /// An containing the tuners. + [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")] + [HttpGet("Tuners/Discover")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> DiscoverTuners([FromQuery] bool newDevicesOnly = false) + { + return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Gets a live tv recording stream. + /// + /// Recording id. + /// Recording stream returned. + /// Recording not found. + /// + /// An containing the recording stream on success, + /// or a if recording not found. + /// + [HttpGet("LiveRecordings/{recordingId}/stream")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesVideoFile] + public ActionResult GetLiveRecordingFile([FromRoute, Required] string recordingId) + { + var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId); + + if (string.IsNullOrWhiteSpace(path)) + { + return NotFound(); } - /// - /// Set channel mappings. - /// - /// The set channel mapping dto. - /// Created channel mapping returned. - /// An containing the created channel mapping. - [HttpPost("ChannelMappings")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto) + var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper); + return new FileStreamResult(stream, MimeTypes.GetMimeType(path)); + } + + /// + /// Gets a live tv channel stream. + /// + /// Stream id. + /// Container type. + /// Stream returned. + /// Stream not found. + /// + /// An containing the channel stream on success, + /// or a if stream not found. + /// + [HttpGet("LiveStreamFiles/{streamId}/stream.{container}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesVideoFile] + public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container) + { + var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId); + if (liveStreamInfo is null) { - return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false); + return NotFound(); } - /// - /// Get tuner host types. - /// - /// Tuner host types returned. - /// An containing the tuner host types. - [HttpGet("TunerHosts/Types")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetTunerHostTypes() + var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream()); + return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container)); + } + + private async Task AssertUserCanManageLiveTv() + { + var user = _userManager.GetUserById(User.GetUserId()); + var session = await _sessionManager.LogSessionActivity( + User.GetClient(), + User.GetVersion(), + User.GetDeviceId(), + User.GetDevice(), + HttpContext.GetNormalizedRemoteIp().ToString(), + user).ConfigureAwait(false); + + if (session.UserId.Equals(default)) { - return _liveTvManager.GetTunerHostTypes(); + throw new SecurityException("Anonymous live tv management is not allowed."); } - /// - /// Discover tuners. - /// - /// Only discover new tuners. - /// Tuners returned. - /// An containing the tuners. - [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")] - [HttpGet("Tuners/Discover")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> DiscoverTuners([FromQuery] bool newDevicesOnly = false) + if (!user.HasPermission(PermissionKind.EnableLiveTvManagement)) { - return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false); - } - - /// - /// Gets a live tv recording stream. - /// - /// Recording id. - /// Recording stream returned. - /// Recording not found. - /// - /// An containing the recording stream on success, - /// or a if recording not found. - /// - [HttpGet("LiveRecordings/{recordingId}/stream")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesVideoFile] - public ActionResult GetLiveRecordingFile([FromRoute, Required] string recordingId) - { - var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId); - - if (string.IsNullOrWhiteSpace(path)) - { - return NotFound(); - } - - var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper); - return new FileStreamResult(stream, MimeTypes.GetMimeType(path)); - } - - /// - /// Gets a live tv channel stream. - /// - /// Stream id. - /// Container type. - /// Stream returned. - /// Stream not found. - /// - /// An containing the channel stream on success, - /// or a if stream not found. - /// - [HttpGet("LiveStreamFiles/{streamId}/stream.{container}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesVideoFile] - public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container) - { - var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId); - if (liveStreamInfo is null) - { - return NotFound(); - } - - var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream()); - return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container)); - } - - private async Task AssertUserCanManageLiveTv() - { - var user = _userManager.GetUserById(User.GetUserId()); - var session = await _sessionManager.LogSessionActivity( - User.GetClient(), - User.GetVersion(), - User.GetDeviceId(), - User.GetDevice(), - HttpContext.GetNormalizedRemoteIp().ToString(), - user).ConfigureAwait(false); - - if (session.UserId.Equals(default)) - { - throw new SecurityException("Anonymous live tv management is not allowed."); - } - - if (!user.HasPermission(PermissionKind.EnableLiveTvManagement)) - { - throw new SecurityException("The current user does not have permission to manage live tv."); - } + throw new SecurityException("The current user does not have permission to manage live tv."); } } } diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs index 3d8b9e0cac..b9772a0693 100644 --- a/Jellyfin.Api/Controllers/LocalizationController.cs +++ b/Jellyfin.Api/Controllers/LocalizationController.cs @@ -6,71 +6,70 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Localization controller. +/// +[Authorize(Policy = Policies.FirstTimeSetupOrDefault)] +public class LocalizationController : BaseJellyfinApiController { + private readonly ILocalizationManager _localization; + /// - /// Localization controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] - public class LocalizationController : BaseJellyfinApiController + /// Instance of the interface. + public LocalizationController(ILocalizationManager localization) { - private readonly ILocalizationManager _localization; + _localization = localization; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - public LocalizationController(ILocalizationManager localization) - { - _localization = localization; - } + /// + /// Gets known cultures. + /// + /// Known cultures returned. + /// An containing the list of cultures. + [HttpGet("Cultures")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetCultures() + { + return Ok(_localization.GetCultures()); + } - /// - /// Gets known cultures. - /// - /// Known cultures returned. - /// An containing the list of cultures. - [HttpGet("Cultures")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetCultures() - { - return Ok(_localization.GetCultures()); - } + /// + /// Gets known countries. + /// + /// Known countries returned. + /// An containing the list of countries. + [HttpGet("Countries")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetCountries() + { + return Ok(_localization.GetCountries()); + } - /// - /// Gets known countries. - /// - /// Known countries returned. - /// An containing the list of countries. - [HttpGet("Countries")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetCountries() - { - return Ok(_localization.GetCountries()); - } + /// + /// Gets known parental ratings. + /// + /// Known parental ratings returned. + /// An containing the list of parental ratings. + [HttpGet("ParentalRatings")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetParentalRatings() + { + return Ok(_localization.GetParentalRatings()); + } - /// - /// Gets known parental ratings. - /// - /// Known parental ratings returned. - /// An containing the list of parental ratings. - [HttpGet("ParentalRatings")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetParentalRatings() - { - return Ok(_localization.GetParentalRatings()); - } - - /// - /// Gets localization options. - /// - /// Localization options returned. - /// An containing the list of localization options. - [HttpGet("Options")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetLocalizationOptions() - { - return Ok(_localization.GetLocalizationOptions()); - } + /// + /// Gets localization options. + /// + /// Localization options returned. + /// An containing the list of localization options. + [HttpGet("Options")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetLocalizationOptions() + { + return Ok(_localization.GetLocalizationOptions()); } } diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index 8115c35852..eee7df3af8 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -19,295 +19,294 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The media info controller. +/// +[Route("")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class MediaInfoController : BaseJellyfinApiController { + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IDeviceManager _deviceManager; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly MediaInfoHelper _mediaInfoHelper; + /// - /// The media info controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class MediaInfoController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the . + public MediaInfoController( + IMediaSourceManager mediaSourceManager, + IDeviceManager deviceManager, + ILibraryManager libraryManager, + ILogger logger, + MediaInfoHelper mediaInfoHelper) { - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IDeviceManager _deviceManager; - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly MediaInfoHelper _mediaInfoHelper; + _mediaSourceManager = mediaSourceManager; + _deviceManager = deviceManager; + _libraryManager = libraryManager; + _logger = logger; + _mediaInfoHelper = mediaInfoHelper; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the . - public MediaInfoController( - IMediaSourceManager mediaSourceManager, - IDeviceManager deviceManager, - ILibraryManager libraryManager, - ILogger logger, - MediaInfoHelper mediaInfoHelper) + /// + /// Gets live playback media info for an item. + /// + /// The item id. + /// The user id. + /// Playback info returned. + /// A containing a with the playback information. + [HttpGet("Items/{itemId}/PlaybackInfo")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId) + { + return await _mediaInfoHelper.GetPlaybackInfo( + itemId, + userId) + .ConfigureAwait(false); + } + + /// + /// Gets live playback media info for an item. + /// + /// + /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. + /// Query parameters are obsolete. + /// + /// The item id. + /// The user id. + /// The maximum streaming bitrate. + /// The start time in ticks. + /// The audio stream index. + /// The subtitle stream index. + /// The maximum number of audio channels. + /// The media source id. + /// The livestream id. + /// Whether to auto open the livestream. + /// Whether to enable direct play. Default: true. + /// Whether to enable direct stream. Default: true. + /// Whether to enable transcoding. Default: true. + /// Whether to allow to copy the video stream. Default: true. + /// Whether to allow to copy the audio stream. Default: true. + /// The playback info. + /// Playback info returned. + /// A containing a with the playback info. + [HttpPost("Items/{itemId}/PlaybackInfo")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetPostedPlaybackInfo( + [FromRoute, Required] Guid itemId, + [FromQuery, ParameterObsolete] Guid? userId, + [FromQuery, ParameterObsolete] int? maxStreamingBitrate, + [FromQuery, ParameterObsolete] long? startTimeTicks, + [FromQuery, ParameterObsolete] int? audioStreamIndex, + [FromQuery, ParameterObsolete] int? subtitleStreamIndex, + [FromQuery, ParameterObsolete] int? maxAudioChannels, + [FromQuery, ParameterObsolete] string? mediaSourceId, + [FromQuery, ParameterObsolete] string? liveStreamId, + [FromQuery, ParameterObsolete] bool? autoOpenLiveStream, + [FromQuery, ParameterObsolete] bool? enableDirectPlay, + [FromQuery, ParameterObsolete] bool? enableDirectStream, + [FromQuery, ParameterObsolete] bool? enableTranscoding, + [FromQuery, ParameterObsolete] bool? allowVideoStreamCopy, + [FromQuery, ParameterObsolete] bool? allowAudioStreamCopy, + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto) + { + var profile = playbackInfoDto?.DeviceProfile; + _logger.LogDebug("GetPostedPlaybackInfo profile: {@Profile}", profile); + + if (profile is null) { - _mediaSourceManager = mediaSourceManager; - _deviceManager = deviceManager; - _libraryManager = libraryManager; - _logger = logger; - _mediaInfoHelper = mediaInfoHelper; + var caps = _deviceManager.GetCapabilities(User.GetDeviceId()); + if (caps is not null) + { + profile = caps.DeviceProfile; + } } - /// - /// Gets live playback media info for an item. - /// - /// The item id. - /// The user id. - /// Playback info returned. - /// A containing a with the playback information. - [HttpGet("Items/{itemId}/PlaybackInfo")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId) + // Copy params from posted body + // TODO clean up when breaking API compatibility. + userId ??= playbackInfoDto?.UserId; + maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate; + startTimeTicks ??= playbackInfoDto?.StartTimeTicks; + audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex; + subtitleStreamIndex ??= playbackInfoDto?.SubtitleStreamIndex; + maxAudioChannels ??= playbackInfoDto?.MaxAudioChannels; + mediaSourceId ??= playbackInfoDto?.MediaSourceId; + liveStreamId ??= playbackInfoDto?.LiveStreamId; + autoOpenLiveStream ??= playbackInfoDto?.AutoOpenLiveStream ?? false; + enableDirectPlay ??= playbackInfoDto?.EnableDirectPlay ?? true; + enableDirectStream ??= playbackInfoDto?.EnableDirectStream ?? true; + enableTranscoding ??= playbackInfoDto?.EnableTranscoding ?? true; + allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true; + allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? true; + + var info = await _mediaInfoHelper.GetPlaybackInfo( + itemId, + userId, + mediaSourceId, + liveStreamId) + .ConfigureAwait(false); + + if (info.ErrorCode is not null) { - return await _mediaInfoHelper.GetPlaybackInfo( - itemId, - userId) - .ConfigureAwait(false); - } - - /// - /// Gets live playback media info for an item. - /// - /// - /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. - /// Query parameters are obsolete. - /// - /// The item id. - /// The user id. - /// The maximum streaming bitrate. - /// The start time in ticks. - /// The audio stream index. - /// The subtitle stream index. - /// The maximum number of audio channels. - /// The media source id. - /// The livestream id. - /// Whether to auto open the livestream. - /// Whether to enable direct play. Default: true. - /// Whether to enable direct stream. Default: true. - /// Whether to enable transcoding. Default: true. - /// Whether to allow to copy the video stream. Default: true. - /// Whether to allow to copy the audio stream. Default: true. - /// The playback info. - /// Playback info returned. - /// A containing a with the playback info. - [HttpPost("Items/{itemId}/PlaybackInfo")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetPostedPlaybackInfo( - [FromRoute, Required] Guid itemId, - [FromQuery, ParameterObsolete] Guid? userId, - [FromQuery, ParameterObsolete] int? maxStreamingBitrate, - [FromQuery, ParameterObsolete] long? startTimeTicks, - [FromQuery, ParameterObsolete] int? audioStreamIndex, - [FromQuery, ParameterObsolete] int? subtitleStreamIndex, - [FromQuery, ParameterObsolete] int? maxAudioChannels, - [FromQuery, ParameterObsolete] string? mediaSourceId, - [FromQuery, ParameterObsolete] string? liveStreamId, - [FromQuery, ParameterObsolete] bool? autoOpenLiveStream, - [FromQuery, ParameterObsolete] bool? enableDirectPlay, - [FromQuery, ParameterObsolete] bool? enableDirectStream, - [FromQuery, ParameterObsolete] bool? enableTranscoding, - [FromQuery, ParameterObsolete] bool? allowVideoStreamCopy, - [FromQuery, ParameterObsolete] bool? allowAudioStreamCopy, - [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto) - { - var profile = playbackInfoDto?.DeviceProfile; - _logger.LogDebug("GetPostedPlaybackInfo profile: {@Profile}", profile); - - if (profile is null) - { - var caps = _deviceManager.GetCapabilities(User.GetDeviceId()); - if (caps is not null) - { - profile = caps.DeviceProfile; - } - } - - // Copy params from posted body - // TODO clean up when breaking API compatibility. - userId ??= playbackInfoDto?.UserId; - maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate; - startTimeTicks ??= playbackInfoDto?.StartTimeTicks; - audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex; - subtitleStreamIndex ??= playbackInfoDto?.SubtitleStreamIndex; - maxAudioChannels ??= playbackInfoDto?.MaxAudioChannels; - mediaSourceId ??= playbackInfoDto?.MediaSourceId; - liveStreamId ??= playbackInfoDto?.LiveStreamId; - autoOpenLiveStream ??= playbackInfoDto?.AutoOpenLiveStream ?? false; - enableDirectPlay ??= playbackInfoDto?.EnableDirectPlay ?? true; - enableDirectStream ??= playbackInfoDto?.EnableDirectStream ?? true; - enableTranscoding ??= playbackInfoDto?.EnableTranscoding ?? true; - allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true; - allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? true; - - var info = await _mediaInfoHelper.GetPlaybackInfo( - itemId, - userId, - mediaSourceId, - liveStreamId) - .ConfigureAwait(false); - - if (info.ErrorCode is not null) - { - return info; - } - - if (profile is not null) - { - // set device specific data - var item = _libraryManager.GetItemById(itemId); - - foreach (var mediaSource in info.MediaSources) - { - _mediaInfoHelper.SetDeviceSpecificData( - item, - mediaSource, - profile, - User, - maxStreamingBitrate ?? profile.MaxStreamingBitrate, - startTimeTicks ?? 0, - mediaSourceId ?? string.Empty, - audioStreamIndex, - subtitleStreamIndex, - maxAudioChannels, - info.PlaySessionId!, - userId ?? Guid.Empty, - enableDirectPlay.Value, - enableDirectStream.Value, - enableTranscoding.Value, - allowVideoStreamCopy.Value, - allowAudioStreamCopy.Value, - Request.HttpContext.GetNormalizedRemoteIp()); - } - - _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); - } - - if (autoOpenLiveStream.Value) - { - var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal)); - - if (mediaSource is not null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId)) - { - var openStreamResult = await _mediaInfoHelper.OpenMediaSource( - HttpContext, - new LiveStreamRequest - { - AudioStreamIndex = audioStreamIndex, - DeviceProfile = playbackInfoDto?.DeviceProfile, - EnableDirectPlay = enableDirectPlay.Value, - EnableDirectStream = enableDirectStream.Value, - ItemId = itemId, - MaxAudioChannels = maxAudioChannels, - MaxStreamingBitrate = maxStreamingBitrate, - PlaySessionId = info.PlaySessionId, - StartTimeTicks = startTimeTicks, - SubtitleStreamIndex = subtitleStreamIndex, - UserId = userId ?? Guid.Empty, - OpenToken = mediaSource.OpenToken - }).ConfigureAwait(false); - - info.MediaSources = new[] { openStreamResult.MediaSource }; - } - } - return info; } - /// - /// Opens a media source. - /// - /// The open token. - /// The user id. - /// The play session id. - /// The maximum streaming bitrate. - /// The start time in ticks. - /// The audio stream index. - /// The subtitle stream index. - /// The maximum number of audio channels. - /// The item id. - /// The open live stream dto. - /// Whether to enable direct play. Default: true. - /// Whether to enable direct stream. Default: true. - /// Media source opened. - /// A containing a . - [HttpPost("LiveStreams/Open")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> OpenLiveStream( - [FromQuery] string? openToken, - [FromQuery] Guid? userId, - [FromQuery] string? playSessionId, - [FromQuery] int? maxStreamingBitrate, - [FromQuery] long? startTimeTicks, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] int? maxAudioChannels, - [FromQuery] Guid? itemId, - [FromBody] OpenLiveStreamDto? openLiveStreamDto, - [FromQuery] bool? enableDirectPlay, - [FromQuery] bool? enableDirectStream) + if (profile is not null) { - var request = new LiveStreamRequest + // set device specific data + var item = _libraryManager.GetItemById(itemId); + + foreach (var mediaSource in info.MediaSources) { - OpenToken = openToken ?? openLiveStreamDto?.OpenToken, - UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty, - PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId, - MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate, - StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks, - AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex, - SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex, - MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels, - ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty, - DeviceProfile = openLiveStreamDto?.DeviceProfile, - EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true, - EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true, - DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http } - }; - return await _mediaInfoHelper.OpenMediaSource(HttpContext, request).ConfigureAwait(false); + _mediaInfoHelper.SetDeviceSpecificData( + item, + mediaSource, + profile, + User, + maxStreamingBitrate ?? profile.MaxStreamingBitrate, + startTimeTicks ?? 0, + mediaSourceId ?? string.Empty, + audioStreamIndex, + subtitleStreamIndex, + maxAudioChannels, + info.PlaySessionId!, + userId ?? Guid.Empty, + enableDirectPlay.Value, + enableDirectStream.Value, + enableTranscoding.Value, + allowVideoStreamCopy.Value, + allowAudioStreamCopy.Value, + Request.HttpContext.GetNormalizedRemoteIp()); + } + + _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); } - /// - /// Closes a media source. - /// - /// The livestream id. - /// Livestream closed. - /// A indicating success. - [HttpPost("LiveStreams/Close")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task CloseLiveStream([FromQuery, Required] string liveStreamId) + if (autoOpenLiveStream.Value) { - await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false); - return NoContent(); + var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal)); + + if (mediaSource is not null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId)) + { + var openStreamResult = await _mediaInfoHelper.OpenMediaSource( + HttpContext, + new LiveStreamRequest + { + AudioStreamIndex = audioStreamIndex, + DeviceProfile = playbackInfoDto?.DeviceProfile, + EnableDirectPlay = enableDirectPlay.Value, + EnableDirectStream = enableDirectStream.Value, + ItemId = itemId, + MaxAudioChannels = maxAudioChannels, + MaxStreamingBitrate = maxStreamingBitrate, + PlaySessionId = info.PlaySessionId, + StartTimeTicks = startTimeTicks, + SubtitleStreamIndex = subtitleStreamIndex, + UserId = userId ?? Guid.Empty, + OpenToken = mediaSource.OpenToken + }).ConfigureAwait(false); + + info.MediaSources = new[] { openStreamResult.MediaSource }; + } } - /// - /// Tests the network with a request with the size of the bitrate. - /// - /// The bitrate. Defaults to 102400. - /// Test buffer returned. - /// A with specified bitrate. - [HttpGet("Playback/BitrateTest")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesFile(MediaTypeNames.Application.Octet)] - 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) + return info; + } + + /// + /// Opens a media source. + /// + /// The open token. + /// The user id. + /// The play session id. + /// The maximum streaming bitrate. + /// The start time in ticks. + /// The audio stream index. + /// The subtitle stream index. + /// The maximum number of audio channels. + /// The item id. + /// The open live stream dto. + /// Whether to enable direct play. Default: true. + /// Whether to enable direct stream. Default: true. + /// Media source opened. + /// A containing a . + [HttpPost("LiveStreams/Open")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> OpenLiveStream( + [FromQuery] string? openToken, + [FromQuery] Guid? userId, + [FromQuery] string? playSessionId, + [FromQuery] int? maxStreamingBitrate, + [FromQuery] long? startTimeTicks, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] int? maxAudioChannels, + [FromQuery] Guid? itemId, + [FromBody] OpenLiveStreamDto? openLiveStreamDto, + [FromQuery] bool? enableDirectPlay, + [FromQuery] bool? enableDirectStream) + { + var request = new LiveStreamRequest { - byte[] buffer = ArrayPool.Shared.Rent(size); - try - { - Random.Shared.NextBytes(buffer); - return File(buffer, MediaTypeNames.Application.Octet); - } - finally - { - ArrayPool.Shared.Return(buffer); - } + OpenToken = openToken ?? openLiveStreamDto?.OpenToken, + UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty, + PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId, + MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate, + StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks, + AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex, + SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex, + MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels, + ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty, + DeviceProfile = openLiveStreamDto?.DeviceProfile, + EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true, + EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true, + DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http } + }; + return await _mediaInfoHelper.OpenMediaSource(HttpContext, request).ConfigureAwait(false); + } + + /// + /// Closes a media source. + /// + /// The livestream id. + /// Livestream closed. + /// A indicating success. + [HttpPost("LiveStreams/Close")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task CloseLiveStream([FromQuery, Required] string liveStreamId) + { + await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Tests the network with a request with the size of the bitrate. + /// + /// The bitrate. Defaults to 102400. + /// Test buffer returned. + /// A with specified bitrate. + [HttpGet("Playback/BitrateTest")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile(MediaTypeNames.Application.Octet)] + 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) + { + byte[] buffer = ArrayPool.Shared.Rent(size); + try + { + Random.Shared.NextBytes(buffer); + return File(buffer, MediaTypeNames.Application.Octet); + } + finally + { + ArrayPool.Shared.Return(buffer); } } } diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 3cf079362b..4c30dd2b37 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -18,122 +18,122 @@ using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers -{ - /// - /// Movies controller. - /// - [Authorize(Policy = Policies.DefaultAuthorization)] - public class MoviesController : BaseJellyfinApiController - { - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly IDtoService _dtoService; - private readonly IServerConfigurationManager _serverConfigurationManager; +namespace Jellyfin.Api.Controllers; - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public MoviesController( - IUserManager userManager, - ILibraryManager libraryManager, - IDtoService dtoService, - IServerConfigurationManager serverConfigurationManager) +/// +/// Movies controller. +/// +[Authorize(Policy = Policies.DefaultAuthorization)] +public class MoviesController : BaseJellyfinApiController +{ + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public MoviesController( + IUserManager userManager, + ILibraryManager libraryManager, + IDtoService dtoService, + IServerConfigurationManager serverConfigurationManager) + { + _userManager = userManager; + _libraryManager = libraryManager; + _dtoService = dtoService; + _serverConfigurationManager = serverConfigurationManager; + } + + /// + /// Gets movie recommendations. + /// + /// Optional. Filter by user id, and attach user data. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. The fields to return. + /// The max number of categories to return. + /// The max number of items to return per category. + /// Movie recommendations returned. + /// The list of movie recommendations. + [HttpGet("Recommendations")] + public ActionResult> GetMovieRecommendations( + [FromQuery] Guid? userId, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] int categoryLimit = 5, + [FromQuery] int itemLimit = 8) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User); + + var categories = new List(); + + var parentIdGuid = parentId ?? Guid.Empty; + + var query = new InternalItemsQuery(user) { - _userManager = userManager; - _libraryManager = libraryManager; - _dtoService = dtoService; - _serverConfigurationManager = serverConfigurationManager; + IncludeItemTypes = new[] + { + BaseItemKind.Movie, + // nameof(Trailer), + // nameof(LiveTvProgram) + }, + // IsMovie = true + OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) }, + Limit = 7, + ParentId = parentIdGuid, + Recursive = true, + IsPlayed = true, + DtoOptions = dtoOptions + }; + + var recentlyPlayedMovies = _libraryManager.GetItemList(query); + + var itemTypes = new List { BaseItemKind.Movie }; + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); } - /// - /// Gets movie recommendations. - /// - /// Optional. Filter by user id, and attach user data. - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. The fields to return. - /// The max number of categories to return. - /// The max number of items to return per category. - /// Movie recommendations returned. - /// The list of movie recommendations. - [HttpGet("Recommendations")] - public ActionResult> GetMovieRecommendations( - [FromQuery] Guid? userId, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] int categoryLimit = 5, - [FromQuery] int itemLimit = 8) + var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user) { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User); + IncludeItemTypes = itemTypes.ToArray(), + IsMovie = true, + OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, + Limit = 10, + IsFavoriteOrLiked = true, + ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(), + EnableGroupByMetadataKey = true, + ParentId = parentIdGuid, + Recursive = true, + DtoOptions = dtoOptions + }); - var categories = new List(); + var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6)); + // Get recently played directors + var recentDirectors = GetDirectors(mostRecentMovies) + .ToList(); - var parentIdGuid = parentId ?? Guid.Empty; + // Get recently played actors + var recentActors = GetActors(mostRecentMovies) + .ToList(); - var query = new InternalItemsQuery(user) - { - IncludeItemTypes = new[] - { - BaseItemKind.Movie, - // nameof(Trailer), - // nameof(LiveTvProgram) - }, - // IsMovie = true - OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) }, - Limit = 7, - ParentId = parentIdGuid, - Recursive = true, - IsPlayed = true, - DtoOptions = dtoOptions - }; + var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator(); + var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator(); - var recentlyPlayedMovies = _libraryManager.GetItemList(query); + var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator(); + var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator(); - var itemTypes = new List { BaseItemKind.Movie }; - if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) - { - itemTypes.Add(BaseItemKind.Trailer); - itemTypes.Add(BaseItemKind.LiveTvProgram); - } - - var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user) - { - IncludeItemTypes = itemTypes.ToArray(), - IsMovie = true, - OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, - Limit = 10, - IsFavoriteOrLiked = true, - ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(), - EnableGroupByMetadataKey = true, - ParentId = parentIdGuid, - Recursive = true, - DtoOptions = dtoOptions - }); - - var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6)); - // Get recently played directors - var recentDirectors = GetDirectors(mostRecentMovies) - .ToList(); - - // Get recently played actors - var recentActors = GetActors(mostRecentMovies) - .ToList(); - - var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator(); - var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator(); - - var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator(); - var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator(); - - var categoryTypes = new List> + var categoryTypes = new List> { // Give this extra weight similarToRecentlyPlayed, @@ -146,181 +146,180 @@ namespace Jellyfin.Api.Controllers hasActorFromRecentlyPlayed }; - while (categories.Count < categoryLimit) + while (categories.Count < categoryLimit) + { + var allEmpty = true; + + foreach (var category in categoryTypes) { - var allEmpty = true; - - foreach (var category in categoryTypes) + if (category.MoveNext()) { - if (category.MoveNext()) - { - categories.Add(category.Current); - allEmpty = false; + categories.Add(category.Current); + allEmpty = false; - if (categories.Count >= categoryLimit) - { - break; - } + if (categories.Count >= categoryLimit) + { + break; } } - - if (allEmpty) - { - break; - } } - return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable()); - } - - private IEnumerable GetWithDirector( - User? user, - IEnumerable names, - int itemLimit, - DtoOptions dtoOptions, - RecommendationType type) - { - var itemTypes = new List { BaseItemKind.Movie }; - if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + if (allEmpty) { - itemTypes.Add(BaseItemKind.Trailer); - itemTypes.Add(BaseItemKind.LiveTvProgram); - } - - foreach (var name in names) - { - var items = _libraryManager.GetItemList( - new InternalItemsQuery(user) - { - Person = name, - // Account for duplicates by IMDb id, since the database doesn't support this yet - Limit = itemLimit + 2, - PersonTypes = new[] { PersonType.Director }, - IncludeItemTypes = itemTypes.ToArray(), - IsMovie = true, - EnableGroupByMetadataKey = true, - DtoOptions = dtoOptions - }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) - .Take(itemLimit) - .ToList(); - - if (items.Count > 0) - { - var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); - - yield return new RecommendationDto - { - BaselineItemName = name, - CategoryId = name.GetMD5(), - RecommendationType = type, - Items = returnItems - }; - } + break; } } - private IEnumerable GetWithActor(User? user, IEnumerable names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) - { - var itemTypes = new List { BaseItemKind.Movie }; - if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) - { - itemTypes.Add(BaseItemKind.Trailer); - itemTypes.Add(BaseItemKind.LiveTvProgram); - } + return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable()); + } - foreach (var name in names) - { - var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + private IEnumerable GetWithDirector( + User? user, + IEnumerable names, + int itemLimit, + DtoOptions dtoOptions, + RecommendationType type) + { + var itemTypes = new List { BaseItemKind.Movie }; + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); + } + + foreach (var name in names) + { + var items = _libraryManager.GetItemList( + new InternalItemsQuery(user) { Person = name, // Account for duplicates by IMDb id, since the database doesn't support this yet Limit = itemLimit + 2, + PersonTypes = new[] { PersonType.Director }, IncludeItemTypes = itemTypes.ToArray(), IsMovie = true, EnableGroupByMetadataKey = true, DtoOptions = dtoOptions }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) - .Take(itemLimit) - .ToList(); + .Take(itemLimit) + .ToList(); - if (items.Count > 0) - { - var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); - - yield return new RecommendationDto - { - BaselineItemName = name, - CategoryId = name.GetMD5(), - RecommendationType = type, - Items = returnItems - }; - } - } - } - - private IEnumerable GetSimilarTo(User? user, IEnumerable baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) - { - var itemTypes = new List { BaseItemKind.Movie }; - if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + if (items.Count > 0) { - itemTypes.Add(BaseItemKind.Trailer); - itemTypes.Add(BaseItemKind.LiveTvProgram); - } + var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); - foreach (var item in baselineItems) - { - var similar = _libraryManager.GetItemList(new InternalItemsQuery(user) + yield return new RecommendationDto { - Limit = itemLimit, - IncludeItemTypes = itemTypes.ToArray(), - IsMovie = true, - SimilarTo = item, - EnableGroupByMetadataKey = true, - DtoOptions = dtoOptions - }); - - if (similar.Count > 0) - { - var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user); - - yield return new RecommendationDto - { - BaselineItemName = item.Name, - CategoryId = item.Id, - RecommendationType = type, - Items = returnItems - }; - } + BaselineItemName = name, + CategoryId = name.GetMD5(), + RecommendationType = type, + Items = returnItems + }; } } - - private IEnumerable GetActors(IEnumerable items) - { - var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty(), new[] { PersonType.Director }) - { - MaxListOrder = 3 - }); - - var itemIds = items.Select(i => i.Id).ToList(); - - return people - .Where(i => itemIds.Contains(i.ItemId)) - .Select(i => i.Name) - .DistinctNames(); - } - - private IEnumerable GetDirectors(IEnumerable items) - { - var people = _libraryManager.GetPeople(new InternalPeopleQuery( - new[] { PersonType.Director }, - Array.Empty())); - - var itemIds = items.Select(i => i.Id).ToList(); - - return people - .Where(i => itemIds.Contains(i.ItemId)) - .Select(i => i.Name) - .DistinctNames(); - } + } + + private IEnumerable GetWithActor(User? user, IEnumerable names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) + { + var itemTypes = new List { BaseItemKind.Movie }; + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); + } + + foreach (var name in names) + { + var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + Person = name, + // Account for duplicates by IMDb id, since the database doesn't support this yet + Limit = itemLimit + 2, + IncludeItemTypes = itemTypes.ToArray(), + IsMovie = true, + EnableGroupByMetadataKey = true, + DtoOptions = dtoOptions + }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) + .Take(itemLimit) + .ToList(); + + if (items.Count > 0) + { + var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user); + + yield return new RecommendationDto + { + BaselineItemName = name, + CategoryId = name.GetMD5(), + RecommendationType = type, + Items = returnItems + }; + } + } + } + + private IEnumerable GetSimilarTo(User? user, IEnumerable baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) + { + var itemTypes = new List { BaseItemKind.Movie }; + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); + } + + foreach (var item in baselineItems) + { + var similar = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + Limit = itemLimit, + IncludeItemTypes = itemTypes.ToArray(), + IsMovie = true, + SimilarTo = item, + EnableGroupByMetadataKey = true, + DtoOptions = dtoOptions + }); + + if (similar.Count > 0) + { + var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user); + + yield return new RecommendationDto + { + BaselineItemName = item.Name, + CategoryId = item.Id, + RecommendationType = type, + Items = returnItems + }; + } + } + } + + private IEnumerable GetActors(IEnumerable items) + { + var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty(), new[] { PersonType.Director }) + { + MaxListOrder = 3 + }); + + var itemIds = items.Select(i => i.Id).ToList(); + + return people + .Where(i => itemIds.Contains(i.ItemId)) + .Select(i => i.Name) + .DistinctNames(); + } + + private IEnumerable GetDirectors(IEnumerable items) + { + var people = _libraryManager.GetPeople(new InternalPeopleQuery( + new[] { PersonType.Director }, + Array.Empty())); + + var itemIds = items.Select(i => i.Id).ToList(); + + return people + .Where(i => itemIds.Contains(i.ItemId)) + .Select(i => i.Name) + .DistinctNames(); } } diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index f4fb5f44ab..302f138ebc 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -18,181 +18,180 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The music genres controller. +/// +[Authorize(Policy = Policies.DefaultAuthorization)] +public class MusicGenresController : BaseJellyfinApiController { + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IUserManager _userManager; + /// - /// The music genres controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.DefaultAuthorization)] - public class MusicGenresController : BaseJellyfinApiController + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public MusicGenresController( + ILibraryManager libraryManager, + IUserManager userManager, + IDtoService dtoService) { - private readonly ILibraryManager _libraryManager; - private readonly IDtoService _dtoService; - private readonly IUserManager _userManager; + _libraryManager = libraryManager; + _userManager = userManager; + _dtoService = dtoService; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - public MusicGenresController( - ILibraryManager libraryManager, - IUserManager userManager, - IDtoService dtoService) + /// + /// Gets all music genres from a given item, folder, or the entire library. + /// + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// The search term. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited. + /// Optional filter by items that are marked as favorite, or not. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// User id. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional. Specify one or more sort orders, comma delimited. + /// Sort Order - Ascending,Descending. + /// Optional, include image information in output. + /// Optional. Include total record count. + /// Music genres returned. + /// An containing the queryresult of music genres. + [HttpGet] + [Obsolete("Use GetGenres instead")] + public ActionResult> GetMusicGenres( + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string? searchTerm, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery] bool? isFavorite, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] Guid? userId, + [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) + { + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); + + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var parentItem = _libraryManager.GetParentItem(parentId, userId); + + var query = new InternalItemsQuery(user) { - _libraryManager = libraryManager; - _userManager = userManager; - _dtoService = dtoService; - } + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + StartIndex = startIndex, + Limit = limit, + IsFavorite = isFavorite, + NameLessThan = nameLessThan, + NameStartsWith = nameStartsWith, + NameStartsWithOrGreater = nameStartsWithOrGreater, + DtoOptions = dtoOptions, + SearchTerm = searchTerm, + EnableTotalRecordCount = enableTotalRecordCount, + OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) + }; - /// - /// Gets all music genres from a given item, folder, or the entire library. - /// - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// The search term. - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. - /// Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited. - /// Optional filter by items that are marked as favorite, or not. - /// Optional, the max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// User id. - /// Optional filter by items whose name is sorted equally or greater than a given input string. - /// Optional filter by items whose name is sorted equally than a given input string. - /// Optional filter by items whose name is equally or lesser than a given input string. - /// Optional. Specify one or more sort orders, comma delimited. - /// Sort Order - Ascending,Descending. - /// Optional, include image information in output. - /// Optional. Include total record count. - /// Music genres returned. - /// An containing the queryresult of music genres. - [HttpGet] - [Obsolete("Use GetGenres instead")] - public ActionResult> GetMusicGenres( - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] string? searchTerm, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery] bool? isFavorite, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] Guid? userId, - [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) + if (parentId.HasValue) { - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - - User? user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var parentItem = _libraryManager.GetParentItem(parentId, userId); - - var query = new InternalItemsQuery(user) + if (parentItem is Folder) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, - StartIndex = startIndex, - Limit = limit, - IsFavorite = isFavorite, - NameLessThan = nameLessThan, - NameStartsWith = nameStartsWith, - NameStartsWithOrGreater = nameStartsWithOrGreater, - DtoOptions = dtoOptions, - SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount, - OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder) - }; - - if (parentId.HasValue) - { - if (parentItem is Folder) - { - query.AncestorIds = new[] { parentId.Value }; - } - else - { - query.ItemIds = new[] { parentId.Value }; - } - } - - var result = _libraryManager.GetMusicGenres(query); - - var shouldIncludeItemTypes = includeItemTypes.Length != 0; - return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); - } - - /// - /// Gets a music genre, by name. - /// - /// The genre name. - /// Optional. Filter by user id, and attach user data. - /// An containing a with the music genre. - [HttpGet("{genreName}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) - { - var dtoOptions = new DtoOptions().AddClientFields(User); - - MusicGenre? item; - - if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) - { - item = GetItemFromSlugName(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre); + query.AncestorIds = new[] { parentId.Value }; } else { - item = _libraryManager.GetMusicGenre(genreName); + query.ItemIds = new[] { parentId.Value }; } - - if (userId.HasValue && !userId.Value.Equals(default)) - { - var user = _userManager.GetUserById(userId.Value); - - return _dtoService.GetBaseItemDto(item, dtoOptions, user); - } - - return _dtoService.GetBaseItemDto(item, dtoOptions); } - private T? GetItemFromSlugName(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind) - where T : BaseItem, new() + var result = _libraryManager.GetMusicGenres(query); + + var shouldIncludeItemTypes = includeItemTypes.Length != 0; + return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); + } + + /// + /// Gets a music genre, by name. + /// + /// The genre name. + /// Optional. Filter by user id, and attach user data. + /// An containing a with the music genre. + [HttpGet("{genreName}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) + { + var dtoOptions = new DtoOptions().AddClientFields(User); + + MusicGenre? item; + + if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) { - var result = libraryManager.GetItemList(new InternalItemsQuery - { - Name = name.Replace(BaseItem.SlugChar, '&'), - IncludeItemTypes = new[] { baseItemKind }, - DtoOptions = dtoOptions - }).OfType().FirstOrDefault(); - - result ??= libraryManager.GetItemList(new InternalItemsQuery - { - Name = name.Replace(BaseItem.SlugChar, '/'), - IncludeItemTypes = new[] { baseItemKind }, - DtoOptions = dtoOptions - }).OfType().FirstOrDefault(); - - result ??= libraryManager.GetItemList(new InternalItemsQuery - { - Name = name.Replace(BaseItem.SlugChar, '?'), - IncludeItemTypes = new[] { baseItemKind }, - DtoOptions = dtoOptions - }).OfType().FirstOrDefault(); - - return result; + item = GetItemFromSlugName(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre); } + else + { + item = _libraryManager.GetMusicGenre(genreName); + } + + if (userId.HasValue && !userId.Value.Equals(default)) + { + var user = _userManager.GetUserById(userId.Value); + + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + return _dtoService.GetBaseItemDto(item, dtoOptions); + } + + private T? GetItemFromSlugName(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind) + where T : BaseItem, new() + { + var result = libraryManager.GetItemList(new InternalItemsQuery + { + Name = name.Replace(BaseItem.SlugChar, '&'), + IncludeItemTypes = new[] { baseItemKind }, + DtoOptions = dtoOptions + }).OfType().FirstOrDefault(); + + result ??= libraryManager.GetItemList(new InternalItemsQuery + { + Name = name.Replace(BaseItem.SlugChar, '/'), + IncludeItemTypes = new[] { baseItemKind }, + DtoOptions = dtoOptions + }).OfType().FirstOrDefault(); + + result ??= libraryManager.GetItemList(new InternalItemsQuery + { + Name = name.Replace(BaseItem.SlugChar, '?'), + IncludeItemTypes = new[] { baseItemKind }, + DtoOptions = dtoOptions + }).OfType().FirstOrDefault(); + + return result; } } diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 10f967dcde..3cb3caadb3 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -11,157 +11,156 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Package Controller. +/// +[Route("")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class PackageController : BaseJellyfinApiController { + private readonly IInstallationManager _installationManager; + private readonly IServerConfigurationManager _serverConfigurationManager; + /// - /// Package Controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class PackageController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager) { - private readonly IInstallationManager _installationManager; - private readonly IServerConfigurationManager _serverConfigurationManager; + _installationManager = installationManager; + _serverConfigurationManager = serverConfigurationManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager) + /// + /// Gets a package by name or assembly GUID. + /// + /// The name of the package. + /// The GUID of the associated assembly. + /// Package retrieved. + /// A containing package information. + [HttpGet("Packages/{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetPackageInfo( + [FromRoute, Required] string name, + [FromQuery] Guid? assemblyGuid) + { + var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); + var result = _installationManager.FilterPackages( + packages, + name, + assemblyGuid ?? default) + .FirstOrDefault(); + + if (result is null) { - _installationManager = installationManager; - _serverConfigurationManager = serverConfigurationManager; + return NotFound(); } - /// - /// Gets a package by name or assembly GUID. - /// - /// The name of the package. - /// The GUID of the associated assembly. - /// Package retrieved. - /// A containing package information. - [HttpGet("Packages/{name}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetPackageInfo( - [FromRoute, Required] string name, - [FromQuery] Guid? assemblyGuid) + return result; + } + + /// + /// Gets available packages. + /// + /// Available packages returned. + /// An containing available packages information. + [HttpGet("Packages")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetPackages() + { + IEnumerable packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); + + return packages; + } + + /// + /// Installs a package. + /// + /// Package name. + /// GUID of the associated assembly. + /// Optional version. Defaults to latest version. + /// Optional. Specify the repository to install from. + /// Package found. + /// Package not found. + /// A on success, or a if the package could not be found. + [HttpPost("Packages/Installed/{name}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = Policies.RequiresElevation)] + public async Task InstallPackage( + [FromRoute, Required] string name, + [FromQuery] Guid? assemblyGuid, + [FromQuery] string? version, + [FromQuery] string? repositoryUrl) + { + var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); + if (!string.IsNullOrEmpty(repositoryUrl)) { - var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); - var result = _installationManager.FilterPackages( - packages, - name, - assemblyGuid ?? default) - .FirstOrDefault(); - - if (result is null) - { - return NotFound(); - } - - return result; + packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase))) + .ToList(); } - /// - /// Gets available packages. - /// - /// Available packages returned. - /// An containing available packages information. - [HttpGet("Packages")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetPackages() - { - IEnumerable packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); + var package = _installationManager.GetCompatibleVersions( + packages, + name, + assemblyGuid ?? Guid.Empty, + specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version)) + .FirstOrDefault(); - return packages; + if (package is null) + { + return NotFound(); } - /// - /// Installs a package. - /// - /// Package name. - /// GUID of the associated assembly. - /// Optional version. Defaults to latest version. - /// Optional. Specify the repository to install from. - /// Package found. - /// Package not found. - /// A on success, or a if the package could not be found. - [HttpPost("Packages/Installed/{name}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [Authorize(Policy = Policies.RequiresElevation)] - public async Task InstallPackage( - [FromRoute, Required] string name, - [FromQuery] Guid? assemblyGuid, - [FromQuery] string? version, - [FromQuery] string? repositoryUrl) - { - var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); - if (!string.IsNullOrEmpty(repositoryUrl)) - { - packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase))) - .ToList(); - } + await _installationManager.InstallPackage(package).ConfigureAwait(false); - var package = _installationManager.GetCompatibleVersions( - packages, - name, - assemblyGuid ?? Guid.Empty, - specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version)) - .FirstOrDefault(); + return NoContent(); + } - if (package is null) - { - return NotFound(); - } + /// + /// Cancels a package installation. + /// + /// Installation Id. + /// Installation cancelled. + /// A on successfully cancelling a package installation. + [HttpDelete("Packages/Installing/{packageId}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult CancelPackageInstallation( + [FromRoute, Required] Guid packageId) + { + _installationManager.CancelInstallation(packageId); + return NoContent(); + } - await _installationManager.InstallPackage(package).ConfigureAwait(false); + /// + /// Gets all package repositories. + /// + /// Package repositories returned. + /// An containing the list of package repositories. + [HttpGet("Repositories")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetRepositories() + { + return Ok(_serverConfigurationManager.Configuration.PluginRepositories.AsEnumerable()); + } - return NoContent(); - } - - /// - /// Cancels a package installation. - /// - /// Installation Id. - /// Installation cancelled. - /// A on successfully cancelling a package installation. - [HttpDelete("Packages/Installing/{packageId}")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CancelPackageInstallation( - [FromRoute, Required] Guid packageId) - { - _installationManager.CancelInstallation(packageId); - return NoContent(); - } - - /// - /// Gets all package repositories. - /// - /// Package repositories returned. - /// An containing the list of package repositories. - [HttpGet("Repositories")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetRepositories() - { - return Ok(_serverConfigurationManager.Configuration.PluginRepositories.AsEnumerable()); - } - - /// - /// Sets the enabled and existing package repositories. - /// - /// The list of package repositories. - /// Package repositories saved. - /// A . - [HttpPost("Repositories")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos) - { - _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos; - _serverConfigurationManager.SaveConfiguration(); - return NoContent(); - } + /// + /// Sets the enabled and existing package repositories. + /// + /// The list of package repositories. + /// Package repositories saved. + /// A . + [HttpPost("Repositories")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos) + { + _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos; + _serverConfigurationManager.SaveConfiguration(); + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 09f7281ecb..9fb6da5276 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -15,125 +15,124 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Persons controller. +/// +[Authorize(Policy = Policies.DefaultAuthorization)] +public class PersonsController : BaseJellyfinApiController { + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IUserManager _userManager; + /// - /// Persons controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.DefaultAuthorization)] - public class PersonsController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public PersonsController( + ILibraryManager libraryManager, + IDtoService dtoService, + IUserManager userManager) { - private readonly ILibraryManager _libraryManager; - private readonly IDtoService _dtoService; - private readonly IUserManager _userManager; + _libraryManager = libraryManager; + _dtoService = dtoService; + _userManager = userManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public PersonsController( - ILibraryManager libraryManager, - IDtoService dtoService, - IUserManager userManager) + /// + /// Gets all persons. + /// + /// Optional. The maximum number of records to return. + /// The search term. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Specify additional filters to apply. + /// Optional filter by items that are marked as favorite, or not. userId is required. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited. + /// Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, person results will be filtered on items related to said persons. + /// User id. + /// Optional, include image information in output. + /// Persons returned. + /// An containing the queryresult of persons. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetPersons( + [FromQuery] int? limit, + [FromQuery] string? searchTerm, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery] bool? isFavorite, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, + [FromQuery] Guid? appearsInItemId, + [FromQuery] Guid? userId, + [FromQuery] bool? enableImages = true) + { + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); + var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery( + personTypes, + excludePersonTypes) { - _libraryManager = libraryManager; - _dtoService = dtoService; - _userManager = userManager; + NameContains = searchTerm, + User = user, + IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, + AppearsInItemId = appearsInItemId ?? Guid.Empty, + Limit = limit ?? 0 + }); + + return new QueryResult( + peopleItems + .Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)) + .ToArray()); + } + + /// + /// Get person by name. + /// + /// Person name. + /// Optional. Filter by user id, and attach user data. + /// Person returned. + /// Person not found. + /// An containing the person on success, + /// or a if person not found. + [HttpGet("{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId) + { + var dtoOptions = new DtoOptions() + .AddClientFields(User); + + var item = _libraryManager.GetPerson(name); + if (item is null) + { + return NotFound(); } - /// - /// Gets all persons. - /// - /// Optional. The maximum number of records to return. - /// The search term. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Specify additional filters to apply. - /// Optional filter by items that are marked as favorite, or not. userId is required. - /// Optional, include user data. - /// Optional, the max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited. - /// Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited. - /// Optional. If specified, person results will be filtered on items related to said persons. - /// User id. - /// Optional, include image information in output. - /// Persons returned. - /// An containing the queryresult of persons. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetPersons( - [FromQuery] int? limit, - [FromQuery] string? searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery] bool? isFavorite, - [FromQuery] bool? enableUserData, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery] Guid? appearsInItemId, - [FromQuery] Guid? userId, - [FromQuery] bool? enableImages = true) + if (userId.HasValue && !userId.Value.Equals(default)) { - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - - User? user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); - var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery( - personTypes, - excludePersonTypes) - { - NameContains = searchTerm, - User = user, - IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, - AppearsInItemId = appearsInItemId ?? Guid.Empty, - Limit = limit ?? 0 - }); - - return new QueryResult( - peopleItems - .Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)) - .ToArray()); + var user = _userManager.GetUserById(userId.Value); + return _dtoService.GetBaseItemDto(item, dtoOptions, user); } - /// - /// Get person by name. - /// - /// Person name. - /// Optional. Filter by user id, and attach user data. - /// Person returned. - /// Person not found. - /// An containing the person on success, - /// or a if person not found. - [HttpGet("{name}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId) - { - var dtoOptions = new DtoOptions() - .AddClientFields(User); - - var item = _libraryManager.GetPerson(name); - if (item is null) - { - return NotFound(); - } - - if (userId.HasValue && !userId.Value.Equals(default)) - { - var user = _userManager.GetUserById(userId.Value); - return _dtoService.GetBaseItemDto(item, dtoOptions, user); - } - - return _dtoService.GetBaseItemDto(item, dtoOptions); - } + return _dtoService.GetBaseItemDto(item, dtoOptions); } } diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index e0c565da18..11e5893017 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -20,202 +20,201 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Playlists controller. +/// +[Authorize(Policy = Policies.DefaultAuthorization)] +public class PlaylistsController : BaseJellyfinApiController { + private readonly IPlaylistManager _playlistManager; + private readonly IDtoService _dtoService; + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + /// - /// Playlists controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.DefaultAuthorization)] - public class PlaylistsController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public PlaylistsController( + IDtoService dtoService, + IPlaylistManager playlistManager, + IUserManager userManager, + ILibraryManager libraryManager) { - private readonly IPlaylistManager _playlistManager; - private readonly IDtoService _dtoService; - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; + _dtoService = dtoService; + _playlistManager = playlistManager; + _userManager = userManager; + _libraryManager = libraryManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public PlaylistsController( - IDtoService dtoService, - IPlaylistManager playlistManager, - IUserManager userManager, - ILibraryManager libraryManager) + /// + /// Creates a new playlist. + /// + /// + /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. + /// Query parameters are obsolete. + /// + /// The playlist name. + /// The item ids. + /// The user id. + /// The media type. + /// The create playlist payload. + /// + /// A that represents the asynchronous operation to create a playlist. + /// The task result contains an indicating success. + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CreatePlaylist( + [FromQuery, ParameterObsolete] string? name, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList ids, + [FromQuery, ParameterObsolete] Guid? userId, + [FromQuery, ParameterObsolete] string? mediaType, + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest) + { + if (ids.Count == 0) { - _dtoService = dtoService; - _playlistManager = playlistManager; - _userManager = userManager; - _libraryManager = libraryManager; + ids = createPlaylistRequest?.Ids ?? Array.Empty(); } - /// - /// Creates a new playlist. - /// - /// - /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. - /// Query parameters are obsolete. - /// - /// The playlist name. - /// The item ids. - /// The user id. - /// The media type. - /// The create playlist payload. - /// - /// A that represents the asynchronous operation to create a playlist. - /// The task result contains an indicating success. - /// - [HttpPost] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> CreatePlaylist( - [FromQuery, ParameterObsolete] string? name, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList ids, - [FromQuery, ParameterObsolete] Guid? userId, - [FromQuery, ParameterObsolete] string? mediaType, - [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest) + var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest { - if (ids.Count == 0) - { - ids = createPlaylistRequest?.Ids ?? Array.Empty(); - } + Name = name ?? createPlaylistRequest?.Name, + ItemIdList = ids, + UserId = userId ?? createPlaylistRequest?.UserId ?? default, + MediaType = mediaType ?? createPlaylistRequest?.MediaType + }).ConfigureAwait(false); - var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest - { - Name = name ?? createPlaylistRequest?.Name, - ItemIdList = ids, - UserId = userId ?? createPlaylistRequest?.UserId ?? default, - MediaType = mediaType ?? createPlaylistRequest?.MediaType - }).ConfigureAwait(false); + return result; + } - return result; + /// + /// Adds items to a playlist. + /// + /// The playlist id. + /// Item id, comma delimited. + /// The userId. + /// Items added to playlist. + /// An on success. + [HttpPost("{playlistId}/Items")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task AddToPlaylist( + [FromRoute, Required] Guid playlistId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, + [FromQuery] Guid? userId) + { + await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Moves a playlist item. + /// + /// The playlist id. + /// The item id. + /// The new index. + /// Item moved to new index. + /// An on success. + [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task MoveItem( + [FromRoute, Required] string playlistId, + [FromRoute, Required] string itemId, + [FromRoute, Required] int newIndex) + { + await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Removes items from a playlist. + /// + /// The playlist id. + /// The item ids, comma delimited. + /// Items removed. + /// An on success. + [HttpDelete("{playlistId}/Items")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task RemoveFromPlaylist( + [FromRoute, Required] string playlistId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds) + { + await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Gets the original items of a playlist. + /// + /// The playlist id. + /// User id. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Include image information in output. + /// Optional. Include user data. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Original playlist returned. + /// Playlist not found. + /// The original playlist items. + [HttpGet("{playlistId}/Items")] + public ActionResult> GetPlaylistItems( + [FromRoute, Required] Guid playlistId, + [FromQuery, Required] Guid userId, + [FromQuery] int? startIndex, + [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 playlist = (Playlist)_libraryManager.GetItemById(playlistId); + if (playlist is null) + { + return NotFound(); } - /// - /// Adds items to a playlist. - /// - /// The playlist id. - /// Item id, comma delimited. - /// The userId. - /// Items added to playlist. - /// An on success. - [HttpPost("{playlistId}/Items")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task AddToPlaylist( - [FromRoute, Required] Guid playlistId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, - [FromQuery] Guid? userId) + var user = userId.Equals(default) + ? null + : _userManager.GetUserById(userId); + + var items = playlist.GetManageableItems().ToArray(); + + var count = items.Length; + + if (startIndex.HasValue) { - await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false); - return NoContent(); + items = items.Skip(startIndex.Value).ToArray(); } - /// - /// Moves a playlist item. - /// - /// The playlist id. - /// The item id. - /// The new index. - /// Item moved to new index. - /// An on success. - [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task MoveItem( - [FromRoute, Required] string playlistId, - [FromRoute, Required] string itemId, - [FromRoute, Required] int newIndex) + if (limit.HasValue) { - await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false); - return NoContent(); + items = items.Take(limit.Value).ToArray(); } - /// - /// Removes items from a playlist. - /// - /// The playlist id. - /// The item ids, comma delimited. - /// Items removed. - /// An on success. - [HttpDelete("{playlistId}/Items")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task RemoveFromPlaylist( - [FromRoute, Required] string playlistId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds) + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); + + for (int index = 0; index < dtos.Count; index++) { - await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); - return NoContent(); + dtos[index].PlaylistItemId = items[index].Item1.Id; } - /// - /// Gets the original items of a playlist. - /// - /// The playlist id. - /// User id. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Include image information in output. - /// Optional. Include user data. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Original playlist returned. - /// Playlist not found. - /// The original playlist items. - [HttpGet("{playlistId}/Items")] - public ActionResult> GetPlaylistItems( - [FromRoute, Required] Guid playlistId, - [FromQuery, Required] Guid userId, - [FromQuery] int? startIndex, - [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 playlist = (Playlist)_libraryManager.GetItemById(playlistId); - if (playlist is null) - { - return NotFound(); - } + var result = new QueryResult( + startIndex, + count, + dtos); - var user = userId.Equals(default) - ? null - : _userManager.GetUserById(userId); - - var items = playlist.GetManageableItems().ToArray(); - - var count = items.Length; - - if (startIndex.HasValue) - { - items = items.Skip(startIndex.Value).ToArray(); - } - - if (limit.HasValue) - { - items = items.Take(limit.Value).ToArray(); - } - - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - - var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); - - for (int index = 0; index < dtos.Count; index++) - { - dtos[index].PlaylistItemId = items[index].Item1.Id; - } - - var result = new QueryResult( - startIndex, - count, - dtos); - - return result; - } + return result; } } diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 0260f9e2a6..18d6ebf1e0 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -17,366 +17,365 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Playstate controller. +/// +[Route("")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class PlaystateController : BaseJellyfinApiController { + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataRepository; + private readonly ILibraryManager _libraryManager; + private readonly ISessionManager _sessionManager; + private readonly ILogger _logger; + private readonly TranscodingJobHelper _transcodingJobHelper; + /// - /// Playstate controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class PlaystateController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Th singleton. + public PlaystateController( + IUserManager userManager, + IUserDataManager userDataRepository, + ILibraryManager libraryManager, + ISessionManager sessionManager, + ILoggerFactory loggerFactory, + TranscodingJobHelper transcodingJobHelper) { - private readonly IUserManager _userManager; - private readonly IUserDataManager _userDataRepository; - private readonly ILibraryManager _libraryManager; - private readonly ISessionManager _sessionManager; - private readonly ILogger _logger; - private readonly TranscodingJobHelper _transcodingJobHelper; + _userManager = userManager; + _userDataRepository = userDataRepository; + _libraryManager = libraryManager; + _sessionManager = sessionManager; + _logger = loggerFactory.CreateLogger(); - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Th singleton. - public PlaystateController( - IUserManager userManager, - IUserDataManager userDataRepository, - ILibraryManager libraryManager, - ISessionManager sessionManager, - ILoggerFactory loggerFactory, - TranscodingJobHelper transcodingJobHelper) + _transcodingJobHelper = transcodingJobHelper; + } + + /// + /// Marks an item as played for user. + /// + /// User id. + /// Item id. + /// Optional. The date the item was played. + /// Item marked as played. + /// Item not found. + /// An containing the , or a if item was not found. + [HttpPost("Users/{userId}/PlayedItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> MarkPlayedItem( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId, + [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed) + { + var user = _userManager.GetUserById(userId); + var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + + var item = _libraryManager.GetItemById(itemId); + if (item is null) { - _userManager = userManager; - _userDataRepository = userDataRepository; - _libraryManager = libraryManager; - _sessionManager = sessionManager; - _logger = loggerFactory.CreateLogger(); - - _transcodingJobHelper = transcodingJobHelper; + return NotFound(); } - /// - /// Marks an item as played for user. - /// - /// User id. - /// Item id. - /// Optional. The date the item was played. - /// Item marked as played. - /// Item not found. - /// An containing the , or a if item was not found. - [HttpPost("Users/{userId}/PlayedItems/{itemId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> MarkPlayedItem( - [FromRoute, Required] Guid userId, - [FromRoute, Required] Guid itemId, - [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed) + var dto = UpdatePlayedStatus(user, item, true, datePlayed); + foreach (var additionalUserInfo in session.AdditionalUsers) { - var user = _userManager.GetUserById(userId); - var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + UpdatePlayedStatus(additionalUser, item, true, datePlayed); + } - var item = _libraryManager.GetItemById(itemId); - if (item is null) + return dto; + } + + /// + /// Marks an item as unplayed for user. + /// + /// User id. + /// Item id. + /// Item marked as unplayed. + /// Item not found. + /// A containing the , or a if item was not found. + [HttpDelete("Users/{userId}/PlayedItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + { + var user = _userManager.GetUserById(userId); + var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var item = _libraryManager.GetItemById(itemId); + + if (item is null) + { + return NotFound(); + } + + var dto = UpdatePlayedStatus(user, item, false, null); + foreach (var additionalUserInfo in session.AdditionalUsers) + { + var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + UpdatePlayedStatus(additionalUser, item, false, null); + } + + return dto; + } + + /// + /// Reports playback has started within a session. + /// + /// The playback start info. + /// Playback start recorded. + /// A . + [HttpPost("Sessions/Playing")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo) + { + playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); + playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Reports playback progress within a session. + /// + /// The playback progress info. + /// Playback progress recorded. + /// A . + [HttpPost("Sessions/Playing/Progress")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo) + { + playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); + playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Pings a playback session. + /// + /// Playback session id. + /// Playback session pinged. + /// A . + [HttpPost("Sessions/Playing/Ping")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId) + { + _transcodingJobHelper.PingTranscodingJob(playSessionId, null); + return NoContent(); + } + + /// + /// Reports playback has stopped within a session. + /// + /// The playback stop info. + /// Playback stop recorded. + /// A . + [HttpPost("Sessions/Playing/Stopped")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task ReportPlaybackStopped([FromBody] PlaybackStopInfo playbackStopInfo) + { + _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); + if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) + { + await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + } + + playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Reports that a user has begun playing an item. + /// + /// User id. + /// Item id. + /// The id of the MediaSource. + /// The audio stream index. + /// The subtitle stream index. + /// The play method. + /// The live stream id. + /// The play session id. + /// Indicates if the client can seek. + /// Play start recorded. + /// A . + [HttpPost("Users/{userId}/PlayingItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] + public async Task OnPlaybackStart( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId, + [FromQuery] string? mediaSourceId, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] PlayMethod? playMethod, + [FromQuery] string? liveStreamId, + [FromQuery] string? playSessionId, + [FromQuery] bool canSeek = false) + { + var playbackStartInfo = new PlaybackStartInfo + { + CanSeek = canSeek, + ItemId = itemId, + MediaSourceId = mediaSourceId, + AudioStreamIndex = audioStreamIndex, + SubtitleStreamIndex = subtitleStreamIndex, + PlayMethod = playMethod ?? PlayMethod.Transcode, + PlaySessionId = playSessionId, + LiveStreamId = liveStreamId + }; + + playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); + playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Reports a user's playback progress. + /// + /// User id. + /// Item id. + /// The id of the MediaSource. + /// Optional. The current position, in ticks. 1 tick = 10000 ms. + /// The audio stream index. + /// The subtitle stream index. + /// Scale of 0-100. + /// The play method. + /// The live stream id. + /// The play session id. + /// The repeat mode. + /// Indicates if the player is paused. + /// Indicates if the player is muted. + /// Play progress recorded. + /// A . + [HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] + public async Task OnPlaybackProgress( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId, + [FromQuery] string? mediaSourceId, + [FromQuery] long? positionTicks, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] int? volumeLevel, + [FromQuery] PlayMethod? playMethod, + [FromQuery] string? liveStreamId, + [FromQuery] string? playSessionId, + [FromQuery] RepeatMode? repeatMode, + [FromQuery] bool isPaused = false, + [FromQuery] bool isMuted = false) + { + var playbackProgressInfo = new PlaybackProgressInfo + { + ItemId = itemId, + PositionTicks = positionTicks, + IsMuted = isMuted, + IsPaused = isPaused, + MediaSourceId = mediaSourceId, + AudioStreamIndex = audioStreamIndex, + SubtitleStreamIndex = subtitleStreamIndex, + VolumeLevel = volumeLevel, + PlayMethod = playMethod ?? PlayMethod.Transcode, + PlaySessionId = playSessionId, + LiveStreamId = liveStreamId, + RepeatMode = repeatMode ?? RepeatMode.RepeatNone + }; + + playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); + playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Reports that a user has stopped playing an item. + /// + /// User id. + /// Item id. + /// The id of the MediaSource. + /// The next media type that will play. + /// Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms. + /// The live stream id. + /// The play session id. + /// Playback stop recorded. + /// A . + [HttpDelete("Users/{userId}/PlayingItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] + public async Task OnPlaybackStopped( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId, + [FromQuery] string? mediaSourceId, + [FromQuery] string? nextMediaType, + [FromQuery] long? positionTicks, + [FromQuery] string? liveStreamId, + [FromQuery] string? playSessionId) + { + var playbackStopInfo = new PlaybackStopInfo + { + ItemId = itemId, + PositionTicks = positionTicks, + MediaSourceId = mediaSourceId, + PlaySessionId = playSessionId, + LiveStreamId = liveStreamId, + NextMediaType = nextMediaType + }; + + _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); + if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) + { + await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + } + + playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Updates the played status. + /// + /// The user. + /// The item. + /// if set to true [was played]. + /// The date played. + /// Task. + private UserItemDataDto UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed) + { + if (wasPlayed) + { + item.MarkPlayed(user, datePlayed, true); + } + else + { + item.MarkUnplayed(user); + } + + return _userDataRepository.GetUserDataDto(item, user); + } + + private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId) + { + if (method == PlayMethod.Transcode) + { + var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId); + if (job is null) { - return NotFound(); + return PlayMethod.DirectPlay; } - - var dto = UpdatePlayedStatus(user, item, true, datePlayed); - foreach (var additionalUserInfo in session.AdditionalUsers) - { - var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); - UpdatePlayedStatus(additionalUser, item, true, datePlayed); - } - - return dto; } - /// - /// Marks an item as unplayed for user. - /// - /// User id. - /// Item id. - /// Item marked as unplayed. - /// Item not found. - /// A containing the , or a if item was not found. - [HttpDelete("Users/{userId}/PlayedItems/{itemId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) - { - var user = _userManager.GetUserById(userId); - var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var item = _libraryManager.GetItemById(itemId); - - if (item is null) - { - return NotFound(); - } - - var dto = UpdatePlayedStatus(user, item, false, null); - foreach (var additionalUserInfo in session.AdditionalUsers) - { - var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); - UpdatePlayedStatus(additionalUser, item, false, null); - } - - return dto; - } - - /// - /// Reports playback has started within a session. - /// - /// The playback start info. - /// Playback start recorded. - /// A . - [HttpPost("Sessions/Playing")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo) - { - playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); - playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Reports playback progress within a session. - /// - /// The playback progress info. - /// Playback progress recorded. - /// A . - [HttpPost("Sessions/Playing/Progress")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo) - { - playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); - playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Pings a playback session. - /// - /// Playback session id. - /// Playback session pinged. - /// A . - [HttpPost("Sessions/Playing/Ping")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId) - { - _transcodingJobHelper.PingTranscodingJob(playSessionId, null); - return NoContent(); - } - - /// - /// Reports playback has stopped within a session. - /// - /// The playback stop info. - /// Playback stop recorded. - /// A . - [HttpPost("Sessions/Playing/Stopped")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task ReportPlaybackStopped([FromBody] PlaybackStopInfo playbackStopInfo) - { - _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); - if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) - { - await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); - } - - playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Reports that a user has begun playing an item. - /// - /// User id. - /// Item id. - /// The id of the MediaSource. - /// The audio stream index. - /// The subtitle stream index. - /// The play method. - /// The live stream id. - /// The play session id. - /// Indicates if the client can seek. - /// Play start recorded. - /// A . - [HttpPost("Users/{userId}/PlayingItems/{itemId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] - public async Task OnPlaybackStart( - [FromRoute, Required] Guid userId, - [FromRoute, Required] Guid itemId, - [FromQuery] string? mediaSourceId, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] PlayMethod? playMethod, - [FromQuery] string? liveStreamId, - [FromQuery] string? playSessionId, - [FromQuery] bool canSeek = false) - { - var playbackStartInfo = new PlaybackStartInfo - { - CanSeek = canSeek, - ItemId = itemId, - MediaSourceId = mediaSourceId, - AudioStreamIndex = audioStreamIndex, - SubtitleStreamIndex = subtitleStreamIndex, - PlayMethod = playMethod ?? PlayMethod.Transcode, - PlaySessionId = playSessionId, - LiveStreamId = liveStreamId - }; - - playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId); - playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Reports a user's playback progress. - /// - /// User id. - /// Item id. - /// The id of the MediaSource. - /// Optional. The current position, in ticks. 1 tick = 10000 ms. - /// The audio stream index. - /// The subtitle stream index. - /// Scale of 0-100. - /// The play method. - /// The live stream id. - /// The play session id. - /// The repeat mode. - /// Indicates if the player is paused. - /// Indicates if the player is muted. - /// Play progress recorded. - /// A . - [HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] - public async Task OnPlaybackProgress( - [FromRoute, Required] Guid userId, - [FromRoute, Required] Guid itemId, - [FromQuery] string? mediaSourceId, - [FromQuery] long? positionTicks, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] int? volumeLevel, - [FromQuery] PlayMethod? playMethod, - [FromQuery] string? liveStreamId, - [FromQuery] string? playSessionId, - [FromQuery] RepeatMode? repeatMode, - [FromQuery] bool isPaused = false, - [FromQuery] bool isMuted = false) - { - var playbackProgressInfo = new PlaybackProgressInfo - { - ItemId = itemId, - PositionTicks = positionTicks, - IsMuted = isMuted, - IsPaused = isPaused, - MediaSourceId = mediaSourceId, - AudioStreamIndex = audioStreamIndex, - SubtitleStreamIndex = subtitleStreamIndex, - VolumeLevel = volumeLevel, - PlayMethod = playMethod ?? PlayMethod.Transcode, - PlaySessionId = playSessionId, - LiveStreamId = liveStreamId, - RepeatMode = repeatMode ?? RepeatMode.RepeatNone - }; - - playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId); - playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Reports that a user has stopped playing an item. - /// - /// User id. - /// Item id. - /// The id of the MediaSource. - /// The next media type that will play. - /// Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms. - /// The live stream id. - /// The play session id. - /// Playback stop recorded. - /// A . - [HttpDelete("Users/{userId}/PlayingItems/{itemId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] - public async Task OnPlaybackStopped( - [FromRoute, Required] Guid userId, - [FromRoute, Required] Guid itemId, - [FromQuery] string? mediaSourceId, - [FromQuery] string? nextMediaType, - [FromQuery] long? positionTicks, - [FromQuery] string? liveStreamId, - [FromQuery] string? playSessionId) - { - var playbackStopInfo = new PlaybackStopInfo - { - ItemId = itemId, - PositionTicks = positionTicks, - MediaSourceId = mediaSourceId, - PlaySessionId = playSessionId, - LiveStreamId = liveStreamId, - NextMediaType = nextMediaType - }; - - _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); - if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) - { - await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); - } - - playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Updates the played status. - /// - /// The user. - /// The item. - /// if set to true [was played]. - /// The date played. - /// Task. - private UserItemDataDto UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed) - { - if (wasPlayed) - { - item.MarkPlayed(user, datePlayed, true); - } - else - { - item.MarkUnplayed(user); - } - - return _userDataRepository.GetUserDataDto(item, user); - } - - private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId) - { - if (method == PlayMethod.Transcode) - { - var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId); - if (job is null) - { - return PlayMethod.DirectPlay; - } - } - - return method; - } + return method; } } diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index b8a09990a5..53f5991ad6 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -17,250 +17,249 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Plugins controller. +/// +[Authorize(Policy = Policies.DefaultAuthorization)] +public class PluginsController : BaseJellyfinApiController { + private readonly IInstallationManager _installationManager; + private readonly IPluginManager _pluginManager; + private readonly JsonSerializerOptions _serializerOptions; + /// - /// Plugins controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.DefaultAuthorization)] - public class PluginsController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + public PluginsController( + IInstallationManager installationManager, + IPluginManager pluginManager) { - private readonly IInstallationManager _installationManager; - private readonly IPluginManager _pluginManager; - private readonly JsonSerializerOptions _serializerOptions; + _installationManager = installationManager; + _pluginManager = pluginManager; + _serializerOptions = JsonDefaults.Options; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public PluginsController( - IInstallationManager installationManager, - IPluginManager pluginManager) + /// + /// Gets a list of currently installed plugins. + /// + /// Installed plugins returned. + /// List of currently installed plugins. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetPlugins() + { + return Ok(_pluginManager.Plugins + .OrderBy(p => p.Name) + .Select(p => p.GetPluginInfo())); + } + + /// + /// Enables a disabled plugin. + /// + /// Plugin id. + /// Plugin version. + /// Plugin enabled. + /// Plugin not found. + /// An on success, or a if the plugin could not be found. + [HttpPost("{pluginId}/{version}/Enable")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) + { + var plugin = _pluginManager.GetPlugin(pluginId, version); + if (plugin is null) { - _installationManager = installationManager; - _pluginManager = pluginManager; - _serializerOptions = JsonDefaults.Options; + return NotFound(); } - /// - /// Gets a list of currently installed plugins. - /// - /// Installed plugins returned. - /// List of currently installed plugins. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetPlugins() + _pluginManager.EnablePlugin(plugin); + return NoContent(); + } + + /// + /// Disable a plugin. + /// + /// Plugin id. + /// Plugin version. + /// Plugin disabled. + /// Plugin not found. + /// An on success, or a if the plugin could not be found. + [HttpPost("{pluginId}/{version}/Disable")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) + { + var plugin = _pluginManager.GetPlugin(pluginId, version); + if (plugin is null) { - return Ok(_pluginManager.Plugins - .OrderBy(p => p.Name) - .Select(p => p.GetPluginInfo())); + return NotFound(); } - /// - /// Enables a disabled plugin. - /// - /// Plugin id. - /// Plugin version. - /// Plugin enabled. - /// Plugin not found. - /// An on success, or a if the plugin could not be found. - [HttpPost("{pluginId}/{version}/Enable")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) - { - var plugin = _pluginManager.GetPlugin(pluginId, version); - if (plugin is null) - { - return NotFound(); - } + _pluginManager.DisablePlugin(plugin); + return NoContent(); + } - _pluginManager.EnablePlugin(plugin); - return NoContent(); + /// + /// Uninstalls a plugin by version. + /// + /// Plugin id. + /// Plugin version. + /// Plugin uninstalled. + /// Plugin not found. + /// An on success, or a if the plugin could not be found. + [HttpDelete("{pluginId}/{version}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) + { + var plugin = _pluginManager.GetPlugin(pluginId, version); + if (plugin is null) + { + return NotFound(); } - /// - /// Disable a plugin. - /// - /// Plugin id. - /// Plugin version. - /// Plugin disabled. - /// Plugin not found. - /// An on success, or a if the plugin could not be found. - [HttpPost("{pluginId}/{version}/Disable")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) + _installationManager.UninstallPlugin(plugin); + return NoContent(); + } + + /// + /// Uninstalls a plugin. + /// + /// Plugin id. + /// Plugin uninstalled. + /// Plugin not found. + /// An on success, or a if the plugin could not be found. + [HttpDelete("{pluginId}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Obsolete("Please use the UninstallPluginByVersion API.")] + public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId) + { + // If no version is given, return the current instance. + var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList(); + + // Select the un-instanced one first. + var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault(); + + if (plugin is not null) { - var plugin = _pluginManager.GetPlugin(pluginId, version); - if (plugin is null) - { - return NotFound(); - } - - _pluginManager.DisablePlugin(plugin); - return NoContent(); - } - - /// - /// Uninstalls a plugin by version. - /// - /// Plugin id. - /// Plugin version. - /// Plugin uninstalled. - /// Plugin not found. - /// An on success, or a if the plugin could not be found. - [HttpDelete("{pluginId}/{version}")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) - { - var plugin = _pluginManager.GetPlugin(pluginId, version); - if (plugin is null) - { - return NotFound(); - } - _installationManager.UninstallPlugin(plugin); return NoContent(); } - /// - /// Uninstalls a plugin. - /// - /// Plugin id. - /// Plugin uninstalled. - /// Plugin not found. - /// An on success, or a if the plugin could not be found. - [HttpDelete("{pluginId}")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [Obsolete("Please use the UninstallPluginByVersion API.")] - public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId) + return NotFound(); + } + + /// + /// Gets plugin configuration. + /// + /// Plugin id. + /// Plugin configuration returned. + /// Plugin not found or plugin configuration not found. + /// Plugin configuration. + [HttpGet("{pluginId}/Configuration")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetPluginConfiguration([FromRoute, Required] Guid pluginId) + { + var plugin = _pluginManager.GetPlugin(pluginId); + if (plugin?.Instance is IHasPluginConfiguration configPlugin) { - // If no version is given, return the current instance. - var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList(); + return configPlugin.Configuration; + } - // Select the un-instanced one first. - var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault(); - - if (plugin is not null) - { - _installationManager.UninstallPlugin(plugin); - return NoContent(); - } + return NotFound(); + } + /// + /// Updates plugin configuration. + /// + /// + /// Accepts plugin configuration as JSON body. + /// + /// Plugin id. + /// Plugin configuration updated. + /// Plugin not found or plugin does not have configuration. + /// An on success, or a if the plugin could not be found. + [HttpPost("{pluginId}/Configuration")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdatePluginConfiguration([FromRoute, Required] Guid pluginId) + { + var plugin = _pluginManager.GetPlugin(pluginId); + if (plugin?.Instance is not IHasPluginConfiguration configPlugin) + { return NotFound(); } - /// - /// Gets plugin configuration. - /// - /// Plugin id. - /// Plugin configuration returned. - /// Plugin not found or plugin configuration not found. - /// Plugin configuration. - [HttpGet("{pluginId}/Configuration")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetPluginConfiguration([FromRoute, Required] Guid pluginId) - { - var plugin = _pluginManager.GetPlugin(pluginId); - if (plugin?.Instance is IHasPluginConfiguration configPlugin) - { - return configPlugin.Configuration; - } + var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions) + .ConfigureAwait(false); + if (configuration is not null) + { + configPlugin.UpdateConfiguration(configuration); + } + + return NoContent(); + } + + /// + /// Gets a plugin's image. + /// + /// Plugin id. + /// Plugin version. + /// Plugin image returned. + /// Plugin's image. + [HttpGet("{pluginId}/{version}/Image")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + [AllowAnonymous] + public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) + { + var plugin = _pluginManager.GetPlugin(pluginId, version); + if (plugin is null) + { return NotFound(); } - /// - /// Updates plugin configuration. - /// - /// - /// Accepts plugin configuration as JSON body. - /// - /// Plugin id. - /// Plugin configuration updated. - /// Plugin not found or plugin does not have configuration. - /// An on success, or a if the plugin could not be found. - [HttpPost("{pluginId}/Configuration")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task UpdatePluginConfiguration([FromRoute, Required] Guid pluginId) + var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty); + if (plugin.Manifest.ImagePath is null || !System.IO.File.Exists(imagePath)) { - var plugin = _pluginManager.GetPlugin(pluginId); - if (plugin?.Instance is not IHasPluginConfiguration configPlugin) - { - return NotFound(); - } - - var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions) - .ConfigureAwait(false); - - if (configuration is not null) - { - configPlugin.UpdateConfiguration(configuration); - } - - return NoContent(); - } - - /// - /// Gets a plugin's image. - /// - /// Plugin id. - /// Plugin version. - /// Plugin image returned. - /// Plugin's image. - [HttpGet("{pluginId}/{version}/Image")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesImageFile] - [AllowAnonymous] - public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version) - { - var plugin = _pluginManager.GetPlugin(pluginId, version); - if (plugin is null) - { - return NotFound(); - } - - var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty); - if (plugin.Manifest.ImagePath is null || !System.IO.File.Exists(imagePath)) - { - return NotFound(); - } - - imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); - return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath)); - } - - /// - /// Gets a plugin's manifest. - /// - /// Plugin id. - /// Plugin manifest returned. - /// Plugin not found. - /// A on success, or a if the plugin could not be found. - [HttpPost("{pluginId}/Manifest")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetPluginManifest([FromRoute, Required] Guid pluginId) - { - var plugin = _pluginManager.GetPlugin(pluginId); - - if (plugin is not null) - { - return plugin.Manifest; - } - return NotFound(); } + + imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); + return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath)); + } + + /// + /// Gets a plugin's manifest. + /// + /// Plugin id. + /// Plugin manifest returned. + /// Plugin not found. + /// A on success, or a if the plugin could not be found. + [HttpPost("{pluginId}/Manifest")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetPluginManifest([FromRoute, Required] Guid pluginId) + { + var plugin = _pluginManager.GetPlugin(pluginId); + + if (plugin is not null) + { + return plugin.Manifest; + } + + return NotFound(); } } diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index 6dbcdae228..ce58725c37 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -13,126 +13,125 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Quick connect controller. +/// +public class QuickConnectController : BaseJellyfinApiController { + private readonly IQuickConnect _quickConnect; + private readonly IAuthorizationContext _authContext; + /// - /// Quick connect controller. + /// Initializes a new instance of the class. /// - public class QuickConnectController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext) { - private readonly IQuickConnect _quickConnect; - private readonly IAuthorizationContext _authContext; + _quickConnect = quickConnect; + _authContext = authContext; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext) + /// + /// Gets the current quick connect state. + /// + /// Quick connect state returned. + /// Whether Quick Connect is enabled on the server or not. + [HttpGet("Enabled")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetQuickConnectEnabled() + { + return _quickConnect.IsEnabled; + } + + /// + /// Initiate a new quick connect request. + /// + /// Quick connect request successfully created. + /// Quick connect is not active on this server. + /// A with a secret and code for future use or an error message. + [HttpPost("Initiate")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> InitiateQuickConnect() + { + try { - _quickConnect = quickConnect; - _authContext = authContext; + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); + return _quickConnect.TryConnect(auth); + } + catch (AuthenticationException) + { + return Unauthorized("Quick connect is disabled"); + } + } + + /// + /// Old version of using a GET method. + /// Still available to avoid breaking compatibility. + /// + /// The result of . + [Obsolete("Use POST request instead")] + [HttpGet("Initiate")] + [ApiExplorerSettings(IgnoreApi = true)] + public Task> InitiateQuickConnectLegacy() => InitiateQuickConnect(); + + /// + /// Attempts to retrieve authentication information. + /// + /// Secret previously returned from the Initiate endpoint. + /// Quick connect result returned. + /// Unknown quick connect secret. + /// An updated . + [HttpGet("Connect")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetQuickConnectState([FromQuery, Required] string secret) + { + try + { + return _quickConnect.CheckRequestStatus(secret); + } + catch (ResourceNotFoundException) + { + return NotFound("Unknown secret"); + } + catch (AuthenticationException) + { + return Unauthorized("Quick connect is disabled"); + } + } + + /// + /// Authorizes a pending quick connect request. + /// + /// Quick connect code to authorize. + /// The user the authorize. Access to the requested user is required. + /// Quick connect result authorized successfully. + /// Unknown user id. + /// Boolean indicating if the authorization was successful. + [HttpPost("Authorize")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null) + { + var currentUserId = User.GetUserId(); + var actualUserId = userId ?? currentUserId; + + if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator))) + { + return Forbid("Unknown user id"); } - /// - /// Gets the current quick connect state. - /// - /// Quick connect state returned. - /// Whether Quick Connect is enabled on the server or not. - [HttpGet("Enabled")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetQuickConnectEnabled() + try { - return _quickConnect.IsEnabled; + return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false); } - - /// - /// Initiate a new quick connect request. - /// - /// Quick connect request successfully created. - /// Quick connect is not active on this server. - /// A with a secret and code for future use or an error message. - [HttpPost("Initiate")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> InitiateQuickConnect() + catch (AuthenticationException) { - try - { - var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - return _quickConnect.TryConnect(auth); - } - catch (AuthenticationException) - { - return Unauthorized("Quick connect is disabled"); - } - } - - /// - /// Old version of using a GET method. - /// Still available to avoid breaking compatibility. - /// - /// The result of . - [Obsolete("Use POST request instead")] - [HttpGet("Initiate")] - [ApiExplorerSettings(IgnoreApi = true)] - public Task> InitiateQuickConnectLegacy() => InitiateQuickConnect(); - - /// - /// Attempts to retrieve authentication information. - /// - /// Secret previously returned from the Initiate endpoint. - /// Quick connect result returned. - /// Unknown quick connect secret. - /// An updated . - [HttpGet("Connect")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetQuickConnectState([FromQuery, Required] string secret) - { - try - { - return _quickConnect.CheckRequestStatus(secret); - } - catch (ResourceNotFoundException) - { - return NotFound("Unknown secret"); - } - catch (AuthenticationException) - { - return Unauthorized("Quick connect is disabled"); - } - } - - /// - /// Authorizes a pending quick connect request. - /// - /// Quick connect code to authorize. - /// The user the authorize. Access to the requested user is required. - /// Quick connect result authorized successfully. - /// Unknown user id. - /// Boolean indicating if the authorization was successful. - [HttpPost("Authorize")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null) - { - var currentUserId = User.GetUserId(); - var actualUserId = userId ?? currentUserId; - - if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator))) - { - return Forbid("Unknown user id"); - } - - try - { - return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false); - } - catch (AuthenticationException) - { - return Unauthorized("Quick connect is disabled"); - } + return Unauthorized("Quick connect is disabled"); } } } diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index da9e8cf90d..445c5594f8 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -15,165 +15,164 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Remote Images Controller. +/// +[Route("")] +public class RemoteImageController : BaseJellyfinApiController { + private readonly IProviderManager _providerManager; + private readonly IServerApplicationPaths _applicationPaths; + private readonly ILibraryManager _libraryManager; + /// - /// Remote Images Controller. + /// Initializes a new instance of the class. /// - [Route("")] - public class RemoteImageController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public RemoteImageController( + IProviderManager providerManager, + IServerApplicationPaths applicationPaths, + ILibraryManager libraryManager) { - private readonly IProviderManager _providerManager; - private readonly IServerApplicationPaths _applicationPaths; - private readonly ILibraryManager _libraryManager; + _providerManager = providerManager; + _applicationPaths = applicationPaths; + _libraryManager = libraryManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public RemoteImageController( - IProviderManager providerManager, - IServerApplicationPaths applicationPaths, - ILibraryManager libraryManager) + /// + /// Gets available remote images for an item. + /// + /// Item Id. + /// The image type. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. The image provider to use. + /// Optional. Include all languages. + /// Remote Images returned. + /// Item not found. + /// Remote Image Result. + [HttpGet("Items/{itemId}/RemoteImages")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetRemoteImages( + [FromRoute, Required] Guid itemId, + [FromQuery] ImageType? type, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string? providerName, + [FromQuery] bool includeAllLanguages = false) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) { - _providerManager = providerManager; - _applicationPaths = applicationPaths; - _libraryManager = libraryManager; + return NotFound(); } - /// - /// Gets available remote images for an item. - /// - /// Item Id. - /// The image type. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. The image provider to use. - /// Optional. Include all languages. - /// Remote Images returned. - /// Item not found. - /// Remote Image Result. - [HttpGet("Items/{itemId}/RemoteImages")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetRemoteImages( - [FromRoute, Required] Guid itemId, - [FromQuery] ImageType? type, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] string? providerName, - [FromQuery] bool includeAllLanguages = false) + var images = await _providerManager.GetAvailableRemoteImages( + item, + new RemoteImageQuery(providerName ?? string.Empty) + { + IncludeAllLanguages = includeAllLanguages, + IncludeDisabledProviders = true, + ImageType = type + }, + CancellationToken.None) + .ConfigureAwait(false); + + var imageArray = images.ToArray(); + var allProviders = _providerManager.GetRemoteImageProviderInfo(item); + if (type.HasValue) { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } - - var images = await _providerManager.GetAvailableRemoteImages( - item, - new RemoteImageQuery(providerName ?? string.Empty) - { - IncludeAllLanguages = includeAllLanguages, - IncludeDisabledProviders = true, - ImageType = type - }, - CancellationToken.None) - .ConfigureAwait(false); - - var imageArray = images.ToArray(); - var allProviders = _providerManager.GetRemoteImageProviderInfo(item); - if (type.HasValue) - { - allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value)); - } - - var result = new RemoteImageResult - { - TotalRecordCount = imageArray.Length, - Providers = allProviders.Select(o => o.Name) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray() - }; - - if (startIndex.HasValue) - { - imageArray = imageArray.Skip(startIndex.Value).ToArray(); - } - - if (limit.HasValue) - { - imageArray = imageArray.Take(limit.Value).ToArray(); - } - - result.Images = imageArray; - return result; + allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value)); } - /// - /// Gets available remote image providers for an item. - /// - /// Item Id. - /// Returned remote image providers. - /// Item not found. - /// List of remote image providers. - [HttpGet("Items/{itemId}/RemoteImages/Providers")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult> GetRemoteImageProviders([FromRoute, Required] Guid itemId) + var result = new RemoteImageResult { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } + TotalRecordCount = imageArray.Length, + Providers = allProviders.Select(o => o.Name) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() + }; - return Ok(_providerManager.GetRemoteImageProviderInfo(item)); + if (startIndex.HasValue) + { + imageArray = imageArray.Skip(startIndex.Value).ToArray(); } - /// - /// Downloads a remote image for an item. - /// - /// Item Id. - /// The image type. - /// The image url. - /// Remote image downloaded. - /// Remote image not found. - /// Download status. - [HttpPost("Items/{itemId}/RemoteImages/Download")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DownloadRemoteImage( - [FromRoute, Required] Guid itemId, - [FromQuery, Required] ImageType type, - [FromQuery] string? imageUrl) + if (limit.HasValue) { - var item = _libraryManager.GetItemById(itemId); - if (item is null) - { - return NotFound(); - } - - await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None) - .ConfigureAwait(false); - - await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); - return NoContent(); + imageArray = imageArray.Take(limit.Value).ToArray(); } - /// - /// Gets the full cache path. - /// - /// The filename. - /// System.String. - private string GetFullCachePath(string filename) + result.Images = imageArray; + return result; + } + + /// + /// Gets available remote image providers for an item. + /// + /// Item Id. + /// Returned remote image providers. + /// Item not found. + /// List of remote image providers. + [HttpGet("Items/{itemId}/RemoteImages/Providers")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetRemoteImageProviders([FromRoute, Required] Guid itemId) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) { - return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); + return NotFound(); } + + return Ok(_providerManager.GetRemoteImageProviderInfo(item)); + } + + /// + /// Downloads a remote image for an item. + /// + /// Item Id. + /// The image type. + /// The image url. + /// Remote image downloaded. + /// Remote image not found. + /// Download status. + [HttpPost("Items/{itemId}/RemoteImages/Download")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DownloadRemoteImage( + [FromRoute, Required] Guid itemId, + [FromQuery, Required] ImageType type, + [FromQuery] string? imageUrl) + { + var item = _libraryManager.GetItemById(itemId); + if (item is null) + { + return NotFound(); + } + + await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None) + .ConfigureAwait(false); + + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Gets the full cache path. + /// + /// The filename. + /// System.String. + private string GetFullCachePath(string filename) + { + return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); } } diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index 832e145050..c8fa11ac62 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -8,154 +8,153 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Scheduled Tasks Controller. +/// +[Authorize(Policy = Policies.RequiresElevation)] +public class ScheduledTasksController : BaseJellyfinApiController { + private readonly ITaskManager _taskManager; + /// - /// Scheduled Tasks Controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.RequiresElevation)] - public class ScheduledTasksController : BaseJellyfinApiController + /// Instance of the interface. + public ScheduledTasksController(ITaskManager taskManager) { - private readonly ITaskManager _taskManager; + _taskManager = taskManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - public ScheduledTasksController(ITaskManager taskManager) + /// + /// Get tasks. + /// + /// Optional filter tasks that are hidden, or not. + /// Optional filter tasks that are enabled, or not. + /// Scheduled tasks retrieved. + /// The list of scheduled tasks. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public IEnumerable GetTasks( + [FromQuery] bool? isHidden, + [FromQuery] bool? isEnabled) + { + IEnumerable tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name); + + foreach (var task in tasks) { - _taskManager = taskManager; - } - - /// - /// Get tasks. - /// - /// Optional filter tasks that are hidden, or not. - /// Optional filter tasks that are enabled, or not. - /// Scheduled tasks retrieved. - /// The list of scheduled tasks. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public IEnumerable GetTasks( - [FromQuery] bool? isHidden, - [FromQuery] bool? isEnabled) - { - IEnumerable tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name); - - foreach (var task in tasks) + if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask) { - if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask) + if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden) { - if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden) - { - continue; - } - - if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled) - { - continue; - } + continue; } - yield return ScheduledTaskHelpers.GetTaskInfo(task); - } - } - - /// - /// Get task by id. - /// - /// Task Id. - /// Task retrieved. - /// Task not found. - /// An containing the task on success, or a if the task could not be found. - [HttpGet("{taskId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetTask([FromRoute, Required] string taskId) - { - var task = _taskManager.ScheduledTasks.FirstOrDefault(i => - string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase)); - - if (task is null) - { - return NotFound(); + if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled) + { + continue; + } } - return ScheduledTaskHelpers.GetTaskInfo(task); - } - - /// - /// Start specified task. - /// - /// Task Id. - /// Task started. - /// Task not found. - /// An on success, or a if the file could not be found. - [HttpPost("Running/{taskId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult StartTask([FromRoute, Required] string taskId) - { - var task = _taskManager.ScheduledTasks.FirstOrDefault(o => - o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); - - if (task is null) - { - return NotFound(); - } - - _taskManager.Execute(task, new TaskOptions()); - return NoContent(); - } - - /// - /// Stop specified task. - /// - /// Task Id. - /// Task stopped. - /// Task not found. - /// An on success, or a if the file could not be found. - [HttpDelete("Running/{taskId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult StopTask([FromRoute, Required] string taskId) - { - var task = _taskManager.ScheduledTasks.FirstOrDefault(o => - o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); - - if (task is null) - { - return NotFound(); - } - - _taskManager.Cancel(task); - return NoContent(); - } - - /// - /// Update specified task triggers. - /// - /// Task Id. - /// Triggers. - /// Task triggers updated. - /// Task not found. - /// An on success, or a if the file could not be found. - [HttpPost("{taskId}/Triggers")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateTask( - [FromRoute, Required] string taskId, - [FromBody, Required] TaskTriggerInfo[] triggerInfos) - { - var task = _taskManager.ScheduledTasks.FirstOrDefault(o => - o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); - if (task is null) - { - return NotFound(); - } - - task.Triggers = triggerInfos; - return NoContent(); + yield return ScheduledTaskHelpers.GetTaskInfo(task); } } + + /// + /// Get task by id. + /// + /// Task Id. + /// Task retrieved. + /// Task not found. + /// An containing the task on success, or a if the task could not be found. + [HttpGet("{taskId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetTask([FromRoute, Required] string taskId) + { + var task = _taskManager.ScheduledTasks.FirstOrDefault(i => + string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase)); + + if (task is null) + { + return NotFound(); + } + + return ScheduledTaskHelpers.GetTaskInfo(task); + } + + /// + /// Start specified task. + /// + /// Task Id. + /// Task started. + /// Task not found. + /// An on success, or a if the file could not be found. + [HttpPost("Running/{taskId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult StartTask([FromRoute, Required] string taskId) + { + var task = _taskManager.ScheduledTasks.FirstOrDefault(o => + o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); + + if (task is null) + { + return NotFound(); + } + + _taskManager.Execute(task, new TaskOptions()); + return NoContent(); + } + + /// + /// Stop specified task. + /// + /// Task Id. + /// Task stopped. + /// Task not found. + /// An on success, or a if the file could not be found. + [HttpDelete("Running/{taskId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult StopTask([FromRoute, Required] string taskId) + { + var task = _taskManager.ScheduledTasks.FirstOrDefault(o => + o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); + + if (task is null) + { + return NotFound(); + } + + _taskManager.Cancel(task); + return NoContent(); + } + + /// + /// Update specified task triggers. + /// + /// Task Id. + /// Triggers. + /// Task triggers updated. + /// Task not found. + /// An on success, or a if the file could not be found. + [HttpPost("{taskId}/Triggers")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult UpdateTask( + [FromRoute, Required] string taskId, + [FromBody, Required] TaskTriggerInfo[] triggerInfos) + { + var task = _taskManager.ScheduledTasks.FirstOrDefault(o => + o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); + if (task is null) + { + return NotFound(); + } + + task.Triggers = triggerInfos; + return NoContent(); + } } diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 3b7719f373..46b4920cad 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -20,247 +20,246 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Search controller. +/// +[Route("Search/Hints")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class SearchController : BaseJellyfinApiController { + private readonly ISearchEngine _searchEngine; + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IImageProcessor _imageProcessor; + /// - /// Search controller. + /// Initializes a new instance of the class. /// - [Route("Search/Hints")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class SearchController : BaseJellyfinApiController + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public SearchController( + ISearchEngine searchEngine, + ILibraryManager libraryManager, + IDtoService dtoService, + IImageProcessor imageProcessor) { - private readonly ISearchEngine _searchEngine; - private readonly ILibraryManager _libraryManager; - private readonly IDtoService _dtoService; - private readonly IImageProcessor _imageProcessor; + _searchEngine = searchEngine; + _libraryManager = libraryManager; + _dtoService = dtoService; + _imageProcessor = imageProcessor; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - public SearchController( - ISearchEngine searchEngine, - ILibraryManager libraryManager, - IDtoService dtoService, - IImageProcessor imageProcessor) + /// + /// Gets the search hint result. + /// + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Supply a user id to search within a user's library or omit to search all. + /// The search term to filter on. + /// If specified, only results with the specified item types are returned. This allows multiple, comma delimited. + /// If specified, results with these item types are filtered out. This allows multiple, comma delimited. + /// If specified, only results with the specified media types are returned. This allows multiple, comma delimited. + /// If specified, only children of the parent are returned. + /// Optional filter for movies. + /// Optional filter for series. + /// Optional filter for news. + /// Optional filter for kids. + /// Optional filter for sports. + /// Optional filter whether to include people. + /// Optional filter whether to include media. + /// Optional filter whether to include genres. + /// Optional filter whether to include studios. + /// Optional filter whether to include artists. + /// Search hint returned. + /// An with the results of the search. + [HttpGet] + [Description("Gets search hints based on a search term")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetSearchHints( + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] Guid? userId, + [FromQuery, Required] string searchTerm, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery] Guid? parentId, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, + [FromQuery] bool includePeople = true, + [FromQuery] bool includeMedia = true, + [FromQuery] bool includeGenres = true, + [FromQuery] bool includeStudios = true, + [FromQuery] bool includeArtists = true) + { + var result = _searchEngine.GetSearchHints(new SearchQuery { - _searchEngine = searchEngine; - _libraryManager = libraryManager; - _dtoService = dtoService; - _imageProcessor = imageProcessor; - } + Limit = limit, + SearchTerm = searchTerm, + IncludeArtists = includeArtists, + IncludeGenres = includeGenres, + IncludeMedia = includeMedia, + IncludePeople = includePeople, + IncludeStudios = includeStudios, + StartIndex = startIndex, + UserId = userId ?? Guid.Empty, + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, + MediaTypes = mediaTypes, + ParentId = parentId, - /// - /// Gets the search hint result. - /// - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Supply a user id to search within a user's library or omit to search all. - /// The search term to filter on. - /// If specified, only results with the specified item types are returned. This allows multiple, comma delimited. - /// If specified, results with these item types are filtered out. This allows multiple, comma delimited. - /// If specified, only results with the specified media types are returned. This allows multiple, comma delimited. - /// If specified, only children of the parent are returned. - /// Optional filter for movies. - /// Optional filter for series. - /// Optional filter for news. - /// Optional filter for kids. - /// Optional filter for sports. - /// Optional filter whether to include people. - /// Optional filter whether to include media. - /// Optional filter whether to include genres. - /// Optional filter whether to include studios. - /// Optional filter whether to include artists. - /// Search hint returned. - /// An with the results of the search. - [HttpGet] - [Description("Gets search hints based on a search term")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetSearchHints( - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] Guid? userId, - [FromQuery, Required] string searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery] Guid? parentId, - [FromQuery] bool? isMovie, - [FromQuery] bool? isSeries, - [FromQuery] bool? isNews, - [FromQuery] bool? isKids, - [FromQuery] bool? isSports, - [FromQuery] bool includePeople = true, - [FromQuery] bool includeMedia = true, - [FromQuery] bool includeGenres = true, - [FromQuery] bool includeStudios = true, - [FromQuery] bool includeArtists = true) + IsKids = isKids, + IsMovie = isMovie, + IsNews = isNews, + IsSeries = isSeries, + IsSports = isSports + }); + + return new SearchHintResult(result.Items.Select(GetSearchHintResult).ToArray(), result.TotalRecordCount); + } + + /// + /// Gets the search hint result. + /// + /// The hint info. + /// SearchHintResult. + private SearchHint GetSearchHintResult(SearchHintInfo hintInfo) + { + var item = hintInfo.Item; + + var result = new SearchHint { - var result = _searchEngine.GetSearchHints(new SearchQuery - { - Limit = limit, - SearchTerm = searchTerm, - IncludeArtists = includeArtists, - IncludeGenres = includeGenres, - IncludeMedia = includeMedia, - IncludePeople = includePeople, - IncludeStudios = includeStudios, - StartIndex = startIndex, - UserId = userId ?? Guid.Empty, - IncludeItemTypes = includeItemTypes, - ExcludeItemTypes = excludeItemTypes, - MediaTypes = mediaTypes, - ParentId = parentId, - - IsKids = isKids, - IsMovie = isMovie, - IsNews = isNews, - IsSeries = isSeries, - IsSports = isSports - }); - - return new SearchHintResult(result.Items.Select(GetSearchHintResult).ToArray(), result.TotalRecordCount); - } - - /// - /// Gets the search hint result. - /// - /// The hint info. - /// SearchHintResult. - private SearchHint GetSearchHintResult(SearchHintInfo hintInfo) - { - var item = hintInfo.Item; - - var result = new SearchHint - { - Name = item.Name, - IndexNumber = item.IndexNumber, - ParentIndexNumber = item.ParentIndexNumber, - Id = item.Id, - Type = item.GetBaseItemKind(), - MediaType = item.MediaType, - MatchedTerm = hintInfo.MatchedTerm, - RunTimeTicks = item.RunTimeTicks, - ProductionYear = item.ProductionYear, - ChannelId = item.ChannelId, - EndDate = item.EndDate - }; + Name = item.Name, + IndexNumber = item.IndexNumber, + ParentIndexNumber = item.ParentIndexNumber, + Id = item.Id, + Type = item.GetBaseItemKind(), + MediaType = item.MediaType, + MatchedTerm = hintInfo.MatchedTerm, + RunTimeTicks = item.RunTimeTicks, + ProductionYear = item.ProductionYear, + ChannelId = item.ChannelId, + EndDate = item.EndDate + }; #pragma warning disable CS0618 - // Kept for compatibility with older clients - result.ItemId = result.Id; + // Kept for compatibility with older clients + result.ItemId = result.Id; #pragma warning restore CS0618 - if (item.IsFolder) - { - result.IsFolder = true; - } - - var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary); - - if (primaryImageTag is not null) - { - result.PrimaryImageTag = primaryImageTag; - result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item); - } - - SetThumbImageInfo(result, item); - SetBackdropImageInfo(result, item); - - switch (item) - { - case IHasSeries hasSeries: - result.Series = hasSeries.SeriesName; - break; - case LiveTvProgram program: - result.StartDate = program.StartDate; - break; - case Series series: - if (series.Status.HasValue) - { - result.Status = series.Status.Value.ToString(); - } - - break; - case MusicAlbum album: - result.Artists = album.Artists; - result.AlbumArtist = album.AlbumArtist; - break; - case Audio song: - result.AlbumArtist = song.AlbumArtists?.FirstOrDefault(); - result.Artists = song.Artists; - - MusicAlbum musicAlbum = song.AlbumEntity; - - if (musicAlbum is not null) - { - result.Album = musicAlbum.Name; - result.AlbumId = musicAlbum.Id; - } - else - { - result.Album = song.Album; - } - - break; - } - - if (!item.ChannelId.Equals(default)) - { - var channel = _libraryManager.GetItemById(item.ChannelId); - result.ChannelName = channel?.Name; - } - - return result; + if (item.IsFolder) + { + result.IsFolder = true; } - private void SetThumbImageInfo(SearchHint hint, BaseItem item) + var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary); + + if (primaryImageTag is not null) { - var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null; + result.PrimaryImageTag = primaryImageTag; + result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item); + } - if (itemWithImage is null && item is Episode) - { - itemWithImage = GetParentWithImage(item, ImageType.Thumb); - } + SetThumbImageInfo(result, item); + SetBackdropImageInfo(result, item); - itemWithImage ??= GetParentWithImage(item, ImageType.Thumb); - - if (itemWithImage is not null) - { - var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb); - - if (tag is not null) + switch (item) + { + case IHasSeries hasSeries: + result.Series = hasSeries.SeriesName; + break; + case LiveTvProgram program: + result.StartDate = program.StartDate; + break; + case Series series: + if (series.Status.HasValue) { - hint.ThumbImageTag = tag; - hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture); + result.Status = series.Status.Value.ToString(); } - } - } - private void SetBackdropImageInfo(SearchHint hint, BaseItem item) - { - var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null) - ?? GetParentWithImage(item, ImageType.Backdrop); + break; + case MusicAlbum album: + result.Artists = album.Artists; + result.AlbumArtist = album.AlbumArtist; + break; + case Audio song: + result.AlbumArtist = song.AlbumArtists?.FirstOrDefault(); + result.Artists = song.Artists; - if (itemWithImage is not null) - { - var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop); + MusicAlbum musicAlbum = song.AlbumEntity; - if (tag is not null) + if (musicAlbum is not null) { - hint.BackdropImageTag = tag; - hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture); + result.Album = musicAlbum.Name; + result.AlbumId = musicAlbum.Id; } - } + else + { + result.Album = song.Album; + } + + break; } - private T? GetParentWithImage(BaseItem item, ImageType type) - where T : BaseItem + if (!item.ChannelId.Equals(default)) { - return item.GetParents().OfType().FirstOrDefault(i => i.HasImage(type)); + var channel = _libraryManager.GetItemById(item.ChannelId); + result.ChannelName = channel?.Name; + } + + return result; + } + + private void SetThumbImageInfo(SearchHint hint, BaseItem item) + { + var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null; + + if (itemWithImage is null && item is Episode) + { + itemWithImage = GetParentWithImage(item, ImageType.Thumb); + } + + itemWithImage ??= GetParentWithImage(item, ImageType.Thumb); + + if (itemWithImage is not null) + { + var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb); + + if (tag is not null) + { + hint.ThumbImageTag = tag; + hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture); + } } } + + private void SetBackdropImageInfo(SearchHint hint, BaseItem item) + { + var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null) + ?? GetParentWithImage(item, ImageType.Backdrop); + + if (itemWithImage is not null) + { + var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop); + + if (tag is not null) + { + hint.BackdropImageTag = tag; + hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture); + } + } + } + + private T? GetParentWithImage(BaseItem item, ImageType type) + where T : BaseItem + { + return item.GetParents().OfType().FirstOrDefault(i => i.HasImage(type)); + } } diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 25f9301351..ef33644785 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -19,480 +19,479 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The session controller. +/// +[Route("")] +public class SessionController : BaseJellyfinApiController { + private readonly ISessionManager _sessionManager; + private readonly IUserManager _userManager; + private readonly IDeviceManager _deviceManager; + /// - /// The session controller. + /// Initializes a new instance of the class. /// - [Route("")] - public class SessionController : BaseJellyfinApiController + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public SessionController( + ISessionManager sessionManager, + IUserManager userManager, + IDeviceManager deviceManager) { - private readonly ISessionManager _sessionManager; - private readonly IUserManager _userManager; - private readonly IDeviceManager _deviceManager; + _sessionManager = sessionManager; + _userManager = userManager; + _deviceManager = deviceManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - public SessionController( - ISessionManager sessionManager, - IUserManager userManager, - IDeviceManager deviceManager) + /// + /// Gets a list of sessions. + /// + /// Filter by sessions that a given user is allowed to remote control. + /// Filter by device Id. + /// Optional. Filter by sessions that were active in the last n seconds. + /// List of sessions returned. + /// An with the available sessions. + [HttpGet("Sessions")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetSessions( + [FromQuery] Guid? controllableByUserId, + [FromQuery] string? deviceId, + [FromQuery] int? activeWithinSeconds) + { + var result = _sessionManager.Sessions; + + if (!string.IsNullOrEmpty(deviceId)) { - _sessionManager = sessionManager; - _userManager = userManager; - _deviceManager = deviceManager; + result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); } - /// - /// Gets a list of sessions. - /// - /// Filter by sessions that a given user is allowed to remote control. - /// Filter by device Id. - /// Optional. Filter by sessions that were active in the last n seconds. - /// List of sessions returned. - /// An with the available sessions. - [HttpGet("Sessions")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSessions( - [FromQuery] Guid? controllableByUserId, - [FromQuery] string? deviceId, - [FromQuery] int? activeWithinSeconds) + if (controllableByUserId.HasValue && !controllableByUserId.Equals(default)) { - var result = _sessionManager.Sessions; + result = result.Where(i => i.SupportsRemoteControl); - if (!string.IsNullOrEmpty(deviceId)) + var user = _userManager.GetUserById(controllableByUserId.Value); + + if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) { - result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); + result = result.Where(i => i.UserId.Equals(default) || i.ContainsUser(controllableByUserId.Value)); } - if (controllableByUserId.HasValue && !controllableByUserId.Equals(default)) + if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) { - result = result.Where(i => i.SupportsRemoteControl); + result = result.Where(i => !i.UserId.Equals(default)); + } - var user = _userManager.GetUserById(controllableByUserId.Value); + if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) + { + var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); + result = result.Where(i => i.LastActivityDate >= minActiveDate); + } - if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) + result = result.Where(i => + { + if (!string.IsNullOrWhiteSpace(i.DeviceId)) { - result = result.Where(i => i.UserId.Equals(default) || i.ContainsUser(controllableByUserId.Value)); - } - - if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) - { - result = result.Where(i => !i.UserId.Equals(default)); - } - - if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) - { - var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); - result = result.Where(i => i.LastActivityDate >= minActiveDate); - } - - result = result.Where(i => - { - if (!string.IsNullOrWhiteSpace(i.DeviceId)) + if (!_deviceManager.CanAccessDevice(user, i.DeviceId)) { - if (!_deviceManager.CanAccessDevice(user, i.DeviceId)) - { - return false; - } + return false; } + } - return true; - }); - } - - return Ok(result); - } - - /// - /// Instructs a session to browse to an item or view. - /// - /// The session Id. - /// The type of item to browse to. - /// The Id of the item. - /// The name of the item. - /// Instruction sent to session. - /// A . - [HttpPost("Sessions/{sessionId}/Viewing")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task DisplayContent( - [FromRoute, Required] string sessionId, - [FromQuery, Required] BaseItemKind itemType, - [FromQuery, Required] string itemId, - [FromQuery, Required] string itemName) - { - var command = new BrowseRequest - { - ItemId = itemId, - ItemName = itemName, - ItemType = itemType - }; - - await _sessionManager.SendBrowseCommand( - await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), - sessionId, - command, - CancellationToken.None) - .ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Instructs a session to play an item. - /// - /// The session id. - /// The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now. - /// The ids of the items to play, comma delimited. - /// The starting position of the first item. - /// Optional. The media source id. - /// Optional. The index of the audio stream to play. - /// Optional. The index of the subtitle stream to play. - /// Optional. The start index. - /// Instruction sent to session. - /// A . - [HttpPost("Sessions/{sessionId}/Playing")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task Play( - [FromRoute, Required] string sessionId, - [FromQuery, Required] PlayCommand playCommand, - [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds, - [FromQuery] long? startPositionTicks, - [FromQuery] string? mediaSourceId, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] int? startIndex) - { - var playRequest = new PlayRequest - { - ItemIds = itemIds, - StartPositionTicks = startPositionTicks, - PlayCommand = playCommand, - MediaSourceId = mediaSourceId, - AudioStreamIndex = audioStreamIndex, - SubtitleStreamIndex = subtitleStreamIndex, - StartIndex = startIndex - }; - - await _sessionManager.SendPlayCommand( - await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), - sessionId, - playRequest, - CancellationToken.None) - .ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Issues a playstate command to a client. - /// - /// The session id. - /// The . - /// The optional position ticks. - /// The optional controlling user id. - /// Playstate command sent to session. - /// A . - [HttpPost("Sessions/{sessionId}/Playing/{command}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task SendPlaystateCommand( - [FromRoute, Required] string sessionId, - [FromRoute, Required] PlaystateCommand command, - [FromQuery] long? seekPositionTicks, - [FromQuery] string? controllingUserId) - { - await _sessionManager.SendPlaystateCommand( - await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), - sessionId, - new PlaystateRequest() - { - Command = command, - ControllingUserId = controllingUserId, - SeekPositionTicks = seekPositionTicks, - }, - CancellationToken.None) - .ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Issues a system command to a client. - /// - /// The session id. - /// The command to send. - /// System command sent to session. - /// A . - [HttpPost("Sessions/{sessionId}/System/{command}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task SendSystemCommand( - [FromRoute, Required] string sessionId, - [FromRoute, Required] GeneralCommandType command) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var generalCommand = new GeneralCommand - { - Name = command, - ControllingUserId = currentSession.UserId - }; - - await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None).ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Issues a general command to a client. - /// - /// The session id. - /// The command to send. - /// General command sent to session. - /// A . - [HttpPost("Sessions/{sessionId}/Command/{command}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task SendGeneralCommand( - [FromRoute, Required] string sessionId, - [FromRoute, Required] GeneralCommandType command) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - - var generalCommand = new GeneralCommand - { - Name = command, - ControllingUserId = currentSession.UserId - }; - - await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None) - .ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Issues a full general command to a client. - /// - /// The session id. - /// The . - /// Full general command sent to session. - /// A . - [HttpPost("Sessions/{sessionId}/Command")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task SendFullGeneralCommand( - [FromRoute, Required] string sessionId, - [FromBody, Required] GeneralCommand command) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - - ArgumentNullException.ThrowIfNull(command); - - command.ControllingUserId = currentSession.UserId; - - await _sessionManager.SendGeneralCommand( - currentSession.Id, - sessionId, - command, - CancellationToken.None) - .ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Issues a command to a client to display a message to the user. - /// - /// The session id. - /// The object containing Header, Message Text, and TimeoutMs. - /// Message sent. - /// A . - [HttpPost("Sessions/{sessionId}/Message")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task SendMessageCommand( - [FromRoute, Required] string sessionId, - [FromBody, Required] MessageCommand command) - { - if (string.IsNullOrWhiteSpace(command.Header)) - { - command.Header = "Message from Server"; - } - - await _sessionManager.SendMessageCommand( - await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), - sessionId, - command, - CancellationToken.None) - .ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Adds an additional user to a session. - /// - /// The session id. - /// The user id. - /// User added to session. - /// A . - [HttpPost("Sessions/{sessionId}/User/{userId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult AddUserToSession( - [FromRoute, Required] string sessionId, - [FromRoute, Required] Guid userId) - { - _sessionManager.AddAdditionalUser(sessionId, userId); - return NoContent(); - } - - /// - /// Removes an additional user from a session. - /// - /// The session id. - /// The user id. - /// User removed from session. - /// A . - [HttpDelete("Sessions/{sessionId}/User/{userId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RemoveUserFromSession( - [FromRoute, Required] string sessionId, - [FromRoute, Required] Guid userId) - { - _sessionManager.RemoveAdditionalUser(sessionId, userId); - return NoContent(); - } - - /// - /// Updates capabilities for a device. - /// - /// The session id. - /// A list of playable media types, comma delimited. Audio, Video, Book, Photo. - /// A list of supported remote control commands, comma delimited. - /// Determines whether media can be played remotely.. - /// Determines whether sync is supported. - /// Determines whether the device supports a unique identifier. - /// Capabilities posted. - /// A . - [HttpPost("Sessions/Capabilities")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task PostCapabilities( - [FromQuery] string? id, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, - [FromQuery] bool supportsMediaControl = false, - [FromQuery] bool supportsSync = false, - [FromQuery] bool supportsPersistentIdentifier = true) - { - if (string.IsNullOrWhiteSpace(id)) - { - id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - } - - _sessionManager.ReportCapabilities(id, new ClientCapabilities - { - PlayableMediaTypes = playableMediaTypes, - SupportedCommands = supportedCommands, - SupportsMediaControl = supportsMediaControl, - SupportsSync = supportsSync, - SupportsPersistentIdentifier = supportsPersistentIdentifier + return true; }); - return NoContent(); } - /// - /// Updates capabilities for a device. - /// - /// The session id. - /// The . - /// Capabilities updated. - /// A . - [HttpPost("Sessions/Capabilities/Full")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task PostFullCapabilities( - [FromQuery] string? id, - [FromBody, Required] ClientCapabilitiesDto capabilities) + return Ok(result); + } + + /// + /// Instructs a session to browse to an item or view. + /// + /// The session Id. + /// The type of item to browse to. + /// The Id of the item. + /// The name of the item. + /// Instruction sent to session. + /// A . + [HttpPost("Sessions/{sessionId}/Viewing")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task DisplayContent( + [FromRoute, Required] string sessionId, + [FromQuery, Required] BaseItemKind itemType, + [FromQuery, Required] string itemId, + [FromQuery, Required] string itemName) + { + var command = new BrowseRequest { - if (string.IsNullOrWhiteSpace(id)) + ItemId = itemId, + ItemName = itemName, + ItemType = itemType + }; + + await _sessionManager.SendBrowseCommand( + await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), + sessionId, + command, + CancellationToken.None) + .ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Instructs a session to play an item. + /// + /// The session id. + /// The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now. + /// The ids of the items to play, comma delimited. + /// The starting position of the first item. + /// Optional. The media source id. + /// Optional. The index of the audio stream to play. + /// Optional. The index of the subtitle stream to play. + /// Optional. The start index. + /// Instruction sent to session. + /// A . + [HttpPost("Sessions/{sessionId}/Playing")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task Play( + [FromRoute, Required] string sessionId, + [FromQuery, Required] PlayCommand playCommand, + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds, + [FromQuery] long? startPositionTicks, + [FromQuery] string? mediaSourceId, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] int? startIndex) + { + var playRequest = new PlayRequest + { + ItemIds = itemIds, + StartPositionTicks = startPositionTicks, + PlayCommand = playCommand, + MediaSourceId = mediaSourceId, + AudioStreamIndex = audioStreamIndex, + SubtitleStreamIndex = subtitleStreamIndex, + StartIndex = startIndex + }; + + await _sessionManager.SendPlayCommand( + await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), + sessionId, + playRequest, + CancellationToken.None) + .ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Issues a playstate command to a client. + /// + /// The session id. + /// The . + /// The optional position ticks. + /// The optional controlling user id. + /// Playstate command sent to session. + /// A . + [HttpPost("Sessions/{sessionId}/Playing/{command}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task SendPlaystateCommand( + [FromRoute, Required] string sessionId, + [FromRoute, Required] PlaystateCommand command, + [FromQuery] long? seekPositionTicks, + [FromQuery] string? controllingUserId) + { + await _sessionManager.SendPlaystateCommand( + await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), + sessionId, + new PlaystateRequest() { - id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - } + Command = command, + ControllingUserId = controllingUserId, + SeekPositionTicks = seekPositionTicks, + }, + CancellationToken.None) + .ConfigureAwait(false); - _sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities()); + return NoContent(); + } - return NoContent(); - } - - /// - /// Reports that a session is viewing an item. - /// - /// The session id. - /// The item id. - /// Session reported to server. - /// A . - [HttpPost("Sessions/Viewing")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task ReportViewing( - [FromQuery] string? sessionId, - [FromQuery, Required] string? itemId) + /// + /// Issues a system command to a client. + /// + /// The session id. + /// The command to send. + /// System command sent to session. + /// A . + [HttpPost("Sessions/{sessionId}/System/{command}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task SendSystemCommand( + [FromRoute, Required] string sessionId, + [FromRoute, Required] GeneralCommandType command) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var generalCommand = new GeneralCommand { - string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + Name = command, + ControllingUserId = currentSession.UserId + }; - _sessionManager.ReportNowViewingItem(session, itemId); - return NoContent(); - } + await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None).ConfigureAwait(false); - /// - /// Reports that a session has ended. - /// - /// Session end reported to server. - /// A . - [HttpPost("Sessions/Logout")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task ReportSessionEnded() + return NoContent(); + } + + /// + /// Issues a general command to a client. + /// + /// The session id. + /// The command to send. + /// General command sent to session. + /// A . + [HttpPost("Sessions/{sessionId}/Command/{command}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task SendGeneralCommand( + [FromRoute, Required] string sessionId, + [FromRoute, Required] GeneralCommandType command) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + + var generalCommand = new GeneralCommand { - await _sessionManager.Logout(User.GetToken()).ConfigureAwait(false); - return NoContent(); + Name = command, + ControllingUserId = currentSession.UserId + }; + + await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None) + .ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Issues a full general command to a client. + /// + /// The session id. + /// The . + /// Full general command sent to session. + /// A . + [HttpPost("Sessions/{sessionId}/Command")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task SendFullGeneralCommand( + [FromRoute, Required] string sessionId, + [FromBody, Required] GeneralCommand command) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + + ArgumentNullException.ThrowIfNull(command); + + command.ControllingUserId = currentSession.UserId; + + await _sessionManager.SendGeneralCommand( + currentSession.Id, + sessionId, + command, + CancellationToken.None) + .ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Issues a command to a client to display a message to the user. + /// + /// The session id. + /// The object containing Header, Message Text, and TimeoutMs. + /// Message sent. + /// A . + [HttpPost("Sessions/{sessionId}/Message")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task SendMessageCommand( + [FromRoute, Required] string sessionId, + [FromBody, Required] MessageCommand command) + { + if (string.IsNullOrWhiteSpace(command.Header)) + { + command.Header = "Message from Server"; } - /// - /// Get all auth providers. - /// - /// Auth providers retrieved. - /// An with the auth providers. - [HttpGet("Auth/Providers")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetAuthProviders() + await _sessionManager.SendMessageCommand( + await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), + sessionId, + command, + CancellationToken.None) + .ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Adds an additional user to a session. + /// + /// The session id. + /// The user id. + /// User added to session. + /// A . + [HttpPost("Sessions/{sessionId}/User/{userId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult AddUserToSession( + [FromRoute, Required] string sessionId, + [FromRoute, Required] Guid userId) + { + _sessionManager.AddAdditionalUser(sessionId, userId); + return NoContent(); + } + + /// + /// Removes an additional user from a session. + /// + /// The session id. + /// The user id. + /// User removed from session. + /// A . + [HttpDelete("Sessions/{sessionId}/User/{userId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RemoveUserFromSession( + [FromRoute, Required] string sessionId, + [FromRoute, Required] Guid userId) + { + _sessionManager.RemoveAdditionalUser(sessionId, userId); + return NoContent(); + } + + /// + /// Updates capabilities for a device. + /// + /// The session id. + /// A list of playable media types, comma delimited. Audio, Video, Book, Photo. + /// A list of supported remote control commands, comma delimited. + /// Determines whether media can be played remotely.. + /// Determines whether sync is supported. + /// Determines whether the device supports a unique identifier. + /// Capabilities posted. + /// A . + [HttpPost("Sessions/Capabilities")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task PostCapabilities( + [FromQuery] string? id, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, + [FromQuery] bool supportsMediaControl = false, + [FromQuery] bool supportsSync = false, + [FromQuery] bool supportsPersistentIdentifier = true) + { + if (string.IsNullOrWhiteSpace(id)) { - return _userManager.GetAuthenticationProviders(); + id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); } - /// - /// Get all password reset providers. - /// - /// Password reset providers retrieved. - /// An with the password reset providers. - [HttpGet("Auth/PasswordResetProviders")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.RequiresElevation)] - public ActionResult> GetPasswordResetProviders() + _sessionManager.ReportCapabilities(id, new ClientCapabilities { - return _userManager.GetPasswordResetProviders(); + PlayableMediaTypes = playableMediaTypes, + SupportedCommands = supportedCommands, + SupportsMediaControl = supportsMediaControl, + SupportsSync = supportsSync, + SupportsPersistentIdentifier = supportsPersistentIdentifier + }); + return NoContent(); + } + + /// + /// Updates capabilities for a device. + /// + /// The session id. + /// The . + /// Capabilities updated. + /// A . + [HttpPost("Sessions/Capabilities/Full")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task PostFullCapabilities( + [FromQuery] string? id, + [FromBody, Required] ClientCapabilitiesDto capabilities) + { + if (string.IsNullOrWhiteSpace(id)) + { + id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); } + + _sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities()); + + return NoContent(); + } + + /// + /// Reports that a session is viewing an item. + /// + /// The session id. + /// The item id. + /// Session reported to server. + /// A . + [HttpPost("Sessions/Viewing")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task ReportViewing( + [FromQuery] string? sessionId, + [FromQuery, Required] string? itemId) + { + string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + + _sessionManager.ReportNowViewingItem(session, itemId); + return NoContent(); + } + + /// + /// Reports that a session has ended. + /// + /// Session end reported to server. + /// A . + [HttpPost("Sessions/Logout")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task ReportSessionEnded() + { + await _sessionManager.Logout(User.GetToken()).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Get all auth providers. + /// + /// Auth providers retrieved. + /// An with the auth providers. + [HttpGet("Auth/Providers")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetAuthProviders() + { + return _userManager.GetAuthenticationProviders(); + } + + /// + /// Get all password reset providers. + /// + /// Password reset providers retrieved. + /// An with the password reset providers. + [HttpGet("Auth/PasswordResetProviders")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.RequiresElevation)] + public ActionResult> GetPasswordResetProviders() + { + return _userManager.GetPasswordResetProviders(); } } diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index eec5779e64..aab390d1f9 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -10,141 +10,140 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The startup wizard controller. +/// +[Authorize(Policy = Policies.FirstTimeSetupOrElevated)] +public class StartupController : BaseJellyfinApiController { + private readonly IServerConfigurationManager _config; + private readonly IUserManager _userManager; + /// - /// The startup wizard controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] - public class StartupController : BaseJellyfinApiController + /// The server configuration manager. + /// The user manager. + public StartupController(IServerConfigurationManager config, IUserManager userManager) { - private readonly IServerConfigurationManager _config; - private readonly IUserManager _userManager; + _config = config; + _userManager = userManager; + } - /// - /// Initializes a new instance of the class. - /// - /// The server configuration manager. - /// The user manager. - public StartupController(IServerConfigurationManager config, IUserManager userManager) + /// + /// Completes the startup wizard. + /// + /// Startup wizard completed. + /// A indicating success. + [HttpPost("Complete")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult CompleteWizard() + { + _config.Configuration.IsStartupWizardCompleted = true; + _config.SaveConfiguration(); + return NoContent(); + } + + /// + /// Gets the initial startup wizard configuration. + /// + /// Initial startup wizard configuration retrieved. + /// An containing the initial startup wizard configuration. + [HttpGet("Configuration")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetStartupConfiguration() + { + return new StartupConfigurationDto { - _config = config; - _userManager = userManager; + UICulture = _config.Configuration.UICulture, + MetadataCountryCode = _config.Configuration.MetadataCountryCode, + PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage + }; + } + + /// + /// Sets the initial startup wizard configuration. + /// + /// The updated startup configuration. + /// Configuration saved. + /// A indicating success. + [HttpPost("Configuration")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration) + { + _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty; + _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty; + _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty; + _config.SaveConfiguration(); + return NoContent(); + } + + /// + /// Sets remote access and UPnP. + /// + /// The startup remote access dto. + /// Configuration saved. + /// A indicating success. + [HttpPost("RemoteAccess")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto) + { + NetworkConfiguration settings = _config.GetNetworkConfiguration(); + settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess; + settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping; + _config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings); + return NoContent(); + } + + /// + /// Gets the first user. + /// + /// Initial user retrieved. + /// The first user. + [HttpGet("User")] + [HttpGet("FirstUser", Name = "GetFirstUser_2")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetFirstUser() + { + // TODO: Remove this method when startup wizard no longer requires an existing user. + await _userManager.InitializeAsync().ConfigureAwait(false); + var user = _userManager.Users.First(); + return new StartupUserDto + { + Name = user.Username, + Password = user.Password + }; + } + + /// + /// Sets the user name and password. + /// + /// The DTO containing username and password. + /// Updated user name and password. + /// + /// A that represents the asynchronous update operation. + /// The task result contains a indicating success. + /// + [HttpPost("User")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task UpdateStartupUser([FromBody] StartupUserDto startupUserDto) + { + var user = _userManager.Users.First(); + + if (startupUserDto.Name is not null) + { + user.Username = startupUserDto.Name; } - /// - /// Completes the startup wizard. - /// - /// Startup wizard completed. - /// A indicating success. - [HttpPost("Complete")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CompleteWizard() + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(startupUserDto.Password)) { - _config.Configuration.IsStartupWizardCompleted = true; - _config.SaveConfiguration(); - return NoContent(); + await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); } - /// - /// Gets the initial startup wizard configuration. - /// - /// Initial startup wizard configuration retrieved. - /// An containing the initial startup wizard configuration. - [HttpGet("Configuration")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetStartupConfiguration() - { - return new StartupConfigurationDto - { - UICulture = _config.Configuration.UICulture, - MetadataCountryCode = _config.Configuration.MetadataCountryCode, - PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage - }; - } - - /// - /// Sets the initial startup wizard configuration. - /// - /// The updated startup configuration. - /// Configuration saved. - /// A indicating success. - [HttpPost("Configuration")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration) - { - _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty; - _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty; - _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty; - _config.SaveConfiguration(); - return NoContent(); - } - - /// - /// Sets remote access and UPnP. - /// - /// The startup remote access dto. - /// Configuration saved. - /// A indicating success. - [HttpPost("RemoteAccess")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto) - { - NetworkConfiguration settings = _config.GetNetworkConfiguration(); - settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess; - settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping; - _config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings); - return NoContent(); - } - - /// - /// Gets the first user. - /// - /// Initial user retrieved. - /// The first user. - [HttpGet("User")] - [HttpGet("FirstUser", Name = "GetFirstUser_2")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task GetFirstUser() - { - // TODO: Remove this method when startup wizard no longer requires an existing user. - await _userManager.InitializeAsync().ConfigureAwait(false); - var user = _userManager.Users.First(); - return new StartupUserDto - { - Name = user.Username, - Password = user.Password - }; - } - - /// - /// Sets the user name and password. - /// - /// The DTO containing username and password. - /// Updated user name and password. - /// - /// A that represents the asynchronous update operation. - /// The task result contains a indicating success. - /// - [HttpPost("User")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task UpdateStartupUser([FromBody] StartupUserDto startupUserDto) - { - var user = _userManager.Users.First(); - - if (startupUserDto.Name is not null) - { - user.Username = startupUserDto.Name; - } - - await _userManager.UpdateUserAsync(user).ConfigureAwait(false); - - if (!string.IsNullOrEmpty(startupUserDto.Password)) - { - await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); - } - - return NoContent(); - } + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 1288fb5124..799be2ae86 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -16,141 +16,140 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Studios controller. +/// +[Authorize(Policy = Policies.DefaultAuthorization)] +public class StudiosController : BaseJellyfinApiController { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; + /// - /// Studios controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.DefaultAuthorization)] - public class StudiosController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public StudiosController( + ILibraryManager libraryManager, + IUserManager userManager, + IDtoService dtoService) { - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDtoService _dtoService; + _libraryManager = libraryManager; + _userManager = userManager; + _dtoService = dtoService; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public StudiosController( - ILibraryManager libraryManager, - IUserManager userManager, - IDtoService dtoService) + /// + /// Gets all studios from a given item, folder, or the entire library. + /// + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Search term. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional filter by items that are marked as favorite, or not. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// User id. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional, include image information in output. + /// Total record count. + /// Studios returned. + /// An containing the studios. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetStudios( + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] string? searchTerm, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery] bool? isFavorite, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] Guid? userId, + [FromQuery] string? nameStartsWithOrGreater, + [FromQuery] string? nameStartsWith, + [FromQuery] string? nameLessThan, + [FromQuery] bool? enableImages = true, + [FromQuery] bool enableTotalRecordCount = true) + { + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var parentItem = _libraryManager.GetParentItem(parentId, userId); + + var query = new InternalItemsQuery(user) { - _libraryManager = libraryManager; - _userManager = userManager; - _dtoService = dtoService; - } + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + StartIndex = startIndex, + Limit = limit, + IsFavorite = isFavorite, + NameLessThan = nameLessThan, + NameStartsWith = nameStartsWith, + NameStartsWithOrGreater = nameStartsWithOrGreater, + DtoOptions = dtoOptions, + SearchTerm = searchTerm, + EnableTotalRecordCount = enableTotalRecordCount + }; - /// - /// Gets all studios from a given item, folder, or the entire library. - /// - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Search term. - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional filter by items that are marked as favorite, or not. - /// Optional, include user data. - /// Optional, the max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// User id. - /// Optional filter by items whose name is sorted equally or greater than a given input string. - /// Optional filter by items whose name is sorted equally than a given input string. - /// Optional filter by items whose name is equally or lesser than a given input string. - /// Optional, include image information in output. - /// Total record count. - /// Studios returned. - /// An containing the studios. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetStudios( - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] string? searchTerm, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery] bool? isFavorite, - [FromQuery] bool? enableUserData, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] Guid? userId, - [FromQuery] string? nameStartsWithOrGreater, - [FromQuery] string? nameStartsWith, - [FromQuery] string? nameLessThan, - [FromQuery] bool? enableImages = true, - [FromQuery] bool enableTotalRecordCount = true) + if (parentId.HasValue) { - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - - User? user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var parentItem = _libraryManager.GetParentItem(parentId, userId); - - var query = new InternalItemsQuery(user) + if (parentItem is Folder) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, - StartIndex = startIndex, - Limit = limit, - IsFavorite = isFavorite, - NameLessThan = nameLessThan, - NameStartsWith = nameStartsWith, - NameStartsWithOrGreater = nameStartsWithOrGreater, - DtoOptions = dtoOptions, - SearchTerm = searchTerm, - EnableTotalRecordCount = enableTotalRecordCount - }; - - if (parentId.HasValue) - { - if (parentItem is Folder) - { - query.AncestorIds = new[] { parentId.Value }; - } - else - { - query.ItemIds = new[] { parentId.Value }; - } + query.AncestorIds = new[] { parentId.Value }; } - - var result = _libraryManager.GetStudios(query); - var shouldIncludeItemTypes = includeItemTypes.Length != 0; - return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); - } - - /// - /// Gets a studio by name. - /// - /// Studio name. - /// Optional. Filter by user id, and attach user data. - /// Studio returned. - /// An containing the studio. - [HttpGet("{name}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId) - { - var dtoOptions = new DtoOptions().AddClientFields(User); - - var item = _libraryManager.GetStudio(name); - if (userId.HasValue && !userId.Equals(default)) + else { - var user = _userManager.GetUserById(userId.Value); - - return _dtoService.GetBaseItemDto(item, dtoOptions, user); + query.ItemIds = new[] { parentId.Value }; } - - return _dtoService.GetBaseItemDto(item, dtoOptions); } + + var result = _libraryManager.GetStudios(query); + var shouldIncludeItemTypes = includeItemTypes.Length != 0; + return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); + } + + /// + /// Gets a studio by name. + /// + /// Studio name. + /// Optional. Filter by user id, and attach user data. + /// Studio returned. + /// An containing the studio. + [HttpGet("{name}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId) + { + var dtoOptions = new DtoOptions().AddClientFields(User); + + var item = _libraryManager.GetStudio(name); + if (userId.HasValue && !userId.Equals(default)) + { + var user = _userManager.GetUserById(userId.Value); + + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + return _dtoService.GetBaseItemDto(item, dtoOptions); } } diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index c3ce1868e2..fd0a71f9e3 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -30,522 +30,521 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Subtitle controller. +/// +[Route("")] +public class SubtitleController : BaseJellyfinApiController { + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly ILibraryManager _libraryManager; + private readonly ISubtitleManager _subtitleManager; + private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + /// - /// Subtitle controller. + /// Initializes a new instance of the class. /// - [Route("")] - public class SubtitleController : BaseJellyfinApiController + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public SubtitleController( + IServerConfigurationManager serverConfigurationManager, + ILibraryManager libraryManager, + ISubtitleManager subtitleManager, + ISubtitleEncoder subtitleEncoder, + IMediaSourceManager mediaSourceManager, + IProviderManager providerManager, + IFileSystem fileSystem, + ILogger logger) { - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly ILibraryManager _libraryManager; - private readonly ISubtitleManager _subtitleManager; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IProviderManager _providerManager; - private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; + _serverConfigurationManager = serverConfigurationManager; + _libraryManager = libraryManager; + _subtitleManager = subtitleManager; + _subtitleEncoder = subtitleEncoder; + _mediaSourceManager = mediaSourceManager; + _providerManager = providerManager; + _fileSystem = fileSystem; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - public SubtitleController( - IServerConfigurationManager serverConfigurationManager, - ILibraryManager libraryManager, - ISubtitleManager subtitleManager, - ISubtitleEncoder subtitleEncoder, - IMediaSourceManager mediaSourceManager, - IProviderManager providerManager, - IFileSystem fileSystem, - ILogger logger) + /// + /// Deletes an external subtitle file. + /// + /// The item id. + /// The index of the subtitle file. + /// Subtitle deleted. + /// Item not found. + /// A . + [HttpDelete("Videos/{itemId}/Subtitles/{index}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult DeleteSubtitle( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] int index) + { + var item = _libraryManager.GetItemById(itemId); + + if (item is null) { - _serverConfigurationManager = serverConfigurationManager; - _libraryManager = libraryManager; - _subtitleManager = subtitleManager; - _subtitleEncoder = subtitleEncoder; - _mediaSourceManager = mediaSourceManager; - _providerManager = providerManager; - _fileSystem = fileSystem; - _logger = logger; + return NotFound(); } - /// - /// Deletes an external subtitle file. - /// - /// The item id. - /// The index of the subtitle file. - /// Subtitle deleted. - /// Item not found. - /// A . - [HttpDelete("Videos/{itemId}/Subtitles/{index}")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteSubtitle( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] int index) + _subtitleManager.DeleteSubtitles(item, index); + return NoContent(); + } + + /// + /// Search remote subtitles. + /// + /// The item id. + /// The language of the subtitles. + /// Optional. Only show subtitles which are a perfect match. + /// Subtitles retrieved. + /// An array of . + [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> SearchRemoteSubtitles( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] string language, + [FromQuery] bool? isPerfectMatch) + { + var video = (Video)_libraryManager.GetItemById(itemId); + + return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Downloads a remote subtitle. + /// + /// The item id. + /// The subtitle id. + /// Subtitle downloaded. + /// A . + [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task DownloadRemoteSubtitles( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] string subtitleId) + { + var video = (Video)_libraryManager.GetItemById(itemId); + + try { - var item = _libraryManager.GetItemById(itemId); + await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None) + .ConfigureAwait(false); - if (item is null) - { - return NotFound(); - } - - _subtitleManager.DeleteSubtitles(item, index); - return NoContent(); + _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error downloading subtitles"); } - /// - /// Search remote subtitles. - /// - /// The item id. - /// The language of the subtitles. - /// Optional. Only show subtitles which are a perfect match. - /// Subtitles retrieved. - /// An array of . - [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> SearchRemoteSubtitles( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] string language, - [FromQuery] bool? isPerfectMatch) - { - var video = (Video)_libraryManager.GetItemById(itemId); + return NoContent(); + } - return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false); + /// + /// Gets the remote subtitles. + /// + /// The item id. + /// File returned. + /// A with the subtitle file. + [HttpGet("Providers/Subtitles/Subtitles/{id}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [Produces(MediaTypeNames.Application.Octet)] + [ProducesFile("text/*")] + public async Task GetRemoteSubtitles([FromRoute, Required] string id) + { + var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false); + + return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format)); + } + + /// + /// Gets subtitles in a specified format. + /// + /// The (route) item id. + /// The (route) media source id. + /// The (route) subtitle stream index. + /// The (route) format of the returned subtitle. + /// The item id. + /// The media source id. + /// The subtitle stream index. + /// The format of the returned subtitle. + /// Optional. The end position of the subtitle in ticks. + /// Optional. Whether to copy the timestamps. + /// Optional. Whether to add a VTT time map. + /// The start position of the subtitle in ticks. + /// File returned. + /// A with the subtitle file. + [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile("text/*")] + public async Task GetSubtitle( + [FromRoute, Required] Guid routeItemId, + [FromRoute, Required] string routeMediaSourceId, + [FromRoute, Required] int routeIndex, + [FromRoute, Required] string routeFormat, + [FromQuery, ParameterObsolete] Guid? itemId, + [FromQuery, ParameterObsolete] string? mediaSourceId, + [FromQuery, ParameterObsolete] int? index, + [FromQuery, ParameterObsolete] string? format, + [FromQuery] long? endPositionTicks, + [FromQuery] bool copyTimestamps = false, + [FromQuery] bool addVttTimeMap = false, + [FromQuery] long startPositionTicks = 0) + { + // Set parameters to route value if not provided via query. + itemId ??= routeItemId; + mediaSourceId ??= routeMediaSourceId; + index ??= routeIndex; + format ??= routeFormat; + + if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase)) + { + format = "json"; } - /// - /// Downloads a remote subtitle. - /// - /// The item id. - /// The subtitle id. - /// Subtitle downloaded. - /// A . - [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task DownloadRemoteSubtitles( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] string subtitleId) + if (string.IsNullOrEmpty(format)) { - var video = (Video)_libraryManager.GetItemById(itemId); + var item = (Video)_libraryManager.GetItemById(itemId.Value); - try - { - await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None) - .ConfigureAwait(false); + var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture); + var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false) + .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal)); - _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error downloading subtitles"); - } + var subtitleStream = mediaSource.MediaStreams + .First(i => i.Type == MediaStreamType.Subtitle && i.Index == index); - return NoContent(); + return PhysicalFile(subtitleStream.Path, MimeTypes.GetMimeType(subtitleStream.Path)); } - /// - /// Gets the remote subtitles. - /// - /// The item id. - /// File returned. - /// A with the subtitle file. - [HttpGet("Providers/Subtitles/Subtitles/{id}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [Produces(MediaTypeNames.Application.Octet)] - [ProducesFile("text/*")] - public async Task GetRemoteSubtitles([FromRoute, Required] string id) + if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap) { - var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false); + Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + using var reader = new StreamReader(stream); - return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format)); + var text = await reader.ReadToEndAsync().ConfigureAwait(false); + + text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal); + + return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format)); + } } - /// - /// Gets subtitles in a specified format. - /// - /// The (route) item id. - /// The (route) media source id. - /// The (route) subtitle stream index. - /// The (route) format of the returned subtitle. - /// The item id. - /// The media source id. - /// The subtitle stream index. - /// The format of the returned subtitle. - /// Optional. The end position of the subtitle in ticks. - /// Optional. Whether to copy the timestamps. - /// Optional. Whether to add a VTT time map. - /// The start position of the subtitle in ticks. - /// File returned. - /// A with the subtitle file. - [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesFile("text/*")] - public async Task GetSubtitle( - [FromRoute, Required] Guid routeItemId, - [FromRoute, Required] string routeMediaSourceId, - [FromRoute, Required] int routeIndex, - [FromRoute, Required] string routeFormat, - [FromQuery, ParameterObsolete] Guid? itemId, - [FromQuery, ParameterObsolete] string? mediaSourceId, - [FromQuery, ParameterObsolete] int? index, - [FromQuery, ParameterObsolete] string? format, - [FromQuery] long? endPositionTicks, - [FromQuery] bool copyTimestamps = false, - [FromQuery] bool addVttTimeMap = false, - [FromQuery] long startPositionTicks = 0) - { - // Set parameters to route value if not provided via query. - itemId ??= routeItemId; - mediaSourceId ??= routeMediaSourceId; - index ??= routeIndex; - format ??= routeFormat; - - if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase)) - { - format = "json"; - } - - if (string.IsNullOrEmpty(format)) - { - var item = (Video)_libraryManager.GetItemById(itemId.Value); - - var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture); - var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false) - .First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal)); - - var subtitleStream = mediaSource.MediaStreams - .First(i => i.Type == MediaStreamType.Subtitle && i.Index == index); - - return PhysicalFile(subtitleStream.Path, MimeTypes.GetMimeType(subtitleStream.Path)); - } - - if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap) - { - Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) - { - using var reader = new StreamReader(stream); - - var text = await reader.ReadToEndAsync().ConfigureAwait(false); - - text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal); - - return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format)); - } - } - - return File( - await EncodeSubtitles( - itemId.Value, - mediaSourceId, - index.Value, - format, - startPositionTicks, - endPositionTicks, - copyTimestamps).ConfigureAwait(false), - MimeTypes.GetMimeType("file." + format)); - } - - /// - /// Gets subtitles in a specified format. - /// - /// The (route) item id. - /// The (route) media source id. - /// The (route) subtitle stream index. - /// The (route) start position of the subtitle in ticks. - /// The (route) format of the returned subtitle. - /// The item id. - /// The media source id. - /// The subtitle stream index. - /// The start position of the subtitle in ticks. - /// The format of the returned subtitle. - /// Optional. The end position of the subtitle in ticks. - /// Optional. Whether to copy the timestamps. - /// Optional. Whether to add a VTT time map. - /// File returned. - /// A with the subtitle file. - [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesFile("text/*")] - public Task GetSubtitleWithTicks( - [FromRoute, Required] Guid routeItemId, - [FromRoute, Required] string routeMediaSourceId, - [FromRoute, Required] int routeIndex, - [FromRoute, Required] long routeStartPositionTicks, - [FromRoute, Required] string routeFormat, - [FromQuery, ParameterObsolete] Guid? itemId, - [FromQuery, ParameterObsolete] string? mediaSourceId, - [FromQuery, ParameterObsolete] int? index, - [FromQuery, ParameterObsolete] long? startPositionTicks, - [FromQuery, ParameterObsolete] string? format, - [FromQuery] long? endPositionTicks, - [FromQuery] bool copyTimestamps = false, - [FromQuery] bool addVttTimeMap = false) - { - return GetSubtitle( - routeItemId, - routeMediaSourceId, - routeIndex, - routeFormat, - itemId, + return File( + await EncodeSubtitles( + itemId.Value, mediaSourceId, - index, - format, - endPositionTicks, - copyTimestamps, - addVttTimeMap, - startPositionTicks ?? routeStartPositionTicks); - } - - /// - /// Gets an HLS subtitle playlist. - /// - /// The item id. - /// The subtitle stream index. - /// The media source id. - /// The subtitle segment length. - /// Subtitle playlist retrieved. - /// A with the HLS subtitle playlist. - [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesPlaylistFile] - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] - public async Task GetSubtitlePlaylist( - [FromRoute, Required] Guid itemId, - [FromRoute, Required] int index, - [FromRoute, Required] string mediaSourceId, - [FromQuery, Required] int segmentLength) - { - var item = (Video)_libraryManager.GetItemById(itemId); - - var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false); - - var runtime = mediaSource.RunTimeTicks ?? -1; - - if (runtime <= 0) - { - throw new ArgumentException("HLS Subtitles are not supported for this media."); - } - - var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks; - if (segmentLengthTicks <= 0) - { - throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)"); - } - - var builder = new StringBuilder(); - builder.AppendLine("#EXTM3U") - .Append("#EXT-X-TARGETDURATION:") - .Append(segmentLength) - .AppendLine() - .AppendLine("#EXT-X-VERSION:3") - .AppendLine("#EXT-X-MEDIA-SEQUENCE:0") - .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); - - long positionTicks = 0; - - var accessToken = User.GetToken(); - - while (positionTicks < runtime) - { - var remaining = runtime - positionTicks; - var lengthTicks = Math.Min(remaining, segmentLengthTicks); - - builder.Append("#EXTINF:") - .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds) - .Append(',') - .AppendLine(); - - var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks); - - var url = string.Format( - CultureInfo.InvariantCulture, - "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}", - positionTicks.ToString(CultureInfo.InvariantCulture), - endPositionTicks.ToString(CultureInfo.InvariantCulture), - accessToken); - - builder.AppendLine(url); - - positionTicks += segmentLengthTicks; - } - - builder.AppendLine("#EXT-X-ENDLIST"); - return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); - } - - /// - /// Upload an external subtitle file. - /// - /// The item the subtitle belongs to. - /// The request body. - /// Subtitle uploaded. - /// A . - [HttpPost("Videos/{itemId}/Subtitles")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task UploadSubtitle( - [FromRoute, Required] Guid itemId, - [FromBody, Required] UploadSubtitleDto body) - { - var video = (Video)_libraryManager.GetItemById(itemId); - var data = Convert.FromBase64String(body.Data); - var memoryStream = new MemoryStream(data, 0, data.Length, false, true); - await using (memoryStream.ConfigureAwait(false)) - { - await _subtitleManager.UploadSubtitle( - video, - new SubtitleResponse - { - Format = body.Format, - Language = body.Language, - IsForced = body.IsForced, - Stream = memoryStream - }).ConfigureAwait(false); - _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); - - return NoContent(); - } - } - - /// - /// Encodes a subtitle in the specified format. - /// - /// The media id. - /// The source media id. - /// The subtitle index. - /// The format to convert to. - /// The start position in ticks. - /// The end position in ticks. - /// Whether to copy the timestamps. - /// A with the new subtitle file. - private Task EncodeSubtitles( - Guid id, - string? mediaSourceId, - int index, - string format, - long startPositionTicks, - long? endPositionTicks, - bool copyTimestamps) - { - var item = _libraryManager.GetItemById(id); - - return _subtitleEncoder.GetSubtitles( - item, - mediaSourceId, - index, + index.Value, format, startPositionTicks, - endPositionTicks ?? 0, - copyTimestamps, - CancellationToken.None); + endPositionTicks, + copyTimestamps).ConfigureAwait(false), + MimeTypes.GetMimeType("file." + format)); + } + + /// + /// Gets subtitles in a specified format. + /// + /// The (route) item id. + /// The (route) media source id. + /// The (route) subtitle stream index. + /// The (route) start position of the subtitle in ticks. + /// The (route) format of the returned subtitle. + /// The item id. + /// The media source id. + /// The subtitle stream index. + /// The start position of the subtitle in ticks. + /// The format of the returned subtitle. + /// Optional. The end position of the subtitle in ticks. + /// Optional. Whether to copy the timestamps. + /// Optional. Whether to add a VTT time map. + /// File returned. + /// A with the subtitle file. + [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile("text/*")] + public Task GetSubtitleWithTicks( + [FromRoute, Required] Guid routeItemId, + [FromRoute, Required] string routeMediaSourceId, + [FromRoute, Required] int routeIndex, + [FromRoute, Required] long routeStartPositionTicks, + [FromRoute, Required] string routeFormat, + [FromQuery, ParameterObsolete] Guid? itemId, + [FromQuery, ParameterObsolete] string? mediaSourceId, + [FromQuery, ParameterObsolete] int? index, + [FromQuery, ParameterObsolete] long? startPositionTicks, + [FromQuery, ParameterObsolete] string? format, + [FromQuery] long? endPositionTicks, + [FromQuery] bool copyTimestamps = false, + [FromQuery] bool addVttTimeMap = false) + { + return GetSubtitle( + routeItemId, + routeMediaSourceId, + routeIndex, + routeFormat, + itemId, + mediaSourceId, + index, + format, + endPositionTicks, + copyTimestamps, + addVttTimeMap, + startPositionTicks ?? routeStartPositionTicks); + } + + /// + /// Gets an HLS subtitle playlist. + /// + /// The item id. + /// The subtitle stream index. + /// The media source id. + /// The subtitle segment length. + /// Subtitle playlist retrieved. + /// A with the HLS subtitle playlist. + [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + public async Task GetSubtitlePlaylist( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] int index, + [FromRoute, Required] string mediaSourceId, + [FromQuery, Required] int segmentLength) + { + var item = (Video)_libraryManager.GetItemById(itemId); + + var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false); + + var runtime = mediaSource.RunTimeTicks ?? -1; + + if (runtime <= 0) + { + throw new ArgumentException("HLS Subtitles are not supported for this media."); } - /// - /// Gets a list of available fallback font files. - /// - /// Information retrieved. - /// An array of with the available font files. - [HttpGet("FallbackFont/Fonts")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public IEnumerable GetFallbackFontList() + var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks; + if (segmentLengthTicks <= 0) { - var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - var fallbackFontPath = encodingOptions.FallbackFontPath; - - if (!string.IsNullOrEmpty(fallbackFontPath)) - { - var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false); - var fontFiles = files - .Select(i => new FontFile - { - Name = i.Name, - Size = i.Length, - DateCreated = _fileSystem.GetCreationTimeUtc(i), - DateModified = _fileSystem.GetLastWriteTimeUtc(i) - }) - .OrderBy(i => i.Size) - .ThenBy(i => i.Name) - .ThenByDescending(i => i.DateModified) - .ThenByDescending(i => i.DateCreated); - // max total size 20M - const int MaxSize = 20971520; - var sizeCounter = 0L; - foreach (var fontFile in fontFiles) - { - sizeCounter += fontFile.Size; - if (sizeCounter >= MaxSize) - { - _logger.LogWarning("Some fonts will not be sent due to size limitations"); - yield break; - } - - yield return fontFile; - } - } - else - { - _logger.LogWarning("The path of fallback font folder has not been set"); - encodingOptions.EnableFallbackFont = false; - } + throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)"); } - /// - /// Gets a fallback font file. - /// - /// The name of the fallback font file to get. - /// Fallback font file retrieved. - /// The fallback font file. - [HttpGet("FallbackFont/Fonts/{name}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesFile("font/*")] - public ActionResult GetFallbackFont([FromRoute, Required] string name) + var builder = new StringBuilder(); + builder.AppendLine("#EXTM3U") + .Append("#EXT-X-TARGETDURATION:") + .Append(segmentLength) + .AppendLine() + .AppendLine("#EXT-X-VERSION:3") + .AppendLine("#EXT-X-MEDIA-SEQUENCE:0") + .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); + + long positionTicks = 0; + + var accessToken = User.GetToken(); + + while (positionTicks < runtime) { - var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - var fallbackFontPath = encodingOptions.FallbackFontPath; + var remaining = runtime - positionTicks; + var lengthTicks = Math.Min(remaining, segmentLengthTicks); - if (!string.IsNullOrEmpty(fallbackFontPath)) - { - var fontFile = _fileSystem.GetFiles(fallbackFontPath) - .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); - var fileSize = fontFile?.Length; + builder.Append("#EXTINF:") + .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds) + .Append(',') + .AppendLine(); - if (fontFile is not null && fileSize is not null && fileSize > 0) + var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks); + + var url = string.Format( + CultureInfo.InvariantCulture, + "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}", + positionTicks.ToString(CultureInfo.InvariantCulture), + endPositionTicks.ToString(CultureInfo.InvariantCulture), + accessToken); + + builder.AppendLine(url); + + positionTicks += segmentLengthTicks; + } + + builder.AppendLine("#EXT-X-ENDLIST"); + return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); + } + + /// + /// Upload an external subtitle file. + /// + /// The item the subtitle belongs to. + /// The request body. + /// Subtitle uploaded. + /// A . + [HttpPost("Videos/{itemId}/Subtitles")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task UploadSubtitle( + [FromRoute, Required] Guid itemId, + [FromBody, Required] UploadSubtitleDto body) + { + var video = (Video)_libraryManager.GetItemById(itemId); + var data = Convert.FromBase64String(body.Data); + var memoryStream = new MemoryStream(data, 0, data.Length, false, true); + await using (memoryStream.ConfigureAwait(false)) + { + await _subtitleManager.UploadSubtitle( + video, + new SubtitleResponse { - _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize); - return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName)); - } - else - { - _logger.LogWarning("The selected font is null or empty"); - } - } - else - { - _logger.LogWarning("The path of fallback font folder has not been set"); - encodingOptions.EnableFallbackFont = false; - } + Format = body.Format, + Language = body.Language, + IsForced = body.IsForced, + Stream = memoryStream + }).ConfigureAwait(false); + _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); - // returning HTTP 204 will break the SubtitlesOctopus - return Ok(); + return NoContent(); } } + + /// + /// Encodes a subtitle in the specified format. + /// + /// The media id. + /// The source media id. + /// The subtitle index. + /// The format to convert to. + /// The start position in ticks. + /// The end position in ticks. + /// Whether to copy the timestamps. + /// A with the new subtitle file. + private Task EncodeSubtitles( + Guid id, + string? mediaSourceId, + int index, + string format, + long startPositionTicks, + long? endPositionTicks, + bool copyTimestamps) + { + var item = _libraryManager.GetItemById(id); + + return _subtitleEncoder.GetSubtitles( + item, + mediaSourceId, + index, + format, + startPositionTicks, + endPositionTicks ?? 0, + copyTimestamps, + CancellationToken.None); + } + + /// + /// Gets a list of available fallback font files. + /// + /// Information retrieved. + /// An array of with the available font files. + [HttpGet("FallbackFont/Fonts")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public IEnumerable GetFallbackFontList() + { + var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); + var fallbackFontPath = encodingOptions.FallbackFontPath; + + if (!string.IsNullOrEmpty(fallbackFontPath)) + { + var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false); + var fontFiles = files + .Select(i => new FontFile + { + Name = i.Name, + Size = i.Length, + DateCreated = _fileSystem.GetCreationTimeUtc(i), + DateModified = _fileSystem.GetLastWriteTimeUtc(i) + }) + .OrderBy(i => i.Size) + .ThenBy(i => i.Name) + .ThenByDescending(i => i.DateModified) + .ThenByDescending(i => i.DateCreated); + // max total size 20M + const int MaxSize = 20971520; + var sizeCounter = 0L; + foreach (var fontFile in fontFiles) + { + sizeCounter += fontFile.Size; + if (sizeCounter >= MaxSize) + { + _logger.LogWarning("Some fonts will not be sent due to size limitations"); + yield break; + } + + yield return fontFile; + } + } + else + { + _logger.LogWarning("The path of fallback font folder has not been set"); + encodingOptions.EnableFallbackFont = false; + } + } + + /// + /// Gets a fallback font file. + /// + /// The name of the fallback font file to get. + /// Fallback font file retrieved. + /// The fallback font file. + [HttpGet("FallbackFont/Fonts/{name}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile("font/*")] + public ActionResult GetFallbackFont([FromRoute, Required] string name) + { + var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); + var fallbackFontPath = encodingOptions.FallbackFontPath; + + if (!string.IsNullOrEmpty(fallbackFontPath)) + { + var fontFile = _fileSystem.GetFiles(fallbackFontPath) + .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); + var fileSize = fontFile?.Length; + + if (fontFile is not null && fileSize is not null && fileSize > 0) + { + _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize); + return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName)); + } + else + { + _logger.LogWarning("The selected font is null or empty"); + } + } + else + { + _logger.LogWarning("The path of fallback font folder has not been set"); + encodingOptions.EnableFallbackFont = false; + } + + // returning HTTP 204 will break the SubtitlesOctopus + return Ok(); + } } diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 1cf528153f..c5c429757a 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -13,80 +13,79 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The suggestions controller. +/// +[Route("")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class SuggestionsController : BaseJellyfinApiController { + private readonly IDtoService _dtoService; + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + /// - /// The suggestions controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class SuggestionsController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public SuggestionsController( + IDtoService dtoService, + IUserManager userManager, + ILibraryManager libraryManager) { - private readonly IDtoService _dtoService; - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; + _dtoService = dtoService; + _userManager = userManager; + _libraryManager = libraryManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public SuggestionsController( - IDtoService dtoService, - IUserManager userManager, - ILibraryManager libraryManager) + /// + /// Gets suggestions. + /// + /// The user id. + /// The media types. + /// The type. + /// Optional. The start index. + /// Optional. The limit. + /// Whether to enable the total record count. + /// Suggestions returned. + /// A with the suggestions. + [HttpGet("Users/{userId}/Suggestions")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetSuggestions( + [FromRoute, Required] Guid userId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] bool enableTotalRecordCount = false) + { + var user = userId.Equals(default) + ? null + : _userManager.GetUserById(userId); + + var dtoOptions = new DtoOptions().AddClientFields(User); + var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { - _dtoService = dtoService; - _userManager = userManager; - _libraryManager = libraryManager; - } + OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, + MediaTypes = mediaType, + IncludeItemTypes = type, + IsVirtualItem = false, + StartIndex = startIndex, + Limit = limit, + DtoOptions = dtoOptions, + EnableTotalRecordCount = enableTotalRecordCount, + Recursive = true + }); - /// - /// Gets suggestions. - /// - /// The user id. - /// The media types. - /// The type. - /// Optional. The start index. - /// Optional. The limit. - /// Whether to enable the total record count. - /// Suggestions returned. - /// A with the suggestions. - [HttpGet("Users/{userId}/Suggestions")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSuggestions( - [FromRoute, Required] Guid userId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] bool enableTotalRecordCount = false) - { - var user = userId.Equals(default) - ? null - : _userManager.GetUserById(userId); + var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); - var dtoOptions = new DtoOptions().AddClientFields(User); - var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) - { - OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, - MediaTypes = mediaType, - IncludeItemTypes = type, - IsVirtualItem = false, - StartIndex = startIndex, - Limit = limit, - DtoOptions = dtoOptions, - EnableTotalRecordCount = enableTotalRecordCount, - Recursive = true - }); - - var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); - - return new QueryResult( - startIndex, - result.TotalRecordCount, - dtoList); - } + return new QueryResult( + startIndex, + result.TotalRecordCount, + dtoList); } } diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 99347246e0..23abba7dc7 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -16,409 +16,408 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The sync play controller. +/// +[Authorize(Policy = Policies.SyncPlayHasAccess)] +public class SyncPlayController : BaseJellyfinApiController { + private readonly ISessionManager _sessionManager; + private readonly ISyncPlayManager _syncPlayManager; + private readonly IUserManager _userManager; + /// - /// The sync play controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.SyncPlayHasAccess)] - public class SyncPlayController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public SyncPlayController( + ISessionManager sessionManager, + ISyncPlayManager syncPlayManager, + IUserManager userManager) { - private readonly ISessionManager _sessionManager; - private readonly ISyncPlayManager _syncPlayManager; - private readonly IUserManager _userManager; + _sessionManager = sessionManager; + _syncPlayManager = syncPlayManager; + _userManager = userManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public SyncPlayController( - ISessionManager sessionManager, - ISyncPlayManager syncPlayManager, - IUserManager userManager) - { - _sessionManager = sessionManager; - _syncPlayManager = syncPlayManager; - _userManager = userManager; - } + /// + /// Create a new SyncPlay group. + /// + /// The settings of the new group. + /// New group created. + /// A indicating success. + [HttpPost("New")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayCreateGroup)] + public async Task SyncPlayCreateGroup( + [FromBody, Required] NewGroupRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new NewGroupRequest(requestData.GroupName); + _syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Create a new SyncPlay group. - /// - /// The settings of the new group. - /// New group created. - /// A indicating success. - [HttpPost("New")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayCreateGroup)] - public async Task SyncPlayCreateGroup( - [FromBody, Required] NewGroupRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new NewGroupRequest(requestData.GroupName); - _syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Join an existing SyncPlay group. + /// + /// The group to join. + /// Group join successful. + /// A indicating success. + [HttpPost("Join")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayJoinGroup)] + public async Task SyncPlayJoinGroup( + [FromBody, Required] JoinGroupRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new JoinGroupRequest(requestData.GroupId); + _syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Join an existing SyncPlay group. - /// - /// The group to join. - /// Group join successful. - /// A indicating success. - [HttpPost("Join")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayJoinGroup)] - public async Task SyncPlayJoinGroup( - [FromBody, Required] JoinGroupRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new JoinGroupRequest(requestData.GroupId); - _syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Leave the joined SyncPlay group. + /// + /// Group leave successful. + /// A indicating success. + [HttpPost("Leave")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlayLeaveGroup() + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new LeaveGroupRequest(); + _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Leave the joined SyncPlay group. - /// - /// Group leave successful. - /// A indicating success. - [HttpPost("Leave")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlayLeaveGroup() - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new LeaveGroupRequest(); - _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Gets all SyncPlay groups. + /// + /// Groups returned. + /// An containing the available SyncPlay groups. + [HttpGet("List")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.SyncPlayJoinGroup)] + public async Task>> SyncPlayGetGroups() + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new ListGroupsRequest(); + return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable()); + } - /// - /// Gets all SyncPlay groups. - /// - /// Groups returned. - /// An containing the available SyncPlay groups. - [HttpGet("List")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize(Policy = Policies.SyncPlayJoinGroup)] - public async Task>> SyncPlayGetGroups() - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new ListGroupsRequest(); - return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable()); - } + /// + /// Request to set new playlist in SyncPlay group. + /// + /// The new playlist to play in the group. + /// Queue update sent to all group members. + /// A indicating success. + [HttpPost("SetNewQueue")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlaySetNewQueue( + [FromBody, Required] PlayRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new PlayGroupRequest( + requestData.PlayingQueue, + requestData.PlayingItemPosition, + requestData.StartPositionTicks); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request to set new playlist in SyncPlay group. - /// - /// The new playlist to play in the group. - /// Queue update sent to all group members. - /// A indicating success. - [HttpPost("SetNewQueue")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlaySetNewQueue( - [FromBody, Required] PlayRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new PlayGroupRequest( - requestData.PlayingQueue, - requestData.PlayingItemPosition, - requestData.StartPositionTicks); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request to change playlist item in SyncPlay group. + /// + /// The new item to play. + /// Queue update sent to all group members. + /// A indicating success. + [HttpPost("SetPlaylistItem")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlaySetPlaylistItem( + [FromBody, Required] SetPlaylistItemRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request to change playlist item in SyncPlay group. - /// - /// The new item to play. - /// Queue update sent to all group members. - /// A indicating success. - [HttpPost("SetPlaylistItem")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlaySetPlaylistItem( - [FromBody, Required] SetPlaylistItemRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request to remove items from the playlist in SyncPlay group. + /// + /// The items to remove. + /// Queue update sent to all group members. + /// A indicating success. + [HttpPost("RemoveFromPlaylist")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlayRemoveFromPlaylist( + [FromBody, Required] RemoveFromPlaylistRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request to remove items from the playlist in SyncPlay group. - /// - /// The items to remove. - /// Queue update sent to all group members. - /// A indicating success. - [HttpPost("RemoveFromPlaylist")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlayRemoveFromPlaylist( - [FromBody, Required] RemoveFromPlaylistRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request to move an item in the playlist in SyncPlay group. + /// + /// The new position for the item. + /// Queue update sent to all group members. + /// A indicating success. + [HttpPost("MovePlaylistItem")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlayMovePlaylistItem( + [FromBody, Required] MovePlaylistItemRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request to move an item in the playlist in SyncPlay group. - /// - /// The new position for the item. - /// Queue update sent to all group members. - /// A indicating success. - [HttpPost("MovePlaylistItem")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlayMovePlaylistItem( - [FromBody, Required] MovePlaylistItemRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request to queue items to the playlist of a SyncPlay group. + /// + /// The items to add. + /// Queue update sent to all group members. + /// A indicating success. + [HttpPost("Queue")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlayQueue( + [FromBody, Required] QueueRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request to queue items to the playlist of a SyncPlay group. - /// - /// The items to add. - /// Queue update sent to all group members. - /// A indicating success. - [HttpPost("Queue")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlayQueue( - [FromBody, Required] QueueRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request unpause in SyncPlay group. + /// + /// Unpause update sent to all group members. + /// A indicating success. + [HttpPost("Unpause")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlayUnpause() + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new UnpauseGroupRequest(); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request unpause in SyncPlay group. - /// - /// Unpause update sent to all group members. - /// A indicating success. - [HttpPost("Unpause")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlayUnpause() - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new UnpauseGroupRequest(); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request pause in SyncPlay group. + /// + /// Pause update sent to all group members. + /// A indicating success. + [HttpPost("Pause")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlayPause() + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new PauseGroupRequest(); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request pause in SyncPlay group. - /// - /// Pause update sent to all group members. - /// A indicating success. - [HttpPost("Pause")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlayPause() - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new PauseGroupRequest(); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request stop in SyncPlay group. + /// + /// Stop update sent to all group members. + /// A indicating success. + [HttpPost("Stop")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlayStop() + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new StopGroupRequest(); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request stop in SyncPlay group. - /// - /// Stop update sent to all group members. - /// A indicating success. - [HttpPost("Stop")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlayStop() - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new StopGroupRequest(); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request seek in SyncPlay group. + /// + /// The new playback position. + /// Seek update sent to all group members. + /// A indicating success. + [HttpPost("Seek")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlaySeek( + [FromBody, Required] SeekRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request seek in SyncPlay group. - /// - /// The new playback position. - /// Seek update sent to all group members. - /// A indicating success. - [HttpPost("Seek")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlaySeek( - [FromBody, Required] SeekRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Notify SyncPlay group that member is buffering. + /// + /// The player status. + /// Group state update sent to all group members. + /// A indicating success. + [HttpPost("Buffering")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlayBuffering( + [FromBody, Required] BufferRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new BufferGroupRequest( + requestData.When, + requestData.PositionTicks, + requestData.IsPlaying, + requestData.PlaylistItemId); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Notify SyncPlay group that member is buffering. - /// - /// The player status. - /// Group state update sent to all group members. - /// A indicating success. - [HttpPost("Buffering")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlayBuffering( - [FromBody, Required] BufferRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new BufferGroupRequest( - requestData.When, - requestData.PositionTicks, - requestData.IsPlaying, - requestData.PlaylistItemId); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Notify SyncPlay group that member is ready for playback. + /// + /// The player status. + /// Group state update sent to all group members. + /// A indicating success. + [HttpPost("Ready")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlayReady( + [FromBody, Required] ReadyRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new ReadyGroupRequest( + requestData.When, + requestData.PositionTicks, + requestData.IsPlaying, + requestData.PlaylistItemId); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Notify SyncPlay group that member is ready for playback. - /// - /// The player status. - /// Group state update sent to all group members. - /// A indicating success. - [HttpPost("Ready")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlayReady( - [FromBody, Required] ReadyRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new ReadyGroupRequest( - requestData.When, - requestData.PositionTicks, - requestData.IsPlaying, - requestData.PlaylistItemId); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request SyncPlay group to ignore member during group-wait. + /// + /// The settings to set. + /// Member state updated. + /// A indicating success. + [HttpPost("SetIgnoreWait")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlaySetIgnoreWait( + [FromBody, Required] IgnoreWaitRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request SyncPlay group to ignore member during group-wait. - /// - /// The settings to set. - /// Member state updated. - /// A indicating success. - [HttpPost("SetIgnoreWait")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlaySetIgnoreWait( - [FromBody, Required] IgnoreWaitRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request next item in SyncPlay group. + /// + /// The current item information. + /// Next item update sent to all group members. + /// A indicating success. + [HttpPost("NextItem")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlayNextItem( + [FromBody, Required] NextItemRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request next item in SyncPlay group. - /// - /// The current item information. - /// Next item update sent to all group members. - /// A indicating success. - [HttpPost("NextItem")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlayNextItem( - [FromBody, Required] NextItemRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request previous item in SyncPlay group. + /// + /// The current item information. + /// Previous item update sent to all group members. + /// A indicating success. + [HttpPost("PreviousItem")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlayPreviousItem( + [FromBody, Required] PreviousItemRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request previous item in SyncPlay group. - /// - /// The current item information. - /// Previous item update sent to all group members. - /// A indicating success. - [HttpPost("PreviousItem")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlayPreviousItem( - [FromBody, Required] PreviousItemRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request to set repeat mode in SyncPlay group. + /// + /// The new repeat mode. + /// Play queue update sent to all group members. + /// A indicating success. + [HttpPost("SetRepeatMode")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlaySetRepeatMode( + [FromBody, Required] SetRepeatModeRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request to set repeat mode in SyncPlay group. - /// - /// The new repeat mode. - /// Play queue update sent to all group members. - /// A indicating success. - [HttpPost("SetRepeatMode")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlaySetRepeatMode( - [FromBody, Required] SetRepeatModeRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Request to set shuffle mode in SyncPlay group. + /// + /// The new shuffle mode. + /// Play queue update sent to all group members. + /// A indicating success. + [HttpPost("SetShuffleMode")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize(Policy = Policies.SyncPlayIsInGroup)] + public async Task SyncPlaySetShuffleMode( + [FromBody, Required] SetShuffleModeRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } - /// - /// Request to set shuffle mode in SyncPlay group. - /// - /// The new shuffle mode. - /// Play queue update sent to all group members. - /// A indicating success. - [HttpPost("SetShuffleMode")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.SyncPlayIsInGroup)] - public async Task SyncPlaySetShuffleMode( - [FromBody, Required] SetShuffleModeRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } - - /// - /// Update session ping. - /// - /// The new ping. - /// Ping updated. - /// A indicating success. - [HttpPost("Ping")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task SyncPlayPing( - [FromBody, Required] PingRequestDto requestData) - { - var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new PingGroupRequest(requestData.Ping); - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); - } + /// + /// Update session ping. + /// + /// The new ping. + /// Ping updated. + /// A indicating success. + [HttpPost("Ping")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task SyncPlayPing( + [FromBody, Required] PingRequestDto requestData) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var syncPlayRequest = new PingGroupRequest(requestData.Ping); + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 2d594293e0..b0b2e2d6d8 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -20,204 +20,203 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The system controller. +/// +public class SystemController : BaseJellyfinApiController { + private readonly IServerApplicationHost _appHost; + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + private readonly INetworkManager _network; + private readonly ILogger _logger; + /// - /// The system controller. + /// Initializes a new instance of the class. /// - public class SystemController : BaseJellyfinApiController + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public SystemController( + IServerConfigurationManager serverConfigurationManager, + IServerApplicationHost appHost, + IFileSystem fileSystem, + INetworkManager network, + ILogger logger) { - private readonly IServerApplicationHost _appHost; - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - private readonly INetworkManager _network; - private readonly ILogger _logger; + _appPaths = serverConfigurationManager.ApplicationPaths; + _appHost = appHost; + _fileSystem = fileSystem; + _network = network; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - /// Instance of interface. - public SystemController( - IServerConfigurationManager serverConfigurationManager, - IServerApplicationHost appHost, - IFileSystem fileSystem, - INetworkManager network, - ILogger logger) + /// + /// Gets information about the server. + /// + /// Information retrieved. + /// A with info about the system. + [HttpGet("Info")] + [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetSystemInfo() + { + return _appHost.GetSystemInfo(Request); + } + + /// + /// Gets public information about the server. + /// + /// Information retrieved. + /// A with public info about the system. + [HttpGet("Info/Public")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetPublicSystemInfo() + { + return _appHost.GetPublicSystemInfo(Request); + } + + /// + /// Pings the system. + /// + /// Information retrieved. + /// The server name. + [HttpGet("Ping", Name = "GetPingSystem")] + [HttpPost("Ping", Name = "PostPingSystem")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult PingSystem() + { + return _appHost.Name; + } + + /// + /// Restarts the application. + /// + /// Server restarted. + /// No content. Server restarted. + [HttpPost("Restart")] + [Authorize(Policy = Policies.LocalAccessOrRequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RestartApplication() + { + Task.Run(async () => { - _appPaths = serverConfigurationManager.ApplicationPaths; - _appHost = appHost; - _fileSystem = fileSystem; - _network = network; - _logger = logger; + await Task.Delay(100).ConfigureAwait(false); + _appHost.Restart(); + }); + return NoContent(); + } + + /// + /// Shuts down the application. + /// + /// Server shut down. + /// No content. Server shut down. + [HttpPost("Shutdown")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult ShutdownApplication() + { + Task.Run(async () => + { + await Task.Delay(100).ConfigureAwait(false); + await _appHost.Shutdown().ConfigureAwait(false); + }); + return NoContent(); + } + + /// + /// Gets a list of available server log files. + /// + /// Information retrieved. + /// An array of with the available log files. + [HttpGet("Logs")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetServerLogs() + { + IEnumerable files; + + try + { + files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error getting logs"); + files = Enumerable.Empty(); } - /// - /// Gets information about the server. - /// - /// Information retrieved. - /// A with info about the system. - [HttpGet("Info")] - [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetSystemInfo() + var result = files.Select(i => new LogFile { - return _appHost.GetSystemInfo(Request); - } + DateCreated = _fileSystem.GetCreationTimeUtc(i), + DateModified = _fileSystem.GetLastWriteTimeUtc(i), + Name = i.Name, + Size = i.Length + }) + .OrderByDescending(i => i.DateModified) + .ThenByDescending(i => i.DateCreated) + .ThenBy(i => i.Name) + .ToArray(); - /// - /// Gets public information about the server. - /// - /// Information retrieved. - /// A with public info about the system. - [HttpGet("Info/Public")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetPublicSystemInfo() + return result; + } + + /// + /// Gets information about the request endpoint. + /// + /// Information retrieved. + /// with information about the endpoint. + [HttpGet("Endpoint")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetEndpointInfo() + { + return new EndPointInfo { - return _appHost.GetPublicSystemInfo(Request); - } + IsLocal = HttpContext.IsLocal(), + IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()) + }; + } - /// - /// Pings the system. - /// - /// Information retrieved. - /// The server name. - [HttpGet("Ping", Name = "GetPingSystem")] - [HttpPost("Ping", Name = "PostPingSystem")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult PingSystem() - { - return _appHost.Name; - } + /// + /// Gets a log file. + /// + /// The name of the log file to get. + /// Log file retrieved. + /// The log file. + [HttpGet("Logs/Log")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile(MediaTypeNames.Text.Plain)] + public ActionResult GetLogFile([FromQuery, Required] string name) + { + var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) + .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); - /// - /// Restarts the application. - /// - /// Server restarted. - /// No content. Server restarted. - [HttpPost("Restart")] - [Authorize(Policy = Policies.LocalAccessOrRequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RestartApplication() - { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - _appHost.Restart(); - }); - return NoContent(); - } + // 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, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + return File(stream, "text/plain; charset=utf-8"); + } - /// - /// Shuts down the application. - /// - /// Server shut down. - /// No content. Server shut down. - [HttpPost("Shutdown")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult ShutdownApplication() - { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - await _appHost.Shutdown().ConfigureAwait(false); - }); - return NoContent(); - } - - /// - /// Gets a list of available server log files. - /// - /// Information retrieved. - /// An array of with the available log files. - [HttpGet("Logs")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetServerLogs() - { - IEnumerable files; - - try - { - files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error getting logs"); - files = Enumerable.Empty(); - } - - var result = files.Select(i => new LogFile - { - DateCreated = _fileSystem.GetCreationTimeUtc(i), - DateModified = _fileSystem.GetLastWriteTimeUtc(i), - Name = i.Name, - Size = i.Length - }) - .OrderByDescending(i => i.DateModified) - .ThenByDescending(i => i.DateCreated) - .ThenBy(i => i.Name) - .ToArray(); - - return result; - } - - /// - /// Gets information about the request endpoint. - /// - /// Information retrieved. - /// with information about the endpoint. - [HttpGet("Endpoint")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetEndpointInfo() - { - return new EndPointInfo - { - IsLocal = HttpContext.IsLocal(), - IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()) - }; - } - - /// - /// Gets a log file. - /// - /// The name of the log file to get. - /// Log file retrieved. - /// The log file. - [HttpGet("Logs/Log")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesFile(MediaTypeNames.Text.Plain)] - public ActionResult GetLogFile([FromQuery, Required] string name) - { - var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) - .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); - - // 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, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - return File(stream, "text/plain; charset=utf-8"); - } - - /// - /// Gets wake on lan information. - /// - /// Information retrieved. - /// An with the WakeOnLan infos. - [HttpGet("WakeOnLanInfo")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [Obsolete("This endpoint is obsolete.")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetWakeOnLanInfo() - { - var result = _network.GetMacAddresses() - .Select(i => new WakeOnLanInfo(i)); - return Ok(result); - } + /// + /// Gets wake on lan information. + /// + /// Information retrieved. + /// An with the WakeOnLan infos. + [HttpGet("WakeOnLanInfo")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [Obsolete("This endpoint is obsolete.")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetWakeOnLanInfo() + { + var result = _network.GetMacAddresses() + .Select(i => new WakeOnLanInfo(i)); + return Ok(result); } } diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs index e7c5a71257..d7304cf426 100644 --- a/Jellyfin.Api/Controllers/TimeSyncController.cs +++ b/Jellyfin.Api/Controllers/TimeSyncController.cs @@ -3,32 +3,31 @@ using MediaBrowser.Model.SyncPlay; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The time sync controller. +/// +[Route("")] +public class TimeSyncController : BaseJellyfinApiController { /// - /// The time sync controller. + /// Gets the current UTC time. /// - [Route("")] - public class TimeSyncController : BaseJellyfinApiController + /// Time returned. + /// An to sync the client and server time. + [HttpGet("GetUtcTime")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public ActionResult GetUtcTime() { - /// - /// Gets the current UTC time. - /// - /// Time returned. - /// An to sync the client and server time. - [HttpGet("GetUtcTime")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public ActionResult GetUtcTime() - { - // Important to keep the following line at the beginning - var requestReceptionTime = DateTime.UtcNow; + // Important to keep the following line at the beginning + var requestReceptionTime = DateTime.UtcNow; - // Important to keep the following line at the end - var responseTransmissionTime = DateTime.UtcNow; + // Important to keep the following line at the end + 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. - return new UtcTimeResponse(requestReceptionTime, responseTransmissionTime); - } + // Implementing NTP on such a high level results in this useless + // information being sent. On the other hand it enables future additions. + return new UtcTimeResponse(requestReceptionTime, responseTransmissionTime); } } diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 53a839e431..761f6edbcd 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -10,290 +10,289 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The trailers controller. +/// +[Authorize(Policy = Policies.DefaultAuthorization)] +public class TrailersController : BaseJellyfinApiController { + private readonly ItemsController _itemsController; + /// - /// The trailers controller. + /// Initializes a new instance of the class. /// - [Authorize(Policy = Policies.DefaultAuthorization)] - public class TrailersController : BaseJellyfinApiController + /// Instance of . + public TrailersController(ItemsController itemsController) { - private readonly ItemsController _itemsController; + _itemsController = itemsController; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of . - public TrailersController(ItemsController itemsController) - { - _itemsController = itemsController; - } + /// + /// Finds movies and trailers similar to a given trailer. + /// + /// The user id supplied as query parameter; this is required when not using an API key. + /// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc). + /// Optional filter by items with theme songs. + /// Optional filter by items with theme videos. + /// Optional filter by items with subtitles. + /// Optional filter by items with special features. + /// Optional filter by items with trailers. + /// Optional. Return items that are siblings of a supplied item. + /// Optional filter by parent index number. + /// Optional filter by items that have or do not have a parental rating. + /// Optional filter by items that are HD or not. + /// Optional filter by items that are 4K or not. + /// Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited. + /// Optional filter by items that are missing episodes or not. + /// Optional filter by items that are unaired episodes or not. + /// Optional filter by minimum community rating. + /// Optional filter by minimum critic rating. + /// Optional. The minimum premiere date. Format = ISO. + /// Optional. The minimum last saved date. Format = ISO. + /// Optional. The minimum last saved date for the current user. Format = ISO. + /// Optional. The maximum premiere date. Format = ISO. + /// Optional filter by items that have an overview or not. + /// Optional filter by items that have an IMDb id or not. + /// Optional filter by items that have a TMDb id or not. + /// Optional filter by items that have a TVDb id or not. + /// Optional filter for live tv movies. + /// Optional filter for live tv series. + /// Optional filter for live tv news. + /// Optional filter for live tv kids. + /// Optional filter for live tv sports. + /// Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// When searching within folders, this determines whether or not the search will be recursive. true/false. + /// Optional. Filter based on a search term. + /// Sort Order - Ascending, Descending. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional filter by items that are marked as favorite, or not. + /// Optional filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited. + /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. + /// Optional filter by items that are played, or not. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person id. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered to include only those containing the specified artist id. + /// Optional. If specified, results will be filtered to include only those containing the specified album artist id. + /// Optional. If specified, results will be filtered to include only those containing the specified contributing artist id. + /// Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited. + /// Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited. + /// Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited. + /// Optional filter by minimum official rating (PG, PG-13, TV-MA, etc). + /// Optional filter by items that are locked. + /// Optional filter by items that are placeholders. + /// Optional filter by items that have official ratings. + /// Whether or not to hide items behind their boxsets. + /// Optional. Filter by the minimum width of the item. + /// Optional. Filter by the minimum height of the item. + /// Optional. Filter by the maximum width of the item. + /// Optional. Filter by the maximum height of the item. + /// Optional filter by items that are 3D, or not. + /// Optional filter by Series Status. Allows multiple, comma delimited. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. + /// Optional. Enable the total record count. + /// Optional, include image information in output. + /// A with the trailers. + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetTrailers( + [FromQuery] Guid? userId, + [FromQuery] string? maxOfficialRating, + [FromQuery] bool? hasThemeSong, + [FromQuery] bool? hasThemeVideo, + [FromQuery] bool? hasSubtitles, + [FromQuery] bool? hasSpecialFeature, + [FromQuery] bool? hasTrailer, + [FromQuery] Guid? adjacentTo, + [FromQuery] int? parentIndexNumber, + [FromQuery] bool? hasParentalRating, + [FromQuery] bool? isHd, + [FromQuery] bool? is4K, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, + [FromQuery] bool? isMissing, + [FromQuery] bool? isUnaired, + [FromQuery] double? minCommunityRating, + [FromQuery] double? minCriticRating, + [FromQuery] DateTime? minPremiereDate, + [FromQuery] DateTime? minDateLastSaved, + [FromQuery] DateTime? minDateLastSavedForUser, + [FromQuery] DateTime? maxPremiereDate, + [FromQuery] bool? hasOverview, + [FromQuery] bool? hasImdbId, + [FromQuery] bool? hasTmdbId, + [FromQuery] bool? hasTvdbId, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] bool? recursive, + [FromQuery] string? searchTerm, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery] bool? isFavorite, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, + [FromQuery] bool? isPlayed, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] string? person, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, + [FromQuery] string? minOfficialRating, + [FromQuery] bool? isLocked, + [FromQuery] bool? isPlaceHolder, + [FromQuery] bool? hasOfficialRating, + [FromQuery] bool? collapseBoxSetItems, + [FromQuery] int? minWidth, + [FromQuery] int? minHeight, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] bool? is3D, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus, + [FromQuery] string? nameStartsWithOrGreater, + [FromQuery] string? nameStartsWith, + [FromQuery] string? nameLessThan, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery] bool enableTotalRecordCount = true, + [FromQuery] bool? enableImages = true) + { + var includeItemTypes = new[] { BaseItemKind.Trailer }; - /// - /// Finds movies and trailers similar to a given trailer. - /// - /// The user id supplied as query parameter; this is required when not using an API key. - /// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc). - /// Optional filter by items with theme songs. - /// Optional filter by items with theme videos. - /// Optional filter by items with subtitles. - /// Optional filter by items with special features. - /// Optional filter by items with trailers. - /// Optional. Return items that are siblings of a supplied item. - /// Optional filter by parent index number. - /// Optional filter by items that have or do not have a parental rating. - /// Optional filter by items that are HD or not. - /// Optional filter by items that are 4K or not. - /// Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited. - /// Optional filter by items that are missing episodes or not. - /// Optional filter by items that are unaired episodes or not. - /// Optional filter by minimum community rating. - /// Optional filter by minimum critic rating. - /// Optional. The minimum premiere date. Format = ISO. - /// Optional. The minimum last saved date. Format = ISO. - /// Optional. The minimum last saved date for the current user. Format = ISO. - /// Optional. The maximum premiere date. Format = ISO. - /// Optional filter by items that have an overview or not. - /// Optional filter by items that have an IMDb id or not. - /// Optional filter by items that have a TMDb id or not. - /// Optional filter by items that have a TVDb id or not. - /// Optional filter for live tv movies. - /// Optional filter for live tv series. - /// Optional filter for live tv news. - /// Optional filter for live tv kids. - /// Optional filter for live tv sports. - /// Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// When searching within folders, this determines whether or not the search will be recursive. true/false. - /// Optional. Filter based on a search term. - /// Sort Order - Ascending, Descending. - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. - /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. - /// Optional filter by items that are marked as favorite, or not. - /// Optional filter by MediaType. Allows multiple, comma delimited. - /// Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited. - /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. - /// Optional filter by items that are played, or not. - /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited. - /// Optional, include user data. - /// Optional, the max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. If specified, results will be filtered to include only those containing the specified person. - /// Optional. If specified, results will be filtered to include only those containing the specified person id. - /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. - /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered to include only those containing the specified artist id. - /// Optional. If specified, results will be filtered to include only those containing the specified album artist id. - /// Optional. If specified, results will be filtered to include only those containing the specified contributing artist id. - /// Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited. - /// Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited. - /// Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited. - /// Optional filter by minimum official rating (PG, PG-13, TV-MA, etc). - /// Optional filter by items that are locked. - /// Optional filter by items that are placeholders. - /// Optional filter by items that have official ratings. - /// Whether or not to hide items behind their boxsets. - /// Optional. Filter by the minimum width of the item. - /// Optional. Filter by the minimum height of the item. - /// Optional. Filter by the maximum width of the item. - /// Optional. Filter by the maximum height of the item. - /// Optional filter by items that are 3D, or not. - /// Optional filter by Series Status. Allows multiple, comma delimited. - /// Optional filter by items whose name is sorted equally or greater than a given input string. - /// Optional filter by items whose name is sorted equally than a given input string. - /// Optional filter by items whose name is equally or lesser than a given input string. - /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited. - /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited. - /// Optional. Enable the total record count. - /// Optional, include image information in output. - /// A with the trailers. - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetTrailers( - [FromQuery] Guid? userId, - [FromQuery] string? maxOfficialRating, - [FromQuery] bool? hasThemeSong, - [FromQuery] bool? hasThemeVideo, - [FromQuery] bool? hasSubtitles, - [FromQuery] bool? hasSpecialFeature, - [FromQuery] bool? hasTrailer, - [FromQuery] Guid? adjacentTo, - [FromQuery] int? parentIndexNumber, - [FromQuery] bool? hasParentalRating, - [FromQuery] bool? isHd, - [FromQuery] bool? is4K, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, - [FromQuery] bool? isMissing, - [FromQuery] bool? isUnaired, - [FromQuery] double? minCommunityRating, - [FromQuery] double? minCriticRating, - [FromQuery] DateTime? minPremiereDate, - [FromQuery] DateTime? minDateLastSaved, - [FromQuery] DateTime? minDateLastSavedForUser, - [FromQuery] DateTime? maxPremiereDate, - [FromQuery] bool? hasOverview, - [FromQuery] bool? hasImdbId, - [FromQuery] bool? hasTmdbId, - [FromQuery] bool? hasTvdbId, - [FromQuery] bool? isMovie, - [FromQuery] bool? isSeries, - [FromQuery] bool? isNews, - [FromQuery] bool? isKids, - [FromQuery] bool? isSports, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] bool? recursive, - [FromQuery] string? searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery] bool? isFavorite, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, - [FromQuery] bool? isPlayed, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, - [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, - [FromQuery] bool? enableUserData, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? person, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, - [FromQuery] string? minOfficialRating, - [FromQuery] bool? isLocked, - [FromQuery] bool? isPlaceHolder, - [FromQuery] bool? hasOfficialRating, - [FromQuery] bool? collapseBoxSetItems, - [FromQuery] int? minWidth, - [FromQuery] int? minHeight, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] bool? is3D, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus, - [FromQuery] string? nameStartsWithOrGreater, - [FromQuery] string? nameStartsWith, - [FromQuery] string? nameLessThan, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, - [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool? enableImages = true) - { - var includeItemTypes = new[] { BaseItemKind.Trailer }; - - return _itemsController - .GetItems( - userId, - maxOfficialRating, - hasThemeSong, - hasThemeVideo, - hasSubtitles, - hasSpecialFeature, - hasTrailer, - adjacentTo, - parentIndexNumber, - hasParentalRating, - isHd, - is4K, - locationTypes, - excludeLocationTypes, - isMissing, - isUnaired, - minCommunityRating, - minCriticRating, - minPremiereDate, - minDateLastSaved, - minDateLastSavedForUser, - maxPremiereDate, - hasOverview, - hasImdbId, - hasTmdbId, - hasTvdbId, - isMovie, - isSeries, - isNews, - isKids, - isSports, - excludeItemIds, - startIndex, - limit, - recursive, - searchTerm, - sortOrder, - parentId, - fields, - excludeItemTypes, - includeItemTypes, - filters, - isFavorite, - mediaTypes, - imageTypes, - sortBy, - isPlayed, - genres, - officialRatings, - tags, - years, - enableUserData, - imageTypeLimit, - enableImageTypes, - person, - personIds, - personTypes, - studios, - artists, - excludeArtistIds, - artistIds, - albumArtistIds, - contributingArtistIds, - albums, - albumIds, - ids, - videoTypes, - minOfficialRating, - isLocked, - isPlaceHolder, - hasOfficialRating, - collapseBoxSetItems, - minWidth, - minHeight, - maxWidth, - maxHeight, - is3D, - seriesStatus, - nameStartsWithOrGreater, - nameStartsWith, - nameLessThan, - studioIds, - genreIds, - enableTotalRecordCount, - enableImages); - } + return _itemsController + .GetItems( + userId, + maxOfficialRating, + hasThemeSong, + hasThemeVideo, + hasSubtitles, + hasSpecialFeature, + hasTrailer, + adjacentTo, + parentIndexNumber, + hasParentalRating, + isHd, + is4K, + locationTypes, + excludeLocationTypes, + isMissing, + isUnaired, + minCommunityRating, + minCriticRating, + minPremiereDate, + minDateLastSaved, + minDateLastSavedForUser, + maxPremiereDate, + hasOverview, + hasImdbId, + hasTmdbId, + hasTvdbId, + isMovie, + isSeries, + isNews, + isKids, + isSports, + excludeItemIds, + startIndex, + limit, + recursive, + searchTerm, + sortOrder, + parentId, + fields, + excludeItemTypes, + includeItemTypes, + filters, + isFavorite, + mediaTypes, + imageTypes, + sortBy, + isPlayed, + genres, + officialRatings, + tags, + years, + enableUserData, + imageTypeLimit, + enableImageTypes, + person, + personIds, + personTypes, + studios, + artists, + excludeArtistIds, + artistIds, + albumArtistIds, + contributingArtistIds, + albums, + albumIds, + ids, + videoTypes, + minOfficialRating, + isLocked, + isPlaceHolder, + hasOfficialRating, + collapseBoxSetItems, + minWidth, + minHeight, + maxWidth, + maxHeight, + is3D, + seriesStatus, + nameStartsWithOrGreater, + nameStartsWith, + nameLessThan, + studioIds, + genreIds, + enableTotalRecordCount, + enableImages); } } diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 7f4f4d0776..2be32095e3 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -19,366 +19,365 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The tv shows controller. +/// +[Route("Shows")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class TvShowsController : BaseJellyfinApiController { + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly ITVSeriesManager _tvSeriesManager; + /// - /// The tv shows controller. + /// Initializes a new instance of the class. /// - [Route("Shows")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class TvShowsController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public TvShowsController( + IUserManager userManager, + ILibraryManager libraryManager, + IDtoService dtoService, + ITVSeriesManager tvSeriesManager) { - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly IDtoService _dtoService; - private readonly ITVSeriesManager _tvSeriesManager; + _userManager = userManager; + _libraryManager = libraryManager; + _dtoService = dtoService; + _tvSeriesManager = tvSeriesManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public TvShowsController( - IUserManager userManager, - ILibraryManager libraryManager, - IDtoService dtoService, - ITVSeriesManager tvSeriesManager) - { - _userManager = userManager; - _libraryManager = libraryManager; - _dtoService = dtoService; - _tvSeriesManager = tvSeriesManager; - } + /// + /// Gets a list of next up episodes. + /// + /// The user id of the user to get the next up episodes for. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Filter by series id. + /// Optional. Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Include image information in output. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. Include user data. + /// Optional. Starting date of shows to show in Next Up section. + /// Whether to enable the total records count. Defaults to true. + /// Whether to disable sending the first episode in a series as next up. + /// Whether to include watched episode in next up results. + /// A with the next up episodes. + [HttpGet("NextUp")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetNextUp( + [FromQuery] Guid? userId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] Guid? seriesId, + [FromQuery] Guid? parentId, + [FromQuery] bool? enableImages, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] bool? enableUserData, + [FromQuery] DateTime? nextUpDateCutoff, + [FromQuery] bool enableTotalRecordCount = true, + [FromQuery] bool disableFirstEpisode = false, + [FromQuery] bool enableRewatching = false) + { + var options = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - /// - /// Gets a list of next up episodes. - /// - /// The user id of the user to get the next up episodes for. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Filter by series id. - /// Optional. Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Include image information in output. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. Include user data. - /// Optional. Starting date of shows to show in Next Up section. - /// Whether to enable the total records count. Defaults to true. - /// Whether to disable sending the first episode in a series as next up. - /// Whether to include watched episode in next up results. - /// A with the next up episodes. - [HttpGet("NextUp")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetNextUp( - [FromQuery] Guid? userId, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] Guid? seriesId, - [FromQuery] Guid? parentId, - [FromQuery] bool? enableImages, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] bool? enableUserData, - [FromQuery] DateTime? nextUpDateCutoff, - [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool disableFirstEpisode = false, - [FromQuery] bool enableRewatching = false) - { - var options = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - - var result = _tvSeriesManager.GetNextUp( - new NextUpQuery - { - Limit = limit, - ParentId = parentId, - SeriesId = seriesId, - StartIndex = startIndex, - UserId = userId ?? Guid.Empty, - EnableTotalRecordCount = enableTotalRecordCount, - DisableFirstEpisode = disableFirstEpisode, - NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, - EnableRewatching = enableRewatching - }, - options); - - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user); - - return new QueryResult( - startIndex, - result.TotalRecordCount, - returnItems); - } - - /// - /// Gets a list of upcoming episodes. - /// - /// The user id of the user to get the upcoming episodes for. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Include image information in output. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. Include user data. - /// A with the next up episodes. - [HttpGet("Upcoming")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetUpcomingEpisodes( - [FromQuery] Guid? userId, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] Guid? parentId, - [FromQuery] bool? enableImages, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] bool? enableUserData) - { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1); - - var parentIdGuid = parentId ?? Guid.Empty; - - var options = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - - var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) + var result = _tvSeriesManager.GetNextUp( + new NextUpQuery { - IncludeItemTypes = new[] { BaseItemKind.Episode }, - OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, - MinPremiereDate = minPremiereDate, - StartIndex = startIndex, Limit = limit, - ParentId = parentIdGuid, - Recursive = true, - DtoOptions = options - }); + ParentId = parentId, + SeriesId = seriesId, + StartIndex = startIndex, + UserId = userId ?? Guid.Empty, + EnableTotalRecordCount = enableTotalRecordCount, + DisableFirstEpisode = disableFirstEpisode, + NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, + EnableRewatching = enableRewatching + }, + options); - var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user); + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); - return new QueryResult( - startIndex, - itemsResult.Count, - returnItems); - } + var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user); - /// - /// Gets episodes for a tv season. - /// - /// The series id. - /// The user id. - /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. - /// Optional filter by season number. - /// Optional. Filter by season id. - /// Optional. Filter by items that are missing episodes or not. - /// Optional. Return items that are siblings of a supplied item. - /// Optional. Skip through the list until a given item is found. - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional, include image information in output. - /// Optional, the max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. Include user data. - /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. - /// A with the episodes on success or a if the series was not found. - [HttpGet("{seriesId}/Episodes")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult> GetEpisodes( - [FromRoute, Required] Guid seriesId, - [FromQuery] Guid? userId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] int? season, - [FromQuery] Guid? seasonId, - [FromQuery] bool? isMissing, - [FromQuery] Guid? adjacentTo, - [FromQuery] Guid? startItemId, - [FromQuery] int? startIndex, - [FromQuery] int? limit, - [FromQuery] bool? enableImages, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] bool? enableUserData, - [FromQuery] string? sortBy) + return new QueryResult( + startIndex, + result.TotalRecordCount, + returnItems); + } + + /// + /// Gets a list of upcoming episodes. + /// + /// The user id of the user to get the upcoming episodes for. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Include image information in output. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. Include user data. + /// A with the next up episodes. + [HttpGet("Upcoming")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetUpcomingEpisodes( + [FromQuery] Guid? userId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] Guid? parentId, + [FromQuery] bool? enableImages, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] bool? enableUserData) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1); + + var parentIdGuid = parentId ?? Guid.Empty; + + var options = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); + IncludeItemTypes = new[] { BaseItemKind.Episode }, + OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, + MinPremiereDate = minPremiereDate, + StartIndex = startIndex, + Limit = limit, + ParentId = parentIdGuid, + Recursive = true, + DtoOptions = options + }); - List episodes; + var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + return new QueryResult( + startIndex, + itemsResult.Count, + returnItems); + } - if (seasonId.HasValue) // Season id was supplied. Get episodes by season id. - { - var item = _libraryManager.GetItemById(seasonId.Value); - if (item is not Season seasonItem) - { - return NotFound("No season exists with Id " + seasonId); - } + /// + /// Gets episodes for a tv season. + /// + /// The series id. + /// The user id. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. + /// Optional filter by season number. + /// Optional. Filter by season id. + /// Optional. Filter by items that are missing episodes or not. + /// Optional. Return items that are siblings of a supplied item. + /// Optional. Skip through the list until a given item is found. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// Optional, include image information in output. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. Include user data. + /// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. + /// A with the episodes on success or a if the series was not found. + [HttpGet("{seriesId}/Episodes")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetEpisodes( + [FromRoute, Required] Guid seriesId, + [FromQuery] Guid? userId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] int? season, + [FromQuery] Guid? seasonId, + [FromQuery] bool? isMissing, + [FromQuery] Guid? adjacentTo, + [FromQuery] Guid? startItemId, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] bool? enableImages, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] bool? enableUserData, + [FromQuery] string? sortBy) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); - episodes = seasonItem.GetEpisodes(user, dtoOptions); - } - else if (season.HasValue) // Season number was supplied. Get episodes by season number - { - if (_libraryManager.GetItemById(seriesId) is not Series series) - { - return NotFound("Series not found"); - } + List episodes; - var seasonItem = series - .GetSeasons(user, dtoOptions) - .FirstOrDefault(i => i.IndexNumber == season.Value); + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - episodes = seasonItem is null ? - new List() - : ((Season)seasonItem).GetEpisodes(user, dtoOptions); - } - else // No season number or season id was supplied. Returning all episodes. - { - if (_libraryManager.GetItemById(seriesId) is not Series series) - { - return NotFound("Series not found"); - } - - episodes = series.GetEpisodes(user, dtoOptions).ToList(); - } - - // Filter after the fact in case the ui doesn't want them - if (isMissing.HasValue) - { - var val = isMissing.Value; - episodes = episodes - .Where(i => ((Episode)i).IsMissingEpisode == val) - .ToList(); - } - - if (startItemId.HasValue) - { - episodes = episodes - .SkipWhile(i => !startItemId.Value.Equals(i.Id)) - .ToList(); - } - - // This must be the last filter - if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default)) - { - episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList(); - } - - if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) - { - episodes.Shuffle(); - } - - var returnItems = episodes; - - if (startIndex.HasValue || limit.HasValue) - { - returnItems = ApplyPaging(episodes, startIndex, limit).ToList(); - } - - var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user); - - return new QueryResult( - startIndex, - episodes.Count, - dtos); - } - - /// - /// Gets seasons for a tv series. - /// - /// The series id. - /// The user id. - /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. - /// Optional. Filter by special season. - /// Optional. Filter by items that are missing episodes or not. - /// Optional. Return items that are siblings of a supplied item. - /// Optional. Include image information in output. - /// Optional. The max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. Include user data. - /// A on success or a if the series was not found. - [HttpGet("{seriesId}/Seasons")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult> GetSeasons( - [FromRoute, Required] Guid seriesId, - [FromQuery] Guid? userId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] bool? isSpecialSeason, - [FromQuery] bool? isMissing, - [FromQuery] Guid? adjacentTo, - [FromQuery] bool? enableImages, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] bool? enableUserData) + if (seasonId.HasValue) // Season id was supplied. Get episodes by season id. { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); + var item = _libraryManager.GetItemById(seasonId.Value); + if (item is not Season seasonItem) + { + return NotFound("No season exists with Id " + seasonId); + } + episodes = seasonItem.GetEpisodes(user, dtoOptions); + } + else if (season.HasValue) // Season number was supplied. Get episodes by season number + { if (_libraryManager.GetItemById(seriesId) is not Series series) { return NotFound("Series not found"); } - var seasons = series.GetItemList(new InternalItemsQuery(user) - { - IsMissing = isMissing, - IsSpecialSeason = isSpecialSeason, - AdjacentTo = adjacentTo - }); + var seasonItem = series + .GetSeasons(user, dtoOptions) + .FirstOrDefault(i => i.IndexNumber == season.Value); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - - var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user); - - return new QueryResult(returnItems); + episodes = seasonItem is null ? + new List() + : ((Season)seasonItem).GetEpisodes(user, dtoOptions); } - - /// - /// Applies the paging. - /// - /// The items. - /// The start index. - /// The limit. - /// IEnumerable{BaseItem}. - private IEnumerable ApplyPaging(IEnumerable items, int? startIndex, int? limit) + else // No season number or season id was supplied. Returning all episodes. { - // Start at - if (startIndex.HasValue) + if (_libraryManager.GetItemById(seriesId) is not Series series) { - items = items.Skip(startIndex.Value); + return NotFound("Series not found"); } - // Return limit - if (limit.HasValue) - { - items = items.Take(limit.Value); - } - - return items; + episodes = series.GetEpisodes(user, dtoOptions).ToList(); } + + // Filter after the fact in case the ui doesn't want them + if (isMissing.HasValue) + { + var val = isMissing.Value; + episodes = episodes + .Where(i => ((Episode)i).IsMissingEpisode == val) + .ToList(); + } + + if (startItemId.HasValue) + { + episodes = episodes + .SkipWhile(i => !startItemId.Value.Equals(i.Id)) + .ToList(); + } + + // This must be the last filter + if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default)) + { + episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList(); + } + + if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) + { + episodes.Shuffle(); + } + + var returnItems = episodes; + + if (startIndex.HasValue || limit.HasValue) + { + returnItems = ApplyPaging(episodes, startIndex, limit).ToList(); + } + + var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user); + + return new QueryResult( + startIndex, + episodes.Count, + dtos); + } + + /// + /// Gets seasons for a tv series. + /// + /// The series id. + /// The user id. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. + /// Optional. Filter by special season. + /// Optional. Filter by items that are missing episodes or not. + /// Optional. Return items that are siblings of a supplied item. + /// Optional. Include image information in output. + /// Optional. The max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. Include user data. + /// A on success or a if the series was not found. + [HttpGet("{seriesId}/Seasons")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetSeasons( + [FromRoute, Required] Guid seriesId, + [FromQuery] Guid? userId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] bool? isSpecialSeason, + [FromQuery] bool? isMissing, + [FromQuery] Guid? adjacentTo, + [FromQuery] bool? enableImages, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] bool? enableUserData) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + if (_libraryManager.GetItemById(seriesId) is not Series series) + { + return NotFound("Series not found"); + } + + var seasons = series.GetItemList(new InternalItemsQuery(user) + { + IsMissing = isMissing, + IsSpecialSeason = isSpecialSeason, + AdjacentTo = adjacentTo + }); + + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user); + + return new QueryResult(returnItems); + } + + /// + /// Applies the paging. + /// + /// The items. + /// The start index. + /// The limit. + /// IEnumerable{BaseItem}. + private IEnumerable ApplyPaging(IEnumerable items, int? startIndex, int? limit) + { + // Start at + if (startIndex.HasValue) + { + items = items.Skip(startIndex.Value); + } + + // Return limit + if (limit.HasValue) + { + items = items.Take(limit.Value); + } + + return items; } } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index d77126a353..6946caa2ba 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -20,197 +20,164 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers -{ - /// - /// The universal audio controller. - /// - [Route("")] - public class UniversalAudioController : BaseJellyfinApiController - { - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly MediaInfoHelper _mediaInfoHelper; - private readonly AudioHelper _audioHelper; - private readonly DynamicHlsHelper _dynamicHlsHelper; +namespace Jellyfin.Api.Controllers; - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of . - /// Instance of . - /// Instance of . - public UniversalAudioController( - ILibraryManager libraryManager, - ILogger logger, - MediaInfoHelper mediaInfoHelper, - AudioHelper audioHelper, - DynamicHlsHelper dynamicHlsHelper) +/// +/// The universal audio controller. +/// +[Route("")] +public class UniversalAudioController : BaseJellyfinApiController +{ + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly MediaInfoHelper _mediaInfoHelper; + private readonly AudioHelper _audioHelper; + private readonly DynamicHlsHelper _dynamicHlsHelper; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of . + /// Instance of . + /// Instance of . + public UniversalAudioController( + ILibraryManager libraryManager, + ILogger logger, + MediaInfoHelper mediaInfoHelper, + AudioHelper audioHelper, + DynamicHlsHelper dynamicHlsHelper) + { + _libraryManager = libraryManager; + _logger = logger; + _mediaInfoHelper = mediaInfoHelper; + _audioHelper = audioHelper; + _dynamicHlsHelper = dynamicHlsHelper; + } + + /// + /// Gets an audio stream. + /// + /// The item id. + /// Optional. The audio container. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. The user id. + /// Optional. The audio codec to transcode to. + /// Optional. The maximum number of audio channels. + /// Optional. The number of how many audio channels to transcode to. + /// Optional. The maximum streaming bitrate. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The container to transcode to. + /// Optional. The transcoding protocol. + /// Optional. The maximum audio sample rate. + /// Optional. The maximum audio bit depth. + /// Optional. Whether to enable remote media. + /// Optional. Whether to break on non key frames. + /// Whether to enable redirection. Defaults to true. + /// Audio stream returned. + /// Redirected to remote audio stream. + /// A containing the audio file. + [HttpGet("Audio/{itemId}/universal")] + [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status302Found)] + [ProducesAudioFile] + public async Task GetUniversalAudioStream( + [FromRoute, Required] Guid itemId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] Guid? userId, + [FromQuery] string? audioCodec, + [FromQuery] int? maxAudioChannels, + [FromQuery] int? transcodingAudioChannels, + [FromQuery] int? maxStreamingBitrate, + [FromQuery] int? audioBitRate, + [FromQuery] long? startTimeTicks, + [FromQuery] string? transcodingContainer, + [FromQuery] string? transcodingProtocol, + [FromQuery] int? maxAudioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] bool? enableRemoteMedia, + [FromQuery] bool breakOnNonKeyFrames = false, + [FromQuery] bool enableRedirection = true) + { + var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); + + if (!userId.HasValue || userId.Value.Equals(default)) { - _libraryManager = libraryManager; - _logger = logger; - _mediaInfoHelper = mediaInfoHelper; - _audioHelper = audioHelper; - _dynamicHlsHelper = dynamicHlsHelper; + userId = User.GetUserId(); } - /// - /// Gets an audio stream. - /// - /// The item id. - /// Optional. The audio container. - /// The media version id, if playing an alternate version. - /// The device id of the client requesting. Used to stop encoding processes when needed. - /// Optional. The user id. - /// Optional. The audio codec to transcode to. - /// Optional. The maximum number of audio channels. - /// Optional. The number of how many audio channels to transcode to. - /// Optional. The maximum streaming bitrate. - /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. - /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. - /// Optional. The container to transcode to. - /// Optional. The transcoding protocol. - /// Optional. The maximum audio sample rate. - /// Optional. The maximum audio bit depth. - /// Optional. Whether to enable remote media. - /// Optional. Whether to break on non key frames. - /// Whether to enable redirection. Defaults to true. - /// Audio stream returned. - /// Redirected to remote audio stream. - /// A containing the audio file. - [HttpGet("Audio/{itemId}/universal")] - [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status302Found)] - [ProducesAudioFile] - public async Task GetUniversalAudioStream( - [FromRoute, Required] Guid itemId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container, - [FromQuery] string? mediaSourceId, - [FromQuery] string? deviceId, - [FromQuery] Guid? userId, - [FromQuery] string? audioCodec, - [FromQuery] int? maxAudioChannels, - [FromQuery] int? transcodingAudioChannels, - [FromQuery] int? maxStreamingBitrate, - [FromQuery] int? audioBitRate, - [FromQuery] long? startTimeTicks, - [FromQuery] string? transcodingContainer, - [FromQuery] string? transcodingProtocol, - [FromQuery] int? maxAudioSampleRate, - [FromQuery] int? maxAudioBitDepth, - [FromQuery] bool? enableRemoteMedia, - [FromQuery] bool breakOnNonKeyFrames = false, - [FromQuery] bool enableRedirection = true) + _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); + + var info = await _mediaInfoHelper.GetPlaybackInfo( + itemId, + userId, + mediaSourceId) + .ConfigureAwait(false); + + // set device specific data + var item = _libraryManager.GetItemById(itemId); + + foreach (var sourceInfo in info.MediaSources) { - var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); + _mediaInfoHelper.SetDeviceSpecificData( + item, + sourceInfo, + deviceProfile, + User, + maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate, + startTimeTicks ?? 0, + mediaSourceId ?? string.Empty, + null, + null, + maxAudioChannels, + info.PlaySessionId!, + userId ?? Guid.Empty, + true, + true, + true, + true, + true, + Request.HttpContext.GetNormalizedRemoteIp()); + } - if (!userId.HasValue || userId.Value.Equals(default)) - { - userId = User.GetUserId(); - } + _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); - _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); + foreach (var source in info.MediaSources) + { + _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video); + } - var info = await _mediaInfoHelper.GetPlaybackInfo( - itemId, - userId, - mediaSourceId) - .ConfigureAwait(false); + var mediaSource = info.MediaSources[0]; + if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value) + { + return Redirect(mediaSource.Path); + } - // set device specific data - var item = _libraryManager.GetItemById(itemId); + var isStatic = mediaSource.SupportsDirectStream; + if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) + { + // hls segment container can only be mpegts or fmp4 per ffmpeg documentation + // ffmpeg option -> file extension + // mpegts -> ts + // fmp4 -> mp4 + // TODO: remove this when we switch back to the segment muxer + var supportedHlsContainers = new[] { "ts", "mp4" }; - foreach (var sourceInfo in info.MediaSources) - { - _mediaInfoHelper.SetDeviceSpecificData( - item, - sourceInfo, - deviceProfile, - User, - maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate, - startTimeTicks ?? 0, - mediaSourceId ?? string.Empty, - null, - null, - maxAudioChannels, - info.PlaySessionId!, - userId ?? Guid.Empty, - true, - true, - true, - true, - true, - Request.HttpContext.GetNormalizedRemoteIp()); - } - - _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); - - foreach (var source in info.MediaSources) - { - _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video); - } - - var mediaSource = info.MediaSources[0]; - if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value) - { - return Redirect(mediaSource.Path); - } - - var isStatic = mediaSource.SupportsDirectStream; - if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) - { - // hls segment container can only be mpegts or fmp4 per ffmpeg documentation - // ffmpeg option -> file extension - // mpegts -> ts - // fmp4 -> mp4 - // TODO: remove this when we switch back to the segment muxer - var supportedHlsContainers = new[] { "ts", "mp4" }; - - var dynamicHlsRequestDto = new HlsAudioRequestDto - { - Id = itemId, - Container = ".m3u8", - Static = isStatic, - PlaySessionId = info.PlaySessionId, - // fallback to mpegts if device reports some weird value unsupported by hls - SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts", - MediaSourceId = mediaSourceId, - DeviceId = deviceId, - AudioCodec = audioCodec, - EnableAutoStreamCopy = true, - AllowAudioStreamCopy = true, - AllowVideoStreamCopy = true, - BreakOnNonKeyFrames = breakOnNonKeyFrames, - AudioSampleRate = maxAudioSampleRate, - MaxAudioChannels = maxAudioChannels, - MaxAudioBitDepth = maxAudioBitDepth, - AudioBitRate = audioBitRate ?? maxStreamingBitrate, - StartTimeTicks = startTimeTicks, - SubtitleMethod = SubtitleDeliveryMethod.Hls, - RequireAvc = false, - DeInterlace = false, - RequireNonAnamorphic = false, - EnableMpegtsM2TsMode = false, - TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(), - Context = EncodingContext.Static, - StreamOptions = new Dictionary(), - EnableAdaptiveBitrateStreaming = true - }; - - return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true) - .ConfigureAwait(false); - } - - var audioStreamingDto = new StreamingRequestDto + var dynamicHlsRequestDto = new HlsAudioRequestDto { Id = itemId, - Container = isStatic ? null : ("." + mediaSource.TranscodingContainer), + Container = ".m3u8", Static = isStatic, PlaySessionId = info.PlaySessionId, + // fallback to mpegts if device reports some weird value unsupported by hls + SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts", MediaSourceId = mediaSourceId, DeviceId = deviceId, AudioCodec = audioCodec, @@ -220,121 +187,153 @@ namespace Jellyfin.Api.Controllers BreakOnNonKeyFrames = breakOnNonKeyFrames, AudioSampleRate = maxAudioSampleRate, MaxAudioChannels = maxAudioChannels, - AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate), MaxAudioBitDepth = maxAudioBitDepth, - AudioChannels = maxAudioChannels, - CopyTimestamps = true, + AudioBitRate = audioBitRate ?? maxStreamingBitrate, StartTimeTicks = startTimeTicks, - SubtitleMethod = SubtitleDeliveryMethod.Embed, + SubtitleMethod = SubtitleDeliveryMethod.Hls, + RequireAvc = false, + DeInterlace = false, + RequireNonAnamorphic = false, + EnableMpegtsM2TsMode = false, TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(), - Context = EncodingContext.Static + Context = EncodingContext.Static, + StreamOptions = new Dictionary(), + EnableAdaptiveBitrateStreaming = true }; - return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false); + return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true) + .ConfigureAwait(false); } - private DeviceProfile GetDeviceProfile( - string[] containers, - string? transcodingContainer, - string? audioCodec, - string? transcodingProtocol, - bool? breakOnNonKeyFrames, - int? transcodingAudioChannels, - int? maxAudioSampleRate, - int? maxAudioBitDepth, - int? maxAudioChannels) + var audioStreamingDto = new StreamingRequestDto { - var deviceProfile = new DeviceProfile(); + Id = itemId, + Container = isStatic ? null : ("." + mediaSource.TranscodingContainer), + Static = isStatic, + PlaySessionId = info.PlaySessionId, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = true, + AllowAudioStreamCopy = true, + AllowVideoStreamCopy = true, + BreakOnNonKeyFrames = breakOnNonKeyFrames, + AudioSampleRate = maxAudioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate), + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = maxAudioChannels, + CopyTimestamps = true, + StartTimeTicks = startTimeTicks, + SubtitleMethod = SubtitleDeliveryMethod.Embed, + TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(), + Context = EncodingContext.Static + }; - int len = containers.Length; - var directPlayProfiles = new DirectPlayProfile[len]; - for (int i = 0; i < len; i++) + return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false); + } + + private DeviceProfile GetDeviceProfile( + string[] containers, + string? transcodingContainer, + string? audioCodec, + string? transcodingProtocol, + bool? breakOnNonKeyFrames, + int? transcodingAudioChannels, + int? maxAudioSampleRate, + int? maxAudioBitDepth, + int? maxAudioChannels) + { + var deviceProfile = new DeviceProfile(); + + int len = containers.Length; + var directPlayProfiles = new DirectPlayProfile[len]; + for (int i = 0; i < len; i++) + { + var parts = containers[i].Split('|', StringSplitOptions.RemoveEmptyEntries); + + var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1)); + + directPlayProfiles[i] = new DirectPlayProfile { - var parts = containers[i].Split('|', StringSplitOptions.RemoveEmptyEntries); - - var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1)); - - directPlayProfiles[i] = new DirectPlayProfile - { - Type = DlnaProfileType.Audio, - Container = parts[0], - AudioCodec = audioCodecs - }; - } - - deviceProfile.DirectPlayProfiles = directPlayProfiles; - - deviceProfile.TranscodingProfiles = new[] - { - new TranscodingProfile - { - Type = DlnaProfileType.Audio, - Context = EncodingContext.Streaming, - Container = transcodingContainer ?? "mp3", - AudioCodec = audioCodec ?? "mp3", - Protocol = transcodingProtocol ?? "http", - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, - MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture) - } + Type = DlnaProfileType.Audio, + Container = parts[0], + AudioCodec = audioCodecs }; - - var codecProfiles = new List(); - var conditions = new List(); - - if (maxAudioSampleRate.HasValue) - { - // codec profile - conditions.Add( - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - IsRequired = false, - Property = ProfileConditionValue.AudioSampleRate, - Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) - }); - } - - if (maxAudioBitDepth.HasValue) - { - // codec profile - conditions.Add( - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - IsRequired = false, - Property = ProfileConditionValue.AudioBitDepth, - Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture) - }); - } - - if (maxAudioChannels.HasValue) - { - // codec profile - conditions.Add( - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - IsRequired = false, - Property = ProfileConditionValue.AudioChannels, - Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) - }); - } - - if (conditions.Count > 0) - { - // codec profile - codecProfiles.Add( - new CodecProfile - { - Type = CodecType.Audio, - Container = string.Join(',', containers), - Conditions = conditions.ToArray() - }); - } - - deviceProfile.CodecProfiles = codecProfiles.ToArray(); - - return deviceProfile; } + + deviceProfile.DirectPlayProfiles = directPlayProfiles; + + deviceProfile.TranscodingProfiles = new[] + { + new TranscodingProfile + { + Type = DlnaProfileType.Audio, + Context = EncodingContext.Streaming, + Container = transcodingContainer ?? "mp3", + AudioCodec = audioCodec ?? "mp3", + Protocol = transcodingProtocol ?? "http", + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture) + } + }; + + var codecProfiles = new List(); + var conditions = new List(); + + if (maxAudioSampleRate.HasValue) + { + // codec profile + conditions.Add( + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + IsRequired = false, + Property = ProfileConditionValue.AudioSampleRate, + Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) + }); + } + + if (maxAudioBitDepth.HasValue) + { + // codec profile + conditions.Add( + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + IsRequired = false, + Property = ProfileConditionValue.AudioBitDepth, + Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture) + }); + } + + if (maxAudioChannels.HasValue) + { + // codec profile + conditions.Add( + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + IsRequired = false, + Property = ProfileConditionValue.AudioChannels, + Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) + }); + } + + if (conditions.Count > 0) + { + // codec profile + codecProfiles.Add( + new CodecProfile + { + Type = CodecType.Audio, + Container = string.Join(',', containers), + Conditions = conditions.ToArray() + }); + } + + deviceProfile.CodecProfiles = codecProfiles.ToArray(); + + return deviceProfile; } } diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 06f2227b83..7f184f31e7 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -25,564 +25,563 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// User controller. +/// +[Route("Users")] +public class UserController : BaseJellyfinApiController { + private readonly IUserManager _userManager; + private readonly ISessionManager _sessionManager; + private readonly INetworkManager _networkManager; + private readonly IDeviceManager _deviceManager; + private readonly IAuthorizationContext _authContext; + private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + private readonly IQuickConnect _quickConnectManager; + /// - /// User controller. + /// Initializes a new instance of the class. /// - [Route("Users")] - public class UserController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public UserController( + IUserManager userManager, + ISessionManager sessionManager, + INetworkManager networkManager, + IDeviceManager deviceManager, + IAuthorizationContext authContext, + IServerConfigurationManager config, + ILogger logger, + IQuickConnect quickConnectManager) { - private readonly IUserManager _userManager; - private readonly ISessionManager _sessionManager; - private readonly INetworkManager _networkManager; - private readonly IDeviceManager _deviceManager; - private readonly IAuthorizationContext _authContext; - private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; - private readonly IQuickConnect _quickConnectManager; + _userManager = userManager; + _sessionManager = sessionManager; + _networkManager = networkManager; + _deviceManager = deviceManager; + _authContext = authContext; + _config = config; + _logger = logger; + _quickConnectManager = quickConnectManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public UserController( - IUserManager userManager, - ISessionManager sessionManager, - INetworkManager networkManager, - IDeviceManager deviceManager, - IAuthorizationContext authContext, - IServerConfigurationManager config, - ILogger logger, - IQuickConnect quickConnectManager) + /// + /// Gets a list of users. + /// + /// Optional filter by IsHidden=true or false. + /// Optional filter by IsDisabled=true or false. + /// Users returned. + /// An containing the users. + [HttpGet] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetUsers( + [FromQuery] bool? isHidden, + [FromQuery] bool? isDisabled) + { + var users = Get(isHidden, isDisabled, false, false); + return Ok(users); + } + + /// + /// Gets a list of publicly visible users for display on a login screen. + /// + /// Public users returned. + /// An containing the public users. + [HttpGet("Public")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetPublicUsers() + { + // If the startup wizard hasn't been completed then just return all users + if (!_config.Configuration.IsStartupWizardCompleted) { - _userManager = userManager; - _sessionManager = sessionManager; - _networkManager = networkManager; - _deviceManager = deviceManager; - _authContext = authContext; - _config = config; - _logger = logger; - _quickConnectManager = quickConnectManager; + return Ok(Get(false, false, false, false)); } - /// - /// Gets a list of users. - /// - /// Optional filter by IsHidden=true or false. - /// Optional filter by IsDisabled=true or false. - /// Users returned. - /// An containing the users. - [HttpGet] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetUsers( - [FromQuery] bool? isHidden, - [FromQuery] bool? isDisabled) + return Ok(Get(false, false, true, true)); + } + + /// + /// Gets a user by Id. + /// + /// The user id. + /// User returned. + /// User not found. + /// An with information about the user or a if the user was not found. + [HttpGet("{userId}")] + [Authorize(Policy = Policies.IgnoreParentalControl)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GetUserById([FromRoute, Required] Guid userId) + { + var user = _userManager.GetUserById(userId); + + if (user is null) { - var users = Get(isHidden, isDisabled, false, false); - return Ok(users); + return NotFound("User not found"); } - /// - /// Gets a list of publicly visible users for display on a login screen. - /// - /// Public users returned. - /// An containing the public users. - [HttpGet("Public")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> 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)); - } + var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString()); + return result; + } - return Ok(Get(false, false, true, true)); + /// + /// Deletes a user. + /// + /// The user id. + /// User deleted. + /// User not found. + /// A indicating success or a if the user was not found. + [HttpDelete("{userId}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteUser([FromRoute, Required] Guid userId) + { + var user = _userManager.GetUserById(userId); + await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false); + await _userManager.DeleteUserAsync(userId).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Authenticates a user. + /// + /// The user id. + /// The password as plain text. + /// User authenticated. + /// Sha1-hashed password only is not allowed. + /// User not found. + /// A containing an . + [HttpPost("{userId}/Authenticate")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Obsolete("Authenticate with username instead")] + public async Task> AuthenticateUser( + [FromRoute, Required] Guid userId, + [FromQuery, Required] string pw) + { + var user = _userManager.GetUserById(userId); + + if (user is null) + { + return NotFound("User not found"); } - /// - /// Gets a user by Id. - /// - /// The user id. - /// User returned. - /// User not found. - /// An with information about the user or a if the user was not found. - [HttpGet("{userId}")] - [Authorize(Policy = Policies.IgnoreParentalControl)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetUserById([FromRoute, Required] Guid userId) + AuthenticateUserByName request = new AuthenticateUserByName { - var user = _userManager.GetUserById(userId); + Username = user.Username, + Pw = pw + }; + return await AuthenticateUserByName(request).ConfigureAwait(false); + } - if (user is null) - { - return NotFound("User not found"); - } + /// + /// Authenticates a user by name. + /// + /// The request. + /// User authenticated. + /// A containing an with information about the new session. + [HttpPost("AuthenticateByName")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request) + { + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString()); - return result; - } - - /// - /// Deletes a user. - /// - /// The user id. - /// User deleted. - /// User not found. - /// A indicating success or a if the user was not found. - [HttpDelete("{userId}")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteUser([FromRoute, Required] Guid userId) + try { - var user = _userManager.GetUserById(userId); - await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false); - await _userManager.DeleteUserAsync(userId).ConfigureAwait(false); - return NoContent(); - } - - /// - /// Authenticates a user. - /// - /// The user id. - /// The password as plain text. - /// User authenticated. - /// Sha1-hashed password only is not allowed. - /// User not found. - /// A containing an . - [HttpPost("{userId}/Authenticate")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [Obsolete("Authenticate with username instead")] - public async Task> AuthenticateUser( - [FromRoute, Required] Guid userId, - [FromQuery, Required] string pw) - { - var user = _userManager.GetUserById(userId); - - if (user is null) + var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest { - return NotFound("User not found"); - } - - AuthenticateUserByName request = new AuthenticateUserByName - { - Username = user.Username, - Pw = pw - }; - return await AuthenticateUserByName(request).ConfigureAwait(false); - } - - /// - /// Authenticates a user by name. - /// - /// The request. - /// User authenticated. - /// A containing an with information about the new session. - [HttpPost("AuthenticateByName")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request) - { - var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); - - try - { - var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest - { - App = auth.Client, - AppVersion = auth.Version, - DeviceId = auth.DeviceId, - DeviceName = auth.Device, - Password = request.Pw, - RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(), - Username = request.Username - }).ConfigureAwait(false); - - return result; - } - catch (SecurityException e) - { - // rethrow adding IP address to message - throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e); - } - } - - /// - /// Authenticates a user with quick connect. - /// - /// The request. - /// User authenticated. - /// Missing token. - /// A containing an with information about the new session. - [HttpPost("AuthenticateWithQuickConnect")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) - { - try - { - return _quickConnectManager.GetAuthorizedRequest(request.Secret); - } - catch (SecurityException e) - { - // rethrow adding IP address to message - throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e); - } - } - - /// - /// Updates a user's password. - /// - /// The user id. - /// The request. - /// Password successfully reset. - /// User is not allowed to update the password. - /// User not found. - /// A indicating success or a or a on failure. - [HttpPost("{userId}/Password")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task UpdateUserPassword( - [FromRoute, Required] Guid userId, - [FromBody, Required] UpdateUserPassword request) - { - if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) - { - return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password."); - } - - var user = _userManager.GetUserById(userId); - - if (user is null) - { - return NotFound("User not found"); - } - - if (request.ResetPassword) - { - await _userManager.ResetPassword(user).ConfigureAwait(false); - } - else - { - if (!User.IsInRole(UserRoles.Administrator) || User.GetUserId().Equals(userId)) - { - var success = await _userManager.AuthenticateUser( - user.Username, - request.CurrentPw, - request.CurrentPw, - HttpContext.GetNormalizedRemoteIp().ToString(), - false).ConfigureAwait(false); - - if (success is null) - { - return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered."); - } - } - - await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); - - var currentToken = User.GetToken(); - - await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); - } - - return NoContent(); - } - - /// - /// Updates a user's easy password. - /// - /// The user id. - /// The request. - /// Password successfully reset. - /// User is not allowed to update the password. - /// User not found. - /// A indicating success or a or a on failure. - [HttpPost("{userId}/EasyPassword")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task UpdateUserEasyPassword( - [FromRoute, Required] Guid userId, - [FromBody, Required] UpdateUserEasyPassword request) - { - if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) - { - return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password."); - } - - var user = _userManager.GetUserById(userId); - - if (user is null) - { - return NotFound("User not found"); - } - - if (request.ResetPassword) - { - await _userManager.ResetEasyPassword(user).ConfigureAwait(false); - } - else - { - await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false); - } - - return NoContent(); - } - - /// - /// Updates a user. - /// - /// The user id. - /// The updated user model. - /// User updated. - /// User information was not supplied. - /// User update forbidden. - /// A indicating success or a or a on failure. - [HttpPost("{userId}")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task UpdateUser( - [FromRoute, Required] Guid userId, - [FromBody, Required] UserDto updateUser) - { - if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) - { - return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed."); - } - - var user = _userManager.GetUserById(userId); - - if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) - { - await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); - } - - await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Updates a user policy. - /// - /// The user id. - /// The new user policy. - /// User policy updated. - /// User policy was not supplied. - /// User policy update forbidden. - /// A indicating success or a or a on failure.. - [HttpPost("{userId}/Policy")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task UpdateUserPolicy( - [FromRoute, Required] Guid userId, - [FromBody, Required] UserPolicy newPolicy) - { - var user = _userManager.GetUserById(userId); - - // If removing admin access - if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)) - { - if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) - { - return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access."); - } - } - - // If disabling - if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator)) - { - return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled."); - } - - // If disabling - if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled)) - { - if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) - { - return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system."); - } - - var currentToken = User.GetToken(); - await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); - } - - await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Updates a user configuration. - /// - /// The user id. - /// The new user configuration. - /// User configuration updated. - /// User configuration update forbidden. - /// A indicating success. - [HttpPost("{userId}/Configuration")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task UpdateUserConfiguration( - [FromRoute, Required] Guid userId, - [FromBody, Required] UserConfiguration userConfig) - { - if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) - { - return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed"); - } - - await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Creates a user. - /// - /// The create user by name request body. - /// User created. - /// An of the new user. - [HttpPost("New")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> CreateUserByName([FromBody, Required] CreateUserByName request) - { - var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false); - - // no need to authenticate password for new user - if (request.Password is not null) - { - await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); - } - - var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString()); + App = auth.Client, + AppVersion = auth.Version, + DeviceId = auth.DeviceId, + DeviceName = auth.Device, + Password = request.Pw, + RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(), + Username = request.Username + }).ConfigureAwait(false); return result; } - - /// - /// Initiates the forgot password process for a local user. - /// - /// The forgot password request containing the entered username. - /// Password reset process started. - /// A containing a . - [HttpPost("ForgotPassword")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest) + catch (SecurityException e) { - var ip = HttpContext.GetNormalizedRemoteIp(); - var isLocal = HttpContext.IsLocal() - || _networkManager.IsInLocalNetwork(ip); - - if (isLocal) - { - _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip); - } - - var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false); - - return result; - } - - /// - /// Redeems a forgot password pin. - /// - /// The forgot password pin request containing the entered pin. - /// Pin reset process started. - /// A containing a . - [HttpPost("ForgotPassword/Pin")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest) - { - var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false); - return result; - } - - /// - /// Gets the user based on auth token. - /// - /// User returned. - /// Token is not owned by a user. - /// A for the authenticated user. - [HttpGet("Me")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public ActionResult GetCurrentUser() - { - var userId = User.GetUserId(); - if (userId.Equals(default)) - { - return BadRequest(); - } - - var user = _userManager.GetUserById(userId); - if (user is null) - { - return BadRequest(); - } - - return _userManager.GetUserDto(user); - } - - private IEnumerable Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) - { - var users = _userManager.Users; - - if (isDisabled.HasValue) - { - users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == isDisabled.Value); - } - - if (isHidden.HasValue) - { - users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == isHidden.Value); - } - - if (filterByDevice) - { - var deviceId = User.GetDeviceId(); - - if (!string.IsNullOrWhiteSpace(deviceId)) - { - users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId)); - } - } - - if (filterByNetwork) - { - if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())) - { - users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); - } - } - - var result = users - .OrderBy(u => u.Username) - .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString())); - - return result; + // rethrow adding IP address to message + throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e); } } + + /// + /// Authenticates a user with quick connect. + /// + /// The request. + /// User authenticated. + /// Missing token. + /// A containing an with information about the new session. + [HttpPost("AuthenticateWithQuickConnect")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) + { + try + { + return _quickConnectManager.GetAuthorizedRequest(request.Secret); + } + catch (SecurityException e) + { + // rethrow adding IP address to message + throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e); + } + } + + /// + /// Updates a user's password. + /// + /// The user id. + /// The request. + /// Password successfully reset. + /// User is not allowed to update the password. + /// User not found. + /// A indicating success or a or a on failure. + [HttpPost("{userId}/Password")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateUserPassword( + [FromRoute, Required] Guid userId, + [FromBody, Required] UpdateUserPassword request) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) + { + return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password."); + } + + var user = _userManager.GetUserById(userId); + + if (user is null) + { + return NotFound("User not found"); + } + + if (request.ResetPassword) + { + await _userManager.ResetPassword(user).ConfigureAwait(false); + } + else + { + if (!User.IsInRole(UserRoles.Administrator) || User.GetUserId().Equals(userId)) + { + var success = await _userManager.AuthenticateUser( + user.Username, + request.CurrentPw, + request.CurrentPw, + HttpContext.GetNormalizedRemoteIp().ToString(), + false).ConfigureAwait(false); + + if (success is null) + { + return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered."); + } + } + + await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); + + var currentToken = User.GetToken(); + + await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); + } + + return NoContent(); + } + + /// + /// Updates a user's easy password. + /// + /// The user id. + /// The request. + /// Password successfully reset. + /// User is not allowed to update the password. + /// User not found. + /// A indicating success or a or a on failure. + [HttpPost("{userId}/EasyPassword")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateUserEasyPassword( + [FromRoute, Required] Guid userId, + [FromBody, Required] UpdateUserEasyPassword request) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) + { + return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password."); + } + + var user = _userManager.GetUserById(userId); + + if (user is null) + { + return NotFound("User not found"); + } + + if (request.ResetPassword) + { + await _userManager.ResetEasyPassword(user).ConfigureAwait(false); + } + else + { + await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false); + } + + return NoContent(); + } + + /// + /// Updates a user. + /// + /// The user id. + /// The updated user model. + /// User updated. + /// User information was not supplied. + /// User update forbidden. + /// A indicating success or a or a on failure. + [HttpPost("{userId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task UpdateUser( + [FromRoute, Required] Guid userId, + [FromBody, Required] UserDto updateUser) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) + { + return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed."); + } + + var user = _userManager.GetUserById(userId); + + if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) + { + await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); + } + + await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Updates a user policy. + /// + /// The user id. + /// The new user policy. + /// User policy updated. + /// User policy was not supplied. + /// User policy update forbidden. + /// A indicating success or a or a on failure.. + [HttpPost("{userId}/Policy")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task UpdateUserPolicy( + [FromRoute, Required] Guid userId, + [FromBody, Required] UserPolicy newPolicy) + { + var user = _userManager.GetUserById(userId); + + // If removing admin access + if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator)) + { + if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) + { + return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access."); + } + } + + // If disabling + if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator)) + { + return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled."); + } + + // If disabling + if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled)) + { + if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1) + { + return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system."); + } + + var currentToken = User.GetToken(); + await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false); + } + + await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Updates a user configuration. + /// + /// The user id. + /// The new user configuration. + /// User configuration updated. + /// User configuration update forbidden. + /// A indicating success. + [HttpPost("{userId}/Configuration")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task UpdateUserConfiguration( + [FromRoute, Required] Guid userId, + [FromBody, Required] UserConfiguration userConfig) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) + { + return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed"); + } + + await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Creates a user. + /// + /// The create user by name request body. + /// User created. + /// An of the new user. + [HttpPost("New")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> CreateUserByName([FromBody, Required] CreateUserByName request) + { + var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false); + + // no need to authenticate password for new user + if (request.Password is not null) + { + await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); + } + + var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString()); + + return result; + } + + /// + /// Initiates the forgot password process for a local user. + /// + /// The forgot password request containing the entered username. + /// Password reset process started. + /// A containing a . + [HttpPost("ForgotPassword")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest) + { + var ip = HttpContext.GetNormalizedRemoteIp(); + var isLocal = HttpContext.IsLocal() + || _networkManager.IsInLocalNetwork(ip); + + if (isLocal) + { + _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip); + } + + var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false); + + return result; + } + + /// + /// Redeems a forgot password pin. + /// + /// The forgot password pin request containing the entered pin. + /// Pin reset process started. + /// A containing a . + [HttpPost("ForgotPassword/Pin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest) + { + var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false); + return result; + } + + /// + /// Gets the user based on auth token. + /// + /// User returned. + /// Token is not owned by a user. + /// A for the authenticated user. + [HttpGet("Me")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public ActionResult GetCurrentUser() + { + var userId = User.GetUserId(); + if (userId.Equals(default)) + { + return BadRequest(); + } + + var user = _userManager.GetUserById(userId); + if (user is null) + { + return BadRequest(); + } + + return _userManager.GetUserDto(user); + } + + private IEnumerable Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork) + { + var users = _userManager.Users; + + if (isDisabled.HasValue) + { + users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == isDisabled.Value); + } + + if (isHidden.HasValue) + { + users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == isHidden.Value); + } + + if (filterByDevice) + { + var deviceId = User.GetDeviceId(); + + if (!string.IsNullOrWhiteSpace(deviceId)) + { + users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId)); + } + } + + if (filterByNetwork) + { + if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())) + { + users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); + } + } + + var result = users + .OrderBy(u => u.Username) + .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString())); + + return result; + } } diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index cd21c5f6ff..0727f36d77 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -23,406 +23,405 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// User library controller. +/// +[Route("")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class UserLibraryController : BaseJellyfinApiController { + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataRepository; + private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IUserViewManager _userViewManager; + private readonly IFileSystem _fileSystem; + private readonly ILyricManager _lyricManager; + /// - /// User library controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class UserLibraryController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public UserLibraryController( + IUserManager userManager, + IUserDataManager userDataRepository, + ILibraryManager libraryManager, + IDtoService dtoService, + IUserViewManager userViewManager, + IFileSystem fileSystem, + ILyricManager lyricManager) { - private readonly IUserManager _userManager; - private readonly IUserDataManager _userDataRepository; - private readonly ILibraryManager _libraryManager; - private readonly IDtoService _dtoService; - private readonly IUserViewManager _userViewManager; - private readonly IFileSystem _fileSystem; - private readonly ILyricManager _lyricManager; + _userManager = userManager; + _userDataRepository = userDataRepository; + _libraryManager = libraryManager; + _dtoService = dtoService; + _userViewManager = userViewManager; + _fileSystem = fileSystem; + _lyricManager = lyricManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public UserLibraryController( - IUserManager userManager, - IUserDataManager userDataRepository, - ILibraryManager libraryManager, - IDtoService dtoService, - IUserViewManager userViewManager, - IFileSystem fileSystem, - ILyricManager lyricManager) + /// + /// Gets an item from a user's library. + /// + /// User id. + /// Item id. + /// Item returned. + /// An containing the d item. + [HttpGet("Users/{userId}/Items/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(default) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); + + var dtoOptions = new DtoOptions().AddClientFields(User); + + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + /// + /// Gets the root folder from a user's library. + /// + /// User id. + /// Root folder returned. + /// An containing the user's root folder. + [HttpGet("Users/{userId}/Items/Root")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetRootFolder([FromRoute, Required] Guid userId) + { + var user = _userManager.GetUserById(userId); + var item = _libraryManager.GetUserRootFolder(); + var dtoOptions = new DtoOptions().AddClientFields(User); + return _dtoService.GetBaseItemDto(item, dtoOptions, user); + } + + /// + /// Gets intros to play before the main media item plays. + /// + /// User id. + /// Item id. + /// Intros returned. + /// An containing the intros to play. + [HttpGet("Users/{userId}/Items/{itemId}/Intros")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(default) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false); + var dtoOptions = new DtoOptions().AddClientFields(User); + var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray(); + + return new QueryResult(dtos); + } + + /// + /// Marks an item as a favorite. + /// + /// User id. + /// Item id. + /// Item marked as favorite. + /// An containing the . + [HttpPost("Users/{userId}/FavoriteItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + { + return MarkFavorite(userId, itemId, true); + } + + /// + /// Unmarks item as a favorite. + /// + /// User id. + /// Item id. + /// Item unmarked as favorite. + /// An containing the . + [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + { + return MarkFavorite(userId, itemId, false); + } + + /// + /// Deletes a user's saved personal rating for an item. + /// + /// User id. + /// Item id. + /// Personal rating removed. + /// An containing the . + [HttpDelete("Users/{userId}/Items/{itemId}/Rating")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + { + return UpdateUserItemRatingInternal(userId, itemId, null); + } + + /// + /// Updates a user's rating for an item. + /// + /// User id. + /// Item id. + /// Whether this is likes. + /// Item rating updated. + /// An containing the . + [HttpPost("Users/{userId}/Items/{itemId}/Rating")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes) + { + return UpdateUserItemRatingInternal(userId, itemId, likes); + } + + /// + /// Gets local trailers for an item. + /// + /// User id. + /// Item id. + /// An containing the item's local trailers. + /// The items local trailers. + [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(default) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + var dtoOptions = new DtoOptions().AddClientFields(User); + + if (item is IHasTrailers hasTrailers) { - _userManager = userManager; - _userDataRepository = userDataRepository; - _libraryManager = libraryManager; - _dtoService = dtoService; - _userViewManager = userViewManager; - _fileSystem = fileSystem; - _lyricManager = lyricManager; + var trailers = hasTrailers.LocalTrailers; + return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable()); } - /// - /// Gets an item from a user's library. - /// - /// User id. - /// Item id. - /// Item returned. - /// An containing the d item. - [HttpGet("Users/{userId}/Items/{itemId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + return Ok(item.GetExtras() + .Where(e => e.ExtraType == ExtraType.Trailer) + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))); + } + + /// + /// Gets special features for an item. + /// + /// User id. + /// Item id. + /// Special features returned. + /// An containing the special features. + [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(default) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + var dtoOptions = new DtoOptions().AddClientFields(User); + + return Ok(item + .GetExtras() + .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value)) + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))); + } + + /// + /// Gets latest media. + /// + /// User id. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. + /// Filter by items that are played, or not. + /// Optional. include image information in output. + /// Optional. the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. include user data. + /// Return item limit. + /// Whether or not to group items into a parent container. + /// Latest media returned. + /// An containing the latest media. + [HttpGet("Users/{userId}/Items/Latest")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetLatestMedia( + [FromRoute, Required] Guid userId, + [FromQuery] Guid? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery] bool? isPlayed, + [FromQuery] bool? enableImages, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] bool? enableUserData, + [FromQuery] int limit = 20, + [FromQuery] bool groupItems = true) + { + var user = _userManager.GetUserById(userId); + + if (!isPlayed.HasValue) { - var user = _userManager.GetUserById(userId); - - var item = itemId.Equals(default) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.GetItemById(itemId); - - await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); - - var dtoOptions = new DtoOptions().AddClientFields(User); - - return _dtoService.GetBaseItemDto(item, dtoOptions, user); - } - - /// - /// Gets the root folder from a user's library. - /// - /// User id. - /// Root folder returned. - /// An containing the user's root folder. - [HttpGet("Users/{userId}/Items/Root")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetRootFolder([FromRoute, Required] Guid userId) - { - var user = _userManager.GetUserById(userId); - var item = _libraryManager.GetUserRootFolder(); - var dtoOptions = new DtoOptions().AddClientFields(User); - return _dtoService.GetBaseItemDto(item, dtoOptions, user); - } - - /// - /// Gets intros to play before the main media item plays. - /// - /// User id. - /// Item id. - /// Intros returned. - /// An containing the intros to play. - [HttpGet("Users/{userId}/Items/{itemId}/Intros")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) - { - var user = _userManager.GetUserById(userId); - - var item = itemId.Equals(default) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.GetItemById(itemId); - - var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false); - var dtoOptions = new DtoOptions().AddClientFields(User); - var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray(); - - return new QueryResult(dtos); - } - - /// - /// Marks an item as a favorite. - /// - /// User id. - /// Item id. - /// Item marked as favorite. - /// An containing the . - [HttpPost("Users/{userId}/FavoriteItems/{itemId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) - { - return MarkFavorite(userId, itemId, true); - } - - /// - /// Unmarks item as a favorite. - /// - /// User id. - /// Item id. - /// Item unmarked as favorite. - /// An containing the . - [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) - { - return MarkFavorite(userId, itemId, false); - } - - /// - /// Deletes a user's saved personal rating for an item. - /// - /// User id. - /// Item id. - /// Personal rating removed. - /// An containing the . - [HttpDelete("Users/{userId}/Items/{itemId}/Rating")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) - { - return UpdateUserItemRatingInternal(userId, itemId, null); - } - - /// - /// Updates a user's rating for an item. - /// - /// User id. - /// Item id. - /// Whether this is likes. - /// Item rating updated. - /// An containing the . - [HttpPost("Users/{userId}/Items/{itemId}/Rating")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes) - { - return UpdateUserItemRatingInternal(userId, itemId, likes); - } - - /// - /// Gets local trailers for an item. - /// - /// User id. - /// Item id. - /// An containing the item's local trailers. - /// The items local trailers. - [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) - { - var user = _userManager.GetUserById(userId); - - var item = itemId.Equals(default) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.GetItemById(itemId); - - var dtoOptions = new DtoOptions().AddClientFields(User); - - if (item is IHasTrailers hasTrailers) + if (user.HidePlayedInLatest) { - var trailers = hasTrailers.LocalTrailers; - return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable()); + isPlayed = false; + } + } + + var dtoOptions = new DtoOptions { Fields = fields } + .AddClientFields(User) + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); + + var list = _userViewManager.GetLatestItems( + new LatestItemsQuery + { + GroupItems = groupItems, + IncludeItemTypes = includeItemTypes, + IsPlayed = isPlayed, + Limit = limit, + ParentId = parentId ?? Guid.Empty, + UserId = userId, + }, + dtoOptions); + + var dtos = list.Select(i => + { + var item = i.Item2[0]; + var childCount = 0; + + if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum)) + { + item = i.Item1; + childCount = i.Item2.Count; } - return Ok(item.GetExtras() - .Where(e => e.ExtraType == ExtraType.Trailer) - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))); - } + var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user); - /// - /// Gets special features for an item. - /// - /// User id. - /// Item id. - /// Special features returned. - /// An containing the special features. - [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + dto.ChildCount = childCount; + + return dto; + }); + + return Ok(dtos); + } + + private async Task RefreshItemOnDemandIfNeeded(BaseItem item) + { + if (item is Person) { - var user = _userManager.GetUserById(userId); + var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary); + var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3; - var item = itemId.Equals(default) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.GetItemById(itemId); - - var dtoOptions = new DtoOptions().AddClientFields(User); - - return Ok(item - .GetExtras() - .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value)) - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))); - } - - /// - /// Gets latest media. - /// - /// User id. - /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// Optional. Specify additional fields of information to return in the output. - /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. - /// Filter by items that are played, or not. - /// Optional. include image information in output. - /// Optional. the max number of images to return, per image type. - /// Optional. The image types to include in the output. - /// Optional. include user data. - /// Return item limit. - /// Whether or not to group items into a parent container. - /// Latest media returned. - /// An containing the latest media. - [HttpGet("Users/{userId}/Items/Latest")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetLatestMedia( - [FromRoute, Required] Guid userId, - [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, - [FromQuery] bool? isPlayed, - [FromQuery] bool? enableImages, - [FromQuery] int? imageTypeLimit, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] bool? enableUserData, - [FromQuery] int limit = 20, - [FromQuery] bool groupItems = true) - { - var user = _userManager.GetUserById(userId); - - if (!isPlayed.HasValue) + if (!hasMetdata) { - if (user.HidePlayedInLatest) + var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { - isPlayed = false; - } + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + ForceSave = performFullRefresh + }; + + await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false); } - - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - - var list = _userViewManager.GetLatestItems( - new LatestItemsQuery - { - GroupItems = groupItems, - IncludeItemTypes = includeItemTypes, - IsPlayed = isPlayed, - Limit = limit, - ParentId = parentId ?? Guid.Empty, - UserId = userId, - }, - dtoOptions); - - var dtos = list.Select(i => - { - var item = i.Item2[0]; - var childCount = 0; - - if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum)) - { - item = i.Item1; - childCount = i.Item2.Count; - } - - var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user); - - dto.ChildCount = childCount; - - return dto; - }); - - return Ok(dtos); - } - - private async Task RefreshItemOnDemandIfNeeded(BaseItem item) - { - if (item is Person) - { - var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary); - var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3; - - if (!hasMetdata) - { - var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - ForceSave = performFullRefresh - }; - - await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false); - } - } - } - - /// - /// Marks the favorite. - /// - /// The user id. - /// The item id. - /// if set to true [is favorite]. - private UserItemDataDto MarkFavorite(Guid userId, Guid itemId, bool isFavorite) - { - var user = _userManager.GetUserById(userId); - - var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); - - // Get the user data for this item - var data = _userDataRepository.GetUserData(user, item); - - // Set favorite status - data.IsFavorite = isFavorite; - - _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); - - return _userDataRepository.GetUserDataDto(item, user); - } - - /// - /// Updates the user item rating. - /// - /// The user id. - /// The item id. - /// if set to true [likes]. - private UserItemDataDto UpdateUserItemRatingInternal(Guid userId, Guid itemId, bool? likes) - { - var user = _userManager.GetUserById(userId); - - var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); - - // Get the user data for this item - var data = _userDataRepository.GetUserData(user, item); - - data.Likes = likes; - - _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); - - return _userDataRepository.GetUserDataDto(item, user); - } - - /// - /// Gets an item's lyrics. - /// - /// User id. - /// Item id. - /// Lyrics returned. - /// Something went wrong. No Lyrics will be returned. - /// An containing the item's lyrics. - [HttpGet("Users/{userId}/Items/{itemId}/Lyrics")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetLyrics([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) - { - var user = _userManager.GetUserById(userId); - - if (user is null) - { - return NotFound(); - } - - var item = itemId.Equals(default) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.GetItemById(itemId); - - if (item is null) - { - return NotFound(); - } - - var result = await _lyricManager.GetLyrics(item).ConfigureAwait(false); - if (result is not null) - { - return Ok(result); - } - - return NotFound(); } } + + /// + /// Marks the favorite. + /// + /// The user id. + /// The item id. + /// if set to true [is favorite]. + private UserItemDataDto MarkFavorite(Guid userId, Guid itemId, bool isFavorite) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + + // Get the user data for this item + var data = _userDataRepository.GetUserData(user, item); + + // Set favorite status + data.IsFavorite = isFavorite; + + _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); + + return _userDataRepository.GetUserDataDto(item, user); + } + + /// + /// Updates the user item rating. + /// + /// The user id. + /// The item id. + /// if set to true [likes]. + private UserItemDataDto UpdateUserItemRatingInternal(Guid userId, Guid itemId, bool? likes) + { + var user = _userManager.GetUserById(userId); + + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + + // Get the user data for this item + var data = _userDataRepository.GetUserData(user, item); + + data.Likes = likes; + + _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None); + + return _userDataRepository.GetUserDataDto(item, user); + } + + /// + /// Gets an item's lyrics. + /// + /// User id. + /// Item id. + /// Lyrics returned. + /// Something went wrong. No Lyrics will be returned. + /// An containing the item's lyrics. + [HttpGet("Users/{userId}/Items/{itemId}/Lyrics")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetLyrics([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) + { + var user = _userManager.GetUserById(userId); + + if (user is null) + { + return NotFound(); + } + + var item = itemId.Equals(default) + ? _libraryManager.GetUserRootFolder() + : _libraryManager.GetItemById(itemId); + + if (item is null) + { + return NotFound(); + } + + var result = await _lyricManager.GetLyrics(item).ConfigureAwait(false); + if (result is not null) + { + return Ok(result); + } + + return NotFound(); + } } diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 3aeb444dfa..aa7ba8891b 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -17,122 +17,121 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// User views controller. +/// +[Route("")] +[Authorize(Policy = Policies.DefaultAuthorization)] +public class UserViewsController : BaseJellyfinApiController { + private readonly IUserManager _userManager; + private readonly IUserViewManager _userViewManager; + private readonly IDtoService _dtoService; + private readonly ILibraryManager _libraryManager; + /// - /// User views controller. + /// Initializes a new instance of the class. /// - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class UserViewsController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public UserViewsController( + IUserManager userManager, + IUserViewManager userViewManager, + IDtoService dtoService, + ILibraryManager libraryManager) { - private readonly IUserManager _userManager; - private readonly IUserViewManager _userViewManager; - private readonly IDtoService _dtoService; - private readonly ILibraryManager _libraryManager; + _userManager = userManager; + _userViewManager = userViewManager; + _dtoService = dtoService; + _libraryManager = libraryManager; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public UserViewsController( - IUserManager userManager, - IUserViewManager userViewManager, - IDtoService dtoService, - ILibraryManager libraryManager) + /// + /// Get user views. + /// + /// User id. + /// Whether or not to include external views such as channels or live tv. + /// Preset views. + /// Whether or not to include hidden content. + /// User views returned. + /// An containing the user views. + [HttpGet("Users/{userId}/Views")] + [ProducesResponseType(StatusCodes.Status200OK)] + public QueryResult GetUserViews( + [FromRoute, Required] Guid userId, + [FromQuery] bool? includeExternalContent, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews, + [FromQuery] bool includeHidden = false) + { + var query = new UserViewQuery { - _userManager = userManager; - _userViewManager = userViewManager; - _dtoService = dtoService; - _libraryManager = libraryManager; + UserId = userId, + IncludeHidden = includeHidden + }; + + if (includeExternalContent.HasValue) + { + query.IncludeExternalContent = includeExternalContent.Value; } - /// - /// Get user views. - /// - /// User id. - /// Whether or not to include external views such as channels or live tv. - /// Preset views. - /// Whether or not to include hidden content. - /// User views returned. - /// An containing the user views. - [HttpGet("Users/{userId}/Views")] - [ProducesResponseType(StatusCodes.Status200OK)] - public QueryResult GetUserViews( - [FromRoute, Required] Guid userId, - [FromQuery] bool? includeExternalContent, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews, - [FromQuery] bool includeHidden = false) + if (presetViews.Length != 0) { - var query = new UserViewQuery - { - UserId = userId, - IncludeHidden = includeHidden - }; - - if (includeExternalContent.HasValue) - { - query.IncludeExternalContent = includeExternalContent.Value; - } - - if (presetViews.Length != 0) - { - query.PresetViews = presetViews; - } - - var folders = _userViewManager.GetUserViews(query); - - var dtoOptions = new DtoOptions().AddClientFields(User); - var fields = dtoOptions.Fields.ToList(); - - fields.Add(ItemFields.PrimaryImageAspectRatio); - fields.Add(ItemFields.DisplayPreferencesId); - fields.Remove(ItemFields.BasicSyncInfo); - dtoOptions.Fields = fields.ToArray(); - - var user = _userManager.GetUserById(userId); - - var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)) - .ToArray(); - - return new QueryResult(dtos); + query.PresetViews = presetViews; } - /// - /// Get user view grouping options. - /// - /// User id. - /// User view grouping options returned. - /// User not found. - /// - /// An containing the user view grouping options - /// or a if user not found. - /// - [HttpGet("Users/{userId}/GroupingOptions")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult> GetGroupingOptions([FromRoute, Required] Guid userId) - { - var user = _userManager.GetUserById(userId); - if (user is null) - { - return NotFound(); - } + var folders = _userViewManager.GetUserViews(query); - return Ok(_libraryManager.GetUserRootFolder() - .GetChildren(user, true) - .OfType() - .Where(UserView.IsEligibleForGrouping) - .Select(i => new SpecialViewOptionDto - { - Name = i.Name, - Id = i.Id.ToString("N", CultureInfo.InvariantCulture) - }) - .OrderBy(i => i.Name) - .AsEnumerable()); + var dtoOptions = new DtoOptions().AddClientFields(User); + var fields = dtoOptions.Fields.ToList(); + + fields.Add(ItemFields.PrimaryImageAspectRatio); + fields.Add(ItemFields.DisplayPreferencesId); + fields.Remove(ItemFields.BasicSyncInfo); + dtoOptions.Fields = fields.ToArray(); + + var user = _userManager.GetUserById(userId); + + var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)) + .ToArray(); + + return new QueryResult(dtos); + } + + /// + /// Get user view grouping options. + /// + /// User id. + /// User view grouping options returned. + /// User not found. + /// + /// An containing the user view grouping options + /// or a if user not found. + /// + [HttpGet("Users/{userId}/GroupingOptions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetGroupingOptions([FromRoute, Required] Guid userId) + { + var user = _userManager.GetUserById(userId); + if (user is null) + { + return NotFound(); } + + return Ok(_libraryManager.GetUserRootFolder() + .GetChildren(user, true) + .OfType() + .Where(UserView.IsEligibleForGrouping) + .Select(i => new SpecialViewOptionDto + { + Name = i.Name, + Id = i.Id.ToString("N", CultureInfo.InvariantCulture) + }) + .OrderBy(i => i.Name) + .AsEnumerable()); } } diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index bb31626142..23b9ba46f6 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -10,73 +10,72 @@ using MediaBrowser.Controller.MediaEncoding; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// Attachments controller. +/// +[Route("Videos")] +public class VideoAttachmentsController : BaseJellyfinApiController { + private readonly ILibraryManager _libraryManager; + private readonly IAttachmentExtractor _attachmentExtractor; + /// - /// Attachments controller. + /// Initializes a new instance of the class. /// - [Route("Videos")] - public class VideoAttachmentsController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + public VideoAttachmentsController( + ILibraryManager libraryManager, + IAttachmentExtractor attachmentExtractor) { - private readonly ILibraryManager _libraryManager; - private readonly IAttachmentExtractor _attachmentExtractor; + _libraryManager = libraryManager; + _attachmentExtractor = attachmentExtractor; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public VideoAttachmentsController( - ILibraryManager libraryManager, - IAttachmentExtractor attachmentExtractor) + /// + /// Get video attachment. + /// + /// Video ID. + /// Media Source ID. + /// Attachment Index. + /// Attachment retrieved. + /// Video or attachment not found. + /// An containing the attachment stream on success, or a if the attachment could not be found. + [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")] + [ProducesFile(MediaTypeNames.Application.Octet)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetAttachment( + [FromRoute, Required] Guid videoId, + [FromRoute, Required] string mediaSourceId, + [FromRoute, Required] int index) + { + try { - _libraryManager = libraryManager; - _attachmentExtractor = attachmentExtractor; + var item = _libraryManager.GetItemById(videoId); + if (item is null) + { + return NotFound(); + } + + var (attachment, stream) = await _attachmentExtractor.GetAttachment( + item, + mediaSourceId, + index, + CancellationToken.None) + .ConfigureAwait(false); + + var contentType = string.IsNullOrWhiteSpace(attachment.MimeType) + ? MediaTypeNames.Application.Octet + : attachment.MimeType; + + return new FileStreamResult(stream, contentType); } - - /// - /// Get video attachment. - /// - /// Video ID. - /// Media Source ID. - /// Attachment Index. - /// Attachment retrieved. - /// Video or attachment not found. - /// An containing the attachment stream on success, or a if the attachment could not be found. - [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")] - [ProducesFile(MediaTypeNames.Application.Octet)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetAttachment( - [FromRoute, Required] Guid videoId, - [FromRoute, Required] string mediaSourceId, - [FromRoute, Required] int index) + catch (ResourceNotFoundException e) { - try - { - var item = _libraryManager.GetItemById(videoId); - if (item is null) - { - return NotFound(); - } - - var (attachment, stream) = await _attachmentExtractor.GetAttachment( - item, - mediaSourceId, - index, - CancellationToken.None) - .ConfigureAwait(false); - - var contentType = string.IsNullOrWhiteSpace(attachment.MimeType) - ? MediaTypeNames.Application.Octet - : attachment.MimeType; - - return new FileStreamResult(stream, contentType); - } - catch (ResourceNotFoundException e) - { - return NotFound(e.Message); - } + return NotFound(e.Message); } } } diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 64d8fb498b..36bfc0dd16 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -32,644 +32,643 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Jellyfin.Api.Controllers +namespace Jellyfin.Api.Controllers; + +/// +/// The videos controller. +/// +public class VideosController : BaseJellyfinApiController { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; + private readonly IDlnaManager _dlnaManager; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IDeviceManager _deviceManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly IHttpClientFactory _httpClientFactory; + private readonly EncodingHelper _encodingHelper; + + private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; + /// - /// The videos controller. + /// Initializes a new instance of the class. /// - public class VideosController : BaseJellyfinApiController + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the class. + /// Instance of the interface. + /// Instance of . + public VideosController( + ILibraryManager libraryManager, + IUserManager userManager, + IDtoService dtoService, + IDlnaManager dlnaManager, + IMediaSourceManager mediaSourceManager, + IServerConfigurationManager serverConfigurationManager, + IMediaEncoder mediaEncoder, + IDeviceManager deviceManager, + TranscodingJobHelper transcodingJobHelper, + IHttpClientFactory httpClientFactory, + EncodingHelper encodingHelper) { - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDtoService _dtoService; - private readonly IDlnaManager _dlnaManager; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IDeviceManager _deviceManager; - private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly IHttpClientFactory _httpClientFactory; - private readonly EncodingHelper _encodingHelper; + _libraryManager = libraryManager; + _userManager = userManager; + _dtoService = dtoService; + _dlnaManager = dlnaManager; + _mediaSourceManager = mediaSourceManager; + _serverConfigurationManager = serverConfigurationManager; + _mediaEncoder = mediaEncoder; + _deviceManager = deviceManager; + _transcodingJobHelper = transcodingJobHelper; + _httpClientFactory = httpClientFactory; + _encodingHelper = encodingHelper; + } - private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; + /// + /// Gets additional parts for a video. + /// + /// The item id. + /// Optional. Filter by user id, and attach user data. + /// Additional parts returned. + /// A with the parts. + [HttpGet("{itemId}/AdditionalParts")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) + { + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the class. - /// Instance of the interface. - /// Instance of . - public VideosController( - ILibraryManager libraryManager, - IUserManager userManager, - IDtoService dtoService, - IDlnaManager dlnaManager, - IMediaSourceManager mediaSourceManager, - IServerConfigurationManager serverConfigurationManager, - IMediaEncoder mediaEncoder, - IDeviceManager deviceManager, - TranscodingJobHelper transcodingJobHelper, - IHttpClientFactory httpClientFactory, - EncodingHelper encodingHelper) + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) + : _libraryManager.GetItemById(itemId); + + var dtoOptions = new DtoOptions(); + dtoOptions = dtoOptions.AddClientFields(User); + + BaseItemDto[] items; + if (item is Video video) { - _libraryManager = libraryManager; - _userManager = userManager; - _dtoService = dtoService; - _dlnaManager = dlnaManager; - _mediaSourceManager = mediaSourceManager; - _serverConfigurationManager = serverConfigurationManager; - _mediaEncoder = mediaEncoder; - _deviceManager = deviceManager; - _transcodingJobHelper = transcodingJobHelper; - _httpClientFactory = httpClientFactory; - _encodingHelper = encodingHelper; + items = video.GetAdditionalParts() + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video)) + .ToArray(); + } + else + { + items = Array.Empty(); } - /// - /// Gets additional parts for a video. - /// - /// The item id. - /// Optional. Filter by user id, and attach user data. - /// Additional parts returned. - /// A with the parts. - [HttpGet("{itemId}/AdditionalParts")] - [Authorize(Policy = Policies.DefaultAuthorization)] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) + var result = new QueryResult(items); + return result; + } + + /// + /// Removes alternate video sources. + /// + /// The item id. + /// Alternate sources deleted. + /// Video not found. + /// A indicating success, or a if the video doesn't exist. + [HttpDelete("{itemId}/AlternateSources")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteAlternateSources([FromRoute, Required] Guid itemId) + { + var video = (Video)_libraryManager.GetItemById(itemId); + + if (video is null) { - var user = userId is null || userId.Value.Equals(default) - ? null - : _userManager.GetUserById(userId.Value); - - var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) - ? _libraryManager.RootFolder - : _libraryManager.GetUserRootFolder()) - : _libraryManager.GetItemById(itemId); - - var dtoOptions = new DtoOptions(); - dtoOptions = dtoOptions.AddClientFields(User); - - BaseItemDto[] items; - if (item is Video video) - { - items = video.GetAdditionalParts() - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video)) - .ToArray(); - } - else - { - items = Array.Empty(); - } - - var result = new QueryResult(items); - return result; + return NotFound("The video either does not exist or the id does not belong to a video."); } - /// - /// Removes alternate video sources. - /// - /// The item id. - /// Alternate sources deleted. - /// Video not found. - /// A indicating success, or a if the video doesn't exist. - [HttpDelete("{itemId}/AlternateSources")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteAlternateSources([FromRoute, Required] Guid itemId) + if (video.LinkedAlternateVersions.Length == 0) { - var video = (Video)_libraryManager.GetItemById(itemId); - - if (video is null) - { - return NotFound("The video either does not exist or the id does not belong to a video."); - } - - if (video.LinkedAlternateVersions.Length == 0) - { - video = (Video)_libraryManager.GetItemById(video.PrimaryVersionId); - } - - foreach (var link in video.GetLinkedAlternateVersions()) - { - link.SetPrimaryVersionId(null); - link.LinkedAlternateVersions = Array.Empty(); - - await link.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - } - - video.LinkedAlternateVersions = Array.Empty(); - video.SetPrimaryVersionId(null); - await video.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - - return NoContent(); + video = (Video)_libraryManager.GetItemById(video.PrimaryVersionId); } - /// - /// Merges videos into a single record. - /// - /// Item id list. This allows multiple, comma delimited. - /// Videos merged. - /// Supply at least 2 video ids. - /// A indicating success, or a if less than two ids were supplied. - [HttpPost("MergeVersions")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) + foreach (var link in video.GetLinkedAlternateVersions()) { - var items = ids - .Select(i => _libraryManager.GetItemById(i)) - .OfType