mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-10-31 02:27:18 -04:00 
			
		
		
		
	Migrate to file-scoped namespaces
This commit is contained in:
		
							parent
							
								
									58b3945805
								
							
						
					
					
						commit
						f5f890e685
					
				| @ -2,29 +2,28 @@ | ||||
| 
 | ||||
| using System; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Attributes | ||||
| namespace Jellyfin.Api.Attributes; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Internal produces image attribute. | ||||
| /// </summary> | ||||
| [AttributeUsage(AttributeTargets.Method)] | ||||
| public class AcceptsFileAttribute : Attribute | ||||
| { | ||||
|     private readonly string[] _contentTypes; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Internal produces image attribute. | ||||
|     /// Initializes a new instance of the <see cref="AcceptsFileAttribute"/> class. | ||||
|     /// </summary> | ||||
|     [AttributeUsage(AttributeTargets.Method)] | ||||
|     public class AcceptsFileAttribute : Attribute | ||||
|     /// <param name="contentTypes">Content types this endpoint produces.</param> | ||||
|     public AcceptsFileAttribute(params string[] contentTypes) | ||||
|     { | ||||
|         private readonly string[] _contentTypes; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="AcceptsFileAttribute"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="contentTypes">Content types this endpoint produces.</param> | ||||
|         public AcceptsFileAttribute(params string[] contentTypes) | ||||
|         { | ||||
|             _contentTypes = contentTypes; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the configured content types. | ||||
|         /// </summary> | ||||
|         /// <returns>the configured content types.</returns> | ||||
|         public string[] ContentTypes => _contentTypes; | ||||
|         _contentTypes = contentTypes; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets the configured content types. | ||||
|     /// </summary> | ||||
|     /// <returns>the configured content types.</returns> | ||||
|     public string[] ContentTypes => _contentTypes; | ||||
| } | ||||
|  | ||||
| @ -1,18 +1,17 @@ | ||||
| namespace Jellyfin.Api.Attributes | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Produces file attribute of "image/*". | ||||
|     /// </summary> | ||||
|     public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute | ||||
|     { | ||||
|         private const string ContentType = "image/*"; | ||||
| namespace Jellyfin.Api.Attributes; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class. | ||||
|         /// </summary> | ||||
|         public AcceptsImageFileAttribute() | ||||
|             : base(ContentType) | ||||
|         { | ||||
|         } | ||||
| /// <summary> | ||||
| /// Produces file attribute of "image/*". | ||||
| /// </summary> | ||||
| public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute | ||||
| { | ||||
|     private const string ContentType = "image/*"; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class. | ||||
|     /// </summary> | ||||
|     public AcceptsImageFileAttribute() | ||||
|         : base(ContentType) | ||||
|     { | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -2,29 +2,28 @@ using System; | ||||
| using System.Collections.Generic; | ||||
| using Microsoft.AspNetCore.Mvc.Routing; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Attributes | ||||
| namespace Jellyfin.Api.Attributes; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Identifies an action that supports the HTTP GET method. | ||||
| /// </summary> | ||||
| public sealed class HttpSubscribeAttribute : HttpMethodAttribute | ||||
| { | ||||
|     private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" }; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Identifies an action that supports the HTTP GET method. | ||||
|     /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class. | ||||
|     /// </summary> | ||||
|     public sealed class HttpSubscribeAttribute : HttpMethodAttribute | ||||
|     public HttpSubscribeAttribute() | ||||
|         : base(_supportedMethods) | ||||
|     { | ||||
|         private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" }; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class. | ||||
|         /// </summary> | ||||
|         public HttpSubscribeAttribute() | ||||
|             : base(_supportedMethods) | ||||
|         { | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="template">The route template. May not be null.</param> | ||||
|         public HttpSubscribeAttribute(string template) | ||||
|             : base(_supportedMethods, template) | ||||
|             => ArgumentNullException.ThrowIfNull(template); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class. | ||||
|     /// </summary> | ||||
|     /// <param name="template">The route template. May not be null.</param> | ||||
|     public HttpSubscribeAttribute(string template) | ||||
|         : base(_supportedMethods, template) | ||||
|         => ArgumentNullException.ThrowIfNull(template); | ||||
| } | ||||
|  | ||||
| @ -2,29 +2,28 @@ using System; | ||||
| using System.Collections.Generic; | ||||
| using Microsoft.AspNetCore.Mvc.Routing; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Attributes | ||||
| namespace Jellyfin.Api.Attributes; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Identifies an action that supports the HTTP GET method. | ||||
| /// </summary> | ||||
| public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute | ||||
| { | ||||
|     private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" }; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Identifies an action that supports the HTTP GET method. | ||||
|     /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class. | ||||
|     /// </summary> | ||||
|     public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute | ||||
|     public HttpUnsubscribeAttribute() | ||||
|         : base(_supportedMethods) | ||||
|     { | ||||
|         private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" }; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class. | ||||
|         /// </summary> | ||||
|         public HttpUnsubscribeAttribute() | ||||
|             : base(_supportedMethods) | ||||
|         { | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="template">The route template. May not be null.</param> | ||||
|         public HttpUnsubscribeAttribute(string template) | ||||
|             : base(_supportedMethods, template) | ||||
|             => ArgumentNullException.ThrowIfNull(template); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class. | ||||
|     /// </summary> | ||||
|     /// <param name="template">The route template. May not be null.</param> | ||||
|     public HttpUnsubscribeAttribute(string template) | ||||
|         : base(_supportedMethods, template) | ||||
|         => ArgumentNullException.ThrowIfNull(template); | ||||
| } | ||||
|  | ||||
| @ -1,12 +1,11 @@ | ||||
| using System; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Attributes | ||||
| namespace Jellyfin.Api.Attributes; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Attribute to mark a parameter as obsolete. | ||||
| /// </summary> | ||||
| [AttributeUsage(AttributeTargets.Parameter)] | ||||
| public sealed class ParameterObsoleteAttribute : Attribute | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Attribute to mark a parameter as obsolete. | ||||
|     /// </summary> | ||||
|     [AttributeUsage(AttributeTargets.Parameter)] | ||||
|     public sealed class ParameterObsoleteAttribute : Attribute | ||||
|     { | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,18 +1,17 @@ | ||||
| namespace Jellyfin.Api.Attributes | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Produces file attribute of "image/*". | ||||
|     /// </summary> | ||||
|     public sealed class ProducesAudioFileAttribute : ProducesFileAttribute | ||||
|     { | ||||
|         private const string ContentType = "audio/*"; | ||||
| namespace Jellyfin.Api.Attributes; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ProducesAudioFileAttribute"/> class. | ||||
|         /// </summary> | ||||
|         public ProducesAudioFileAttribute() | ||||
|             : base(ContentType) | ||||
|         { | ||||
|         } | ||||
| /// <summary> | ||||
| /// Produces file attribute of "image/*". | ||||
| /// </summary> | ||||
| public sealed class ProducesAudioFileAttribute : ProducesFileAttribute | ||||
| { | ||||
|     private const string ContentType = "audio/*"; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initializes a new instance of the <see cref="ProducesAudioFileAttribute"/> class. | ||||
|     /// </summary> | ||||
|     public ProducesAudioFileAttribute() | ||||
|         : base(ContentType) | ||||
|     { | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -2,29 +2,28 @@ | ||||
| 
 | ||||
| using System; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Attributes | ||||
| namespace Jellyfin.Api.Attributes; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Internal produces image attribute. | ||||
| /// </summary> | ||||
| [AttributeUsage(AttributeTargets.Method)] | ||||
| public class ProducesFileAttribute : Attribute | ||||
| { | ||||
|     private readonly string[] _contentTypes; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Internal produces image attribute. | ||||
|     /// Initializes a new instance of the <see cref="ProducesFileAttribute"/> class. | ||||
|     /// </summary> | ||||
|     [AttributeUsage(AttributeTargets.Method)] | ||||
|     public class ProducesFileAttribute : Attribute | ||||
|     /// <param name="contentTypes">Content types this endpoint produces.</param> | ||||
|     public ProducesFileAttribute(params string[] contentTypes) | ||||
|     { | ||||
|         private readonly string[] _contentTypes; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ProducesFileAttribute"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="contentTypes">Content types this endpoint produces.</param> | ||||
|         public ProducesFileAttribute(params string[] contentTypes) | ||||
|         { | ||||
|             _contentTypes = contentTypes; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the configured content types. | ||||
|         /// </summary> | ||||
|         /// <returns>the configured content types.</returns> | ||||
|         public string[] ContentTypes => _contentTypes; | ||||
|         _contentTypes = contentTypes; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets the configured content types. | ||||
|     /// </summary> | ||||
|     /// <returns>the configured content types.</returns> | ||||
|     public string[] ContentTypes => _contentTypes; | ||||
| } | ||||
|  | ||||
| @ -1,18 +1,17 @@ | ||||
| namespace Jellyfin.Api.Attributes | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Produces file attribute of "image/*". | ||||
|     /// </summary> | ||||
|     public sealed class ProducesImageFileAttribute : ProducesFileAttribute | ||||
|     { | ||||
|         private const string ContentType = "image/*"; | ||||
| namespace Jellyfin.Api.Attributes; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ProducesImageFileAttribute"/> class. | ||||
|         /// </summary> | ||||
|         public ProducesImageFileAttribute() | ||||
|             : base(ContentType) | ||||
|         { | ||||
|         } | ||||
| /// <summary> | ||||
| /// Produces file attribute of "image/*". | ||||
| /// </summary> | ||||
| public sealed class ProducesImageFileAttribute : ProducesFileAttribute | ||||
| { | ||||
|     private const string ContentType = "image/*"; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initializes a new instance of the <see cref="ProducesImageFileAttribute"/> class. | ||||
|     /// </summary> | ||||
|     public ProducesImageFileAttribute() | ||||
|         : base(ContentType) | ||||
|     { | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,18 +1,17 @@ | ||||
| namespace Jellyfin.Api.Attributes | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Produces file attribute of "image/*". | ||||
|     /// </summary> | ||||
|     public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute | ||||
|     { | ||||
|         private const string ContentType = "application/x-mpegURL"; | ||||
| namespace Jellyfin.Api.Attributes; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ProducesPlaylistFileAttribute"/> class. | ||||
|         /// </summary> | ||||
|         public ProducesPlaylistFileAttribute() | ||||
|             : base(ContentType) | ||||
|         { | ||||
|         } | ||||
| /// <summary> | ||||
| /// Produces file attribute of "image/*". | ||||
| /// </summary> | ||||
| public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute | ||||
| { | ||||
|     private const string ContentType = "application/x-mpegURL"; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initializes a new instance of the <see cref="ProducesPlaylistFileAttribute"/> class. | ||||
|     /// </summary> | ||||
|     public ProducesPlaylistFileAttribute() | ||||
|         : base(ContentType) | ||||
|     { | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,18 +1,17 @@ | ||||
| namespace Jellyfin.Api.Attributes | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Produces file attribute of "video/*". | ||||
|     /// </summary> | ||||
|     public sealed class ProducesVideoFileAttribute : ProducesFileAttribute | ||||
|     { | ||||
|         private const string ContentType = "video/*"; | ||||
| namespace Jellyfin.Api.Attributes; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ProducesVideoFileAttribute"/> class. | ||||
|         /// </summary> | ||||
|         public ProducesVideoFileAttribute() | ||||
|             : base(ContentType) | ||||
|         { | ||||
|         } | ||||
| /// <summary> | ||||
| /// Produces file attribute of "video/*". | ||||
| /// </summary> | ||||
| public sealed class ProducesVideoFileAttribute : ProducesFileAttribute | ||||
| { | ||||
|     private const string ContentType = "video/*"; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initializes a new instance of the <see cref="ProducesVideoFileAttribute"/> class. | ||||
|     /// </summary> | ||||
|     public ProducesVideoFileAttribute() | ||||
|         : base(ContentType) | ||||
|     { | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -4,35 +4,34 @@ using Jellyfin.Api.Results; | ||||
| using Jellyfin.Extensions.Json; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api | ||||
| namespace Jellyfin.Api; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Base api controller for the API setting a default route. | ||||
| /// </summary> | ||||
| [ApiController] | ||||
| [Route("[controller]")]
 | ||||
| [Produces( | ||||
|     MediaTypeNames.Application.Json, | ||||
|     JsonDefaults.CamelCaseMediaType, | ||||
|     JsonDefaults.PascalCaseMediaType)] | ||||
| public class BaseJellyfinApiController : ControllerBase | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Base api controller for the API setting a default route. | ||||
|     /// Create a new <see cref="OkResult{T}"/>. | ||||
|     /// </summary> | ||||
|     [ApiController] | ||||
|     [Route("[controller]")]
 | ||||
|     [Produces( | ||||
|         MediaTypeNames.Application.Json, | ||||
|         JsonDefaults.CamelCaseMediaType, | ||||
|         JsonDefaults.PascalCaseMediaType)] | ||||
|     public class BaseJellyfinApiController : ControllerBase | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Create a new <see cref="OkResult{T}"/>. | ||||
|         /// </summary> | ||||
|         /// <param name="value">The value to return.</param> | ||||
|         /// <typeparam name="T">The type to return.</typeparam> | ||||
|         /// <returns>The <see cref="ActionResult{T}"/>.</returns> | ||||
|         protected ActionResult<IEnumerable<T>> Ok<T>(IEnumerable<T>? value) | ||||
|             => new OkResult<IEnumerable<T>?>(value); | ||||
|     /// <param name="value">The value to return.</param> | ||||
|     /// <typeparam name="T">The type to return.</typeparam> | ||||
|     /// <returns>The <see cref="ActionResult{T}"/>.</returns> | ||||
|     protected ActionResult<IEnumerable<T>> Ok<T>(IEnumerable<T>? value) | ||||
|         => new OkResult<IEnumerable<T>?>(value); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Create a new <see cref="OkResult{T}"/>. | ||||
|         /// </summary> | ||||
|         /// <param name="value">The value to return.</param> | ||||
|         /// <typeparam name="T">The type to return.</typeparam> | ||||
|         /// <returns>The <see cref="ActionResult{T}"/>.</returns> | ||||
|         protected ActionResult<T> Ok<T>(T value) | ||||
|             => new OkResult<T>(value); | ||||
|     } | ||||
|     /// <summary> | ||||
|     /// Create a new <see cref="OkResult{T}"/>. | ||||
|     /// </summary> | ||||
|     /// <param name="value">The value to return.</param> | ||||
|     /// <typeparam name="T">The type to return.</typeparam> | ||||
|     /// <returns>The <see cref="ActionResult{T}"/>.</returns> | ||||
|     protected ActionResult<T> Ok<T>(T value) | ||||
|         => new OkResult<T>(value); | ||||
| } | ||||
|  | ||||
| @ -1,13 +1,12 @@ | ||||
| namespace Jellyfin.Api.Constants | ||||
| namespace Jellyfin.Api.Constants; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Authentication schemes for user authentication in the API. | ||||
| /// </summary> | ||||
| public static class AuthenticationSchemes | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Authentication schemes for user authentication in the API. | ||||
|     /// Scheme name for the custom legacy authentication. | ||||
|     /// </summary> | ||||
|     public static class AuthenticationSchemes | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Scheme name for the custom legacy authentication. | ||||
|         /// </summary> | ||||
|         public const string CustomAuthentication = "CustomAuthentication"; | ||||
|     } | ||||
|     public const string CustomAuthentication = "CustomAuthentication"; | ||||
| } | ||||
|  | ||||
| @ -1,43 +1,42 @@ | ||||
| namespace Jellyfin.Api.Constants | ||||
| namespace Jellyfin.Api.Constants; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Internal claim types for authorization. | ||||
| /// </summary> | ||||
| public static class InternalClaimTypes | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Internal claim types for authorization. | ||||
|     /// User Id. | ||||
|     /// </summary> | ||||
|     public static class InternalClaimTypes | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// User Id. | ||||
|         /// </summary> | ||||
|         public const string UserId = "Jellyfin-UserId"; | ||||
|     public const string UserId = "Jellyfin-UserId"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Device Id. | ||||
|         /// </summary> | ||||
|         public const string DeviceId = "Jellyfin-DeviceId"; | ||||
|     /// <summary> | ||||
|     /// Device Id. | ||||
|     /// </summary> | ||||
|     public const string DeviceId = "Jellyfin-DeviceId"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Device. | ||||
|         /// </summary> | ||||
|         public const string Device = "Jellyfin-Device"; | ||||
|     /// <summary> | ||||
|     /// Device. | ||||
|     /// </summary> | ||||
|     public const string Device = "Jellyfin-Device"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Client. | ||||
|         /// </summary> | ||||
|         public const string Client = "Jellyfin-Client"; | ||||
|     /// <summary> | ||||
|     /// Client. | ||||
|     /// </summary> | ||||
|     public const string Client = "Jellyfin-Client"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Version. | ||||
|         /// </summary> | ||||
|         public const string Version = "Jellyfin-Version"; | ||||
|     /// <summary> | ||||
|     /// Version. | ||||
|     /// </summary> | ||||
|     public const string Version = "Jellyfin-Version"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Token. | ||||
|         /// </summary> | ||||
|         public const string Token = "Jellyfin-Token"; | ||||
|     /// <summary> | ||||
|     /// Token. | ||||
|     /// </summary> | ||||
|     public const string Token = "Jellyfin-Token"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Is Api Key. | ||||
|         /// </summary> | ||||
|         public const string IsApiKey = "Jellyfin-IsApiKey"; | ||||
|     } | ||||
|     /// <summary> | ||||
|     /// Is Api Key. | ||||
|     /// </summary> | ||||
|     public const string IsApiKey = "Jellyfin-IsApiKey"; | ||||
| } | ||||
|  | ||||
| @ -1,78 +1,77 @@ | ||||
| namespace Jellyfin.Api.Constants | ||||
| namespace Jellyfin.Api.Constants; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Policies for the API authorization. | ||||
| /// </summary> | ||||
| public static class Policies | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Policies for the API authorization. | ||||
|     /// Policy name for default authorization. | ||||
|     /// </summary> | ||||
|     public static class Policies | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Policy name for default authorization. | ||||
|         /// </summary> | ||||
|         public const string DefaultAuthorization = "DefaultAuthorization"; | ||||
|     public const string DefaultAuthorization = "DefaultAuthorization"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for requiring first time setup or elevated privileges. | ||||
|         /// </summary> | ||||
|         public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated"; | ||||
|     /// <summary> | ||||
|     /// Policy name for requiring first time setup or elevated privileges. | ||||
|     /// </summary> | ||||
|     public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for requiring elevated privileges. | ||||
|         /// </summary> | ||||
|         public const string RequiresElevation = "RequiresElevation"; | ||||
|     /// <summary> | ||||
|     /// Policy name for requiring elevated privileges. | ||||
|     /// </summary> | ||||
|     public const string RequiresElevation = "RequiresElevation"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for allowing local access only. | ||||
|         /// </summary> | ||||
|         public const string LocalAccessOnly = "LocalAccessOnly"; | ||||
|     /// <summary> | ||||
|     /// Policy name for allowing local access only. | ||||
|     /// </summary> | ||||
|     public const string LocalAccessOnly = "LocalAccessOnly"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for escaping schedule controls. | ||||
|         /// </summary> | ||||
|         public const string IgnoreParentalControl = "IgnoreParentalControl"; | ||||
|     /// <summary> | ||||
|     /// Policy name for escaping schedule controls. | ||||
|     /// </summary> | ||||
|     public const string IgnoreParentalControl = "IgnoreParentalControl"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for requiring download permission. | ||||
|         /// </summary> | ||||
|         public const string Download = "Download"; | ||||
|     /// <summary> | ||||
|     /// Policy name for requiring download permission. | ||||
|     /// </summary> | ||||
|     public const string Download = "Download"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for requiring first time setup or default permissions. | ||||
|         /// </summary> | ||||
|         public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault"; | ||||
|     /// <summary> | ||||
|     /// Policy name for requiring first time setup or default permissions. | ||||
|     /// </summary> | ||||
|     public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for requiring local access or elevated privileges. | ||||
|         /// </summary> | ||||
|         public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation"; | ||||
|     /// <summary> | ||||
|     /// Policy name for requiring local access or elevated privileges. | ||||
|     /// </summary> | ||||
|     public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for requiring (anonymous) LAN access. | ||||
|         /// </summary> | ||||
|         public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy"; | ||||
|     /// <summary> | ||||
|     /// Policy name for requiring (anonymous) LAN access. | ||||
|     /// </summary> | ||||
|     public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for escaping schedule controls or requiring first time setup. | ||||
|         /// </summary> | ||||
|         public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl"; | ||||
|     /// <summary> | ||||
|     /// Policy name for escaping schedule controls or requiring first time setup. | ||||
|     /// </summary> | ||||
|     public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for accessing SyncPlay. | ||||
|         /// </summary> | ||||
|         public const string SyncPlayHasAccess = "SyncPlayHasAccess"; | ||||
|     /// <summary> | ||||
|     /// Policy name for accessing SyncPlay. | ||||
|     /// </summary> | ||||
|     public const string SyncPlayHasAccess = "SyncPlayHasAccess"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for creating a SyncPlay group. | ||||
|         /// </summary> | ||||
|         public const string SyncPlayCreateGroup = "SyncPlayCreateGroup"; | ||||
|     /// <summary> | ||||
|     /// Policy name for creating a SyncPlay group. | ||||
|     /// </summary> | ||||
|     public const string SyncPlayCreateGroup = "SyncPlayCreateGroup"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for joining a SyncPlay group. | ||||
|         /// </summary> | ||||
|         public const string SyncPlayJoinGroup = "SyncPlayJoinGroup"; | ||||
|     /// <summary> | ||||
|     /// Policy name for joining a SyncPlay group. | ||||
|     /// </summary> | ||||
|     public const string SyncPlayJoinGroup = "SyncPlayJoinGroup"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Policy name for accessing a SyncPlay group. | ||||
|         /// </summary> | ||||
|         public const string SyncPlayIsInGroup = "SyncPlayIsInGroup"; | ||||
|     } | ||||
|     /// <summary> | ||||
|     /// Policy name for accessing a SyncPlay group. | ||||
|     /// </summary> | ||||
|     public const string SyncPlayIsInGroup = "SyncPlayIsInGroup"; | ||||
| } | ||||
|  | ||||
| @ -1,23 +1,22 @@ | ||||
| namespace Jellyfin.Api.Constants | ||||
| namespace Jellyfin.Api.Constants; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Constants for user roles used in the authentication and authorization for the API. | ||||
| /// </summary> | ||||
| public static class UserRoles | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Constants for user roles used in the authentication and authorization for the API. | ||||
|     /// Guest user. | ||||
|     /// </summary> | ||||
|     public static class UserRoles | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Guest user. | ||||
|         /// </summary> | ||||
|         public const string Guest = "Guest"; | ||||
|     public const string Guest = "Guest"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Regular user with no special privileges. | ||||
|         /// </summary> | ||||
|         public const string User = "User"; | ||||
|     /// <summary> | ||||
|     /// Regular user with no special privileges. | ||||
|     /// </summary> | ||||
|     public const string User = "User"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Administrator user with elevated privileges. | ||||
|         /// </summary> | ||||
|         public const string Administrator = "Administrator"; | ||||
|     } | ||||
|     /// <summary> | ||||
|     /// Administrator user with elevated privileges. | ||||
|     /// </summary> | ||||
|     public const string Administrator = "Administrator"; | ||||
| } | ||||
|  | ||||
| @ -8,50 +8,49 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Activity log controller. | ||||
| /// </summary> | ||||
| [Route("System/ActivityLog")] | ||||
| [Authorize(Policy = Policies.RequiresElevation)] | ||||
| public class ActivityLogController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IActivityManager _activityManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Activity log controller. | ||||
|     /// Initializes a new instance of the <see cref="ActivityLogController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("System/ActivityLog")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     public class ActivityLogController : BaseJellyfinApiController | ||||
|     /// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param> | ||||
|     public ActivityLogController(IActivityManager activityManager) | ||||
|     { | ||||
|         private readonly IActivityManager _activityManager; | ||||
|         _activityManager = activityManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ActivityLogController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param> | ||||
|         public ActivityLogController(IActivityManager activityManager) | ||||
|     /// <summary> | ||||
|     /// Gets activity log entries. | ||||
|     /// </summary> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="minDate">Optional. The minimum date. Format = ISO.</param> | ||||
|     /// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param> | ||||
|     /// <response code="200">Activity log returned.</response> | ||||
|     /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns> | ||||
|     [HttpGet("Entries")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<QueryResult<ActivityLogEntry>>> GetLogEntries( | ||||
|         [FromQuery] int? startIndex, | ||||
|         [FromQuery] int? limit, | ||||
|         [FromQuery] DateTime? minDate, | ||||
|         [FromQuery] bool? hasUserId) | ||||
|     { | ||||
|         return await _activityManager.GetPagedResultAsync(new ActivityLogQuery | ||||
|         { | ||||
|             _activityManager = activityManager; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets activity log entries. | ||||
|         /// </summary> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="minDate">Optional. The minimum date. Format = ISO.</param> | ||||
|         /// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param> | ||||
|         /// <response code="200">Activity log returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns> | ||||
|         [HttpGet("Entries")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<QueryResult<ActivityLogEntry>>> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -7,70 +7,69 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Authentication controller. | ||||
| /// </summary> | ||||
| [Route("Auth")] | ||||
| public class ApiKeyController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IAuthenticationManager _authenticationManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Authentication controller. | ||||
|     /// Initializes a new instance of the <see cref="ApiKeyController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("Auth")] | ||||
|     public class ApiKeyController : BaseJellyfinApiController | ||||
|     /// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param> | ||||
|     public ApiKeyController(IAuthenticationManager authenticationManager) | ||||
|     { | ||||
|         private readonly IAuthenticationManager _authenticationManager; | ||||
|         _authenticationManager = authenticationManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ApiKeyController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param> | ||||
|         public ApiKeyController(IAuthenticationManager authenticationManager) | ||||
|         { | ||||
|             _authenticationManager = authenticationManager; | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Get all keys. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Api keys retrieved.</response> | ||||
|     /// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns> | ||||
|     [HttpGet("Keys")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys() | ||||
|     { | ||||
|         var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get all keys. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Api keys retrieved.</response> | ||||
|         /// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns> | ||||
|         [HttpGet("Keys")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys() | ||||
|         { | ||||
|             var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false); | ||||
|         return new QueryResult<AuthenticationInfo>(keys); | ||||
|     } | ||||
| 
 | ||||
|             return new QueryResult<AuthenticationInfo>(keys); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Create a new api key. | ||||
|     /// </summary> | ||||
|     /// <param name="app">Name of the app using the authentication key.</param> | ||||
|     /// <response code="204">Api key created.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Keys")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> CreateKey([FromQuery, Required] string app) | ||||
|     { | ||||
|         await _authenticationManager.CreateApiKey(app).ConfigureAwait(false); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Create a new api key. | ||||
|         /// </summary> | ||||
|         /// <param name="app">Name of the app using the authentication key.</param> | ||||
|         /// <response code="204">Api key created.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Keys")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> CreateKey([FromQuery, Required] string app) | ||||
|         { | ||||
|             await _authenticationManager.CreateApiKey(app).ConfigureAwait(false); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Remove an api key. | ||||
|     /// </summary> | ||||
|     /// <param name="key">The access token to delete.</param> | ||||
|     /// <response code="204">Api key deleted.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpDelete("Keys/{key}")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> RevokeKey([FromRoute, Required] string key) | ||||
|     { | ||||
|         await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Remove an api key. | ||||
|         /// </summary> | ||||
|         /// <param name="key">The access token to delete.</param> | ||||
|         /// <response code="204">Api key deleted.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpDelete("Keys/{key}")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> RevokeKey([FromRoute, Required] string key) | ||||
|         { | ||||
|             await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false); | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
|         return NoContent(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -17,464 +17,463 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The artists controller. | ||||
| /// </summary> | ||||
| [Route("Artists")] | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class ArtistsController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly IUserManager _userManager; | ||||
|     private readonly IDtoService _dtoService; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The artists controller. | ||||
|     /// Initializes a new instance of the <see cref="ArtistsController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("Artists")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class ArtistsController : BaseJellyfinApiController | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ArtistsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         public ArtistsController( | ||||
|             ILibraryManager libraryManager, | ||||
|             IUserManager userManager, | ||||
|             IDtoService dtoService) | ||||
|     /// <summary> | ||||
|     /// Gets all artists from a given item, folder, or the entire library. | ||||
|     /// </summary> | ||||
|     /// <param name="minCommunityRating">Optional filter by minimum community rating.</param> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="searchTerm">Optional. Search term.</param> | ||||
|     /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="filters">Optional. Specify additional filters to apply.</param> | ||||
|     /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|     /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|     /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="enableUserData">Optional, include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param> | ||||
|     /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param> | ||||
|     /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param> | ||||
|     /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|     /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|     /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|     /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> | ||||
|     /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> | ||||
|     /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|     /// <param name="enableTotalRecordCount">Total record count.</param> | ||||
|     /// <response code="200">Artists returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the artists.</returns> | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets all artists from a given item, folder, or the entire library. | ||||
|         /// </summary> | ||||
|         /// <param name="minCommunityRating">Optional filter by minimum community rating.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="searchTerm">Optional. Search term.</param> | ||||
|         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="filters">Optional. Specify additional filters to apply.</param> | ||||
|         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|         /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|         /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="enableUserData">Optional, include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param> | ||||
|         /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param> | ||||
|         /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param> | ||||
|         /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|         /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|         /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> | ||||
|         /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> | ||||
|         /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|         /// <param name="enableTotalRecordCount">Total record count.</param> | ||||
|         /// <response code="200">Artists returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the artists.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>( | ||||
|                 query.StartIndex, | ||||
|                 result.TotalRecordCount, | ||||
|                 dtos.ToArray()); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets all album artists from a given item, folder, or the entire library. | ||||
|         /// </summary> | ||||
|         /// <param name="minCommunityRating">Optional filter by minimum community rating.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="searchTerm">Optional. Search term.</param> | ||||
|         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="filters">Optional. Specify additional filters to apply.</param> | ||||
|         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|         /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|         /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="enableUserData">Optional, include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param> | ||||
|         /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param> | ||||
|         /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param> | ||||
|         /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|         /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|         /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> | ||||
|         /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> | ||||
|         /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|         /// <param name="enableTotalRecordCount">Total record count.</param> | ||||
|         /// <response code="200">Album artists returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the album artists.</returns> | ||||
|         [HttpGet("AlbumArtists")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>( | ||||
|                 query.StartIndex, | ||||
|                 result.TotalRecordCount, | ||||
|                 dtos.ToArray()); | ||||
|             }).Where(i => i is not null).Select(i => i!.Id).ToArray(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an artist by name. | ||||
|         /// </summary> | ||||
|         /// <param name="name">Studio name.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <response code="200">Artist returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the artist.</returns> | ||||
|         [HttpGet("{name}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<BaseItemDto> 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<BaseItemDto>( | ||||
|             query.StartIndex, | ||||
|             result.TotalRecordCount, | ||||
|             dtos.ToArray()); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets all album artists from a given item, folder, or the entire library. | ||||
|     /// </summary> | ||||
|     /// <param name="minCommunityRating">Optional filter by minimum community rating.</param> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="searchTerm">Optional. Search term.</param> | ||||
|     /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="filters">Optional. Specify additional filters to apply.</param> | ||||
|     /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|     /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|     /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="enableUserData">Optional, include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param> | ||||
|     /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param> | ||||
|     /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param> | ||||
|     /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|     /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|     /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|     /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> | ||||
|     /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> | ||||
|     /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|     /// <param name="enableTotalRecordCount">Total record count.</param> | ||||
|     /// <response code="200">Album artists returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the album artists.</returns> | ||||
|     [HttpGet("AlbumArtists")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>( | ||||
|             query.StartIndex, | ||||
|             result.TotalRecordCount, | ||||
|             dtos.ToArray()); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets an artist by name. | ||||
|     /// </summary> | ||||
|     /// <param name="name">Studio name.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <response code="200">Artist returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the artist.</returns> | ||||
|     [HttpGet("{name}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<BaseItemDto> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -10,355 +10,354 @@ using MediaBrowser.Model.Dlna; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The audio controller. | ||||
| /// </summary> | ||||
| // 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; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The audio controller. | ||||
|     /// Initializes a new instance of the <see cref="AudioController"/> class. | ||||
|     /// </summary> | ||||
|     // TODO: In order to authenticate this in the future, Dlna playback will require updating | ||||
|     public class AudioController : BaseJellyfinApiController | ||||
|     /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param> | ||||
|     public AudioController(AudioHelper audioHelper) | ||||
|     { | ||||
|         private readonly AudioHelper _audioHelper; | ||||
|         _audioHelper = audioHelper; | ||||
|     } | ||||
| 
 | ||||
|         private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="AudioController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param> | ||||
|         public AudioController(AudioHelper audioHelper) | ||||
|     /// <summary> | ||||
|     /// Gets an audio stream. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="container">The audio container.</param> | ||||
|     /// <param name="static">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.</param> | ||||
|     /// <param name="params">The streaming parameters.</param> | ||||
|     /// <param name="tag">The tag.</param> | ||||
|     /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> | ||||
|     /// <param name="playSessionId">The play session id.</param> | ||||
|     /// <param name="segmentContainer">The segment container.</param> | ||||
|     /// <param name="segmentLength">The segment length.</param> | ||||
|     /// <param name="minSegments">The minimum number of segments.</param> | ||||
|     /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> | ||||
|     /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> | ||||
|     /// <param name="audioCodec">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.</param> | ||||
|     /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> | ||||
|     /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> | ||||
|     /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> | ||||
|     /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> | ||||
|     /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> | ||||
|     /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> | ||||
|     /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> | ||||
|     /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param> | ||||
|     /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param> | ||||
|     /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param> | ||||
|     /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param> | ||||
|     /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> | ||||
|     /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> | ||||
|     /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param> | ||||
|     /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param> | ||||
|     /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param> | ||||
|     /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param> | ||||
|     /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param> | ||||
|     /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param> | ||||
|     /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param> | ||||
|     /// <param name="maxRefFrames">Optional.</param> | ||||
|     /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> | ||||
|     /// <param name="requireAvc">Optional. Whether to require avc.</param> | ||||
|     /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> | ||||
|     /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param> | ||||
|     /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> | ||||
|     /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param> | ||||
|     /// <param name="liveStreamId">The live stream id.</param> | ||||
|     /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> | ||||
|     /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param> | ||||
|     /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> | ||||
|     /// <param name="transcodeReasons">Optional. The transcoding reason.</param> | ||||
|     /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> | ||||
|     /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> | ||||
|     /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> | ||||
|     /// <param name="streamOptions">Optional. The streaming options.</param> | ||||
|     /// <response code="200">Audio stream returned.</response> | ||||
|     /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> | ||||
|     [HttpGet("{itemId}/stream", Name = "GetAudioStream")] | ||||
|     [HttpHead("{itemId}/stream", Name = "HeadAudioStream")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesAudioFile] | ||||
|     public async Task<ActionResult> 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<string, string>? 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 | ||||
|         }; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an audio stream. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="container">The audio container.</param> | ||||
|         /// <param name="static">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.</param> | ||||
|         /// <param name="params">The streaming parameters.</param> | ||||
|         /// <param name="tag">The tag.</param> | ||||
|         /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> | ||||
|         /// <param name="playSessionId">The play session id.</param> | ||||
|         /// <param name="segmentContainer">The segment container.</param> | ||||
|         /// <param name="segmentLength">The segment length.</param> | ||||
|         /// <param name="minSegments">The minimum number of segments.</param> | ||||
|         /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> | ||||
|         /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> | ||||
|         /// <param name="audioCodec">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.</param> | ||||
|         /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> | ||||
|         /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> | ||||
|         /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> | ||||
|         /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> | ||||
|         /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> | ||||
|         /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> | ||||
|         /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> | ||||
|         /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param> | ||||
|         /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param> | ||||
|         /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param> | ||||
|         /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param> | ||||
|         /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> | ||||
|         /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> | ||||
|         /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param> | ||||
|         /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param> | ||||
|         /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param> | ||||
|         /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param> | ||||
|         /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param> | ||||
|         /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param> | ||||
|         /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param> | ||||
|         /// <param name="maxRefFrames">Optional.</param> | ||||
|         /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> | ||||
|         /// <param name="requireAvc">Optional. Whether to require avc.</param> | ||||
|         /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> | ||||
|         /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param> | ||||
|         /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> | ||||
|         /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param> | ||||
|         /// <param name="liveStreamId">The live stream id.</param> | ||||
|         /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> | ||||
|         /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param> | ||||
|         /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> | ||||
|         /// <param name="transcodeReasons">Optional. The transcoding reason.</param> | ||||
|         /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> | ||||
|         /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> | ||||
|         /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> | ||||
|         /// <param name="streamOptions">Optional. The streaming options.</param> | ||||
|         /// <response code="200">Audio stream returned.</response> | ||||
|         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> | ||||
|         [HttpGet("{itemId}/stream", Name = "GetAudioStream")] | ||||
|         [HttpHead("{itemId}/stream", Name = "HeadAudioStream")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesAudioFile] | ||||
|         public async Task<ActionResult> 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<string, string>? streamOptions) | ||||
|         return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets an audio stream. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="container">The audio container.</param> | ||||
|     /// <param name="static">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.</param> | ||||
|     /// <param name="params">The streaming parameters.</param> | ||||
|     /// <param name="tag">The tag.</param> | ||||
|     /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> | ||||
|     /// <param name="playSessionId">The play session id.</param> | ||||
|     /// <param name="segmentContainer">The segment container.</param> | ||||
|     /// <param name="segmentLength">The segment length.</param> | ||||
|     /// <param name="minSegments">The minimum number of segments.</param> | ||||
|     /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> | ||||
|     /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> | ||||
|     /// <param name="audioCodec">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.</param> | ||||
|     /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> | ||||
|     /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> | ||||
|     /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> | ||||
|     /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> | ||||
|     /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> | ||||
|     /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> | ||||
|     /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> | ||||
|     /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param> | ||||
|     /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param> | ||||
|     /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param> | ||||
|     /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param> | ||||
|     /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> | ||||
|     /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> | ||||
|     /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param> | ||||
|     /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param> | ||||
|     /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param> | ||||
|     /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param> | ||||
|     /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param> | ||||
|     /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param> | ||||
|     /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param> | ||||
|     /// <param name="maxRefFrames">Optional.</param> | ||||
|     /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> | ||||
|     /// <param name="requireAvc">Optional. Whether to require avc.</param> | ||||
|     /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> | ||||
|     /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> | ||||
|     /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> | ||||
|     /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param> | ||||
|     /// <param name="liveStreamId">The live stream id.</param> | ||||
|     /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> | ||||
|     /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param> | ||||
|     /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> | ||||
|     /// <param name="transcodeReasons">Optional. The transcoding reason.</param> | ||||
|     /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> | ||||
|     /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> | ||||
|     /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> | ||||
|     /// <param name="streamOptions">Optional. The streaming options.</param> | ||||
|     /// <response code="200">Audio stream returned.</response> | ||||
|     /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> | ||||
|     [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")] | ||||
|     [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesAudioFile] | ||||
|     public async Task<ActionResult> 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<string, string>? 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an audio stream. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="container">The audio container.</param> | ||||
|         /// <param name="static">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.</param> | ||||
|         /// <param name="params">The streaming parameters.</param> | ||||
|         /// <param name="tag">The tag.</param> | ||||
|         /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> | ||||
|         /// <param name="playSessionId">The play session id.</param> | ||||
|         /// <param name="segmentContainer">The segment container.</param> | ||||
|         /// <param name="segmentLength">The segment length.</param> | ||||
|         /// <param name="minSegments">The minimum number of segments.</param> | ||||
|         /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> | ||||
|         /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> | ||||
|         /// <param name="audioCodec">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.</param> | ||||
|         /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> | ||||
|         /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> | ||||
|         /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> | ||||
|         /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> | ||||
|         /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> | ||||
|         /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> | ||||
|         /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> | ||||
|         /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param> | ||||
|         /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param> | ||||
|         /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param> | ||||
|         /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param> | ||||
|         /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> | ||||
|         /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> | ||||
|         /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param> | ||||
|         /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param> | ||||
|         /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param> | ||||
|         /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param> | ||||
|         /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param> | ||||
|         /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param> | ||||
|         /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param> | ||||
|         /// <param name="maxRefFrames">Optional.</param> | ||||
|         /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> | ||||
|         /// <param name="requireAvc">Optional. Whether to require avc.</param> | ||||
|         /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> | ||||
|         /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> | ||||
|         /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> | ||||
|         /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param> | ||||
|         /// <param name="liveStreamId">The live stream id.</param> | ||||
|         /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> | ||||
|         /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param> | ||||
|         /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> | ||||
|         /// <param name="transcodeReasons">Optional. The transcoding reason.</param> | ||||
|         /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> | ||||
|         /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> | ||||
|         /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> | ||||
|         /// <param name="streamOptions">Optional. The streaming options.</param> | ||||
|         /// <response code="200">Audio stream returned.</response> | ||||
|         /// <returns>A <see cref="FileResult"/> containing the audio file.</returns> | ||||
|         [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")] | ||||
|         [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesAudioFile] | ||||
|         public async Task<ActionResult> 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<string, string>? 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -4,54 +4,53 @@ using MediaBrowser.Model.Branding; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Branding controller. | ||||
| /// </summary> | ||||
| public class BrandingController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Branding controller. | ||||
|     /// Initializes a new instance of the <see cref="BrandingController"/> class. | ||||
|     /// </summary> | ||||
|     public class BrandingController : BaseJellyfinApiController | ||||
|     /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     public BrandingController(IServerConfigurationManager serverConfigurationManager) | ||||
|     { | ||||
|         private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
|         _serverConfigurationManager = serverConfigurationManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="BrandingController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         public BrandingController(IServerConfigurationManager serverConfigurationManager) | ||||
|         { | ||||
|             _serverConfigurationManager = serverConfigurationManager; | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Gets branding configuration. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Branding configuration returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns> | ||||
|     [HttpGet("Configuration")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<BrandingOptions> GetBrandingOptions() | ||||
|     { | ||||
|         return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets branding configuration. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Branding configuration returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns> | ||||
|         [HttpGet("Configuration")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<BrandingOptions> GetBrandingOptions() | ||||
|         { | ||||
|             return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets branding css. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Branding css returned.</response> | ||||
|         /// <response code="204">No branding css configured.</response> | ||||
|         /// <returns> | ||||
|         /// An <see cref="OkResult"/> containing the branding css if exist, | ||||
|         /// or a <see cref="NoContentResult"/> if the css is not configured. | ||||
|         /// </returns> | ||||
|         [HttpGet("Css")] | ||||
|         [HttpGet("Css.css", Name = "GetBrandingCss_2")] | ||||
|         [Produces("text/css")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult<string> GetBrandingCss() | ||||
|         { | ||||
|             var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); | ||||
|             return options.CustomCss ?? string.Empty; | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Gets branding css. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Branding css returned.</response> | ||||
|     /// <response code="204">No branding css configured.</response> | ||||
|     /// <returns> | ||||
|     /// An <see cref="OkResult"/> containing the branding css if exist, | ||||
|     /// or a <see cref="NoContentResult"/> if the css is not configured. | ||||
|     /// </returns> | ||||
|     [HttpGet("Css")] | ||||
|     [HttpGet("Css.css", Name = "GetBrandingCss_2")] | ||||
|     [Produces("text/css")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public ActionResult<string> GetBrandingCss() | ||||
|     { | ||||
|         var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); | ||||
|         return options.CustomCss ?? string.Empty; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -18,234 +18,233 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Channels Controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class ChannelsController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IChannelManager _channelManager; | ||||
|     private readonly IUserManager _userManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Channels Controller. | ||||
|     /// Initializes a new instance of the <see cref="ChannelsController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class ChannelsController : BaseJellyfinApiController | ||||
|     /// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     public ChannelsController(IChannelManager channelManager, IUserManager userManager) | ||||
|     { | ||||
|         private readonly IChannelManager _channelManager; | ||||
|         private readonly IUserManager _userManager; | ||||
|         _channelManager = channelManager; | ||||
|         _userManager = userManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ChannelsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         public ChannelsController(IChannelManager channelManager, IUserManager userManager) | ||||
|     /// <summary> | ||||
|     /// Gets available channels. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</param> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param> | ||||
|     /// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param> | ||||
|     /// <param name="isFavorite">Optional. Filter by channels that are favorite.</param> | ||||
|     /// <response code="200">Channels returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the channels.</returns> | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets available channels. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param> | ||||
|         /// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param> | ||||
|         /// <param name="isFavorite">Optional. Filter by channels that are favorite.</param> | ||||
|         /// <response code="200">Channels returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the channels.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> GetChannels( | ||||
|             [FromQuery] Guid? userId, | ||||
|             [FromQuery] int? startIndex, | ||||
|             [FromQuery] int? limit, | ||||
|             [FromQuery] bool? supportsLatestItems, | ||||
|             [FromQuery] bool? supportsMediaDeletion, | ||||
|             [FromQuery] bool? isFavorite) | ||||
|     /// <summary> | ||||
|     /// Get all channel features. | ||||
|     /// </summary> | ||||
|     /// <response code="200">All channel features returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the channel features.</returns> | ||||
|     [HttpGet("Features")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures() | ||||
|     { | ||||
|         return _channelManager.GetAllChannelFeatures(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get channel features. | ||||
|     /// </summary> | ||||
|     /// <param name="channelId">Channel id.</param> | ||||
|     /// <response code="200">Channel features returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the channel features.</returns> | ||||
|     [HttpGet("{channelId}/Features")] | ||||
|     public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] Guid channelId) | ||||
|     { | ||||
|         return _channelManager.GetChannelFeatures(channelId); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get channel items. | ||||
|     /// </summary> | ||||
|     /// <param name="channelId">Channel Id.</param> | ||||
|     /// <param name="folderId">Optional. Folder Id.</param> | ||||
|     /// <param name="userId">Optional. User Id.</param> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param> | ||||
|     /// <param name="filters">Optional. Specify additional filters to apply.</param> | ||||
|     /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <response code="200">Channel items returned.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task"/> representing the request to get the channel items. | ||||
|     /// The task result contains an <see cref="OkResult"/> containing the channel items. | ||||
|     /// </returns> | ||||
|     [HttpGet("{channelId}/Items")] | ||||
|     public async Task<ActionResult<QueryResult<BaseItemDto>>> 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 | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get all channel features. | ||||
|         /// </summary> | ||||
|         /// <response code="200">All channel features returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the channel features.</returns> | ||||
|         [HttpGet("Features")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures() | ||||
|         { | ||||
|             return _channelManager.GetAllChannelFeatures(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get channel features. | ||||
|         /// </summary> | ||||
|         /// <param name="channelId">Channel id.</param> | ||||
|         /// <response code="200">Channel features returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the channel features.</returns> | ||||
|         [HttpGet("{channelId}/Features")] | ||||
|         public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] Guid channelId) | ||||
|         { | ||||
|             return _channelManager.GetChannelFeatures(channelId); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get channel items. | ||||
|         /// </summary> | ||||
|         /// <param name="channelId">Channel Id.</param> | ||||
|         /// <param name="folderId">Optional. Folder Id.</param> | ||||
|         /// <param name="userId">Optional. User Id.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param> | ||||
|         /// <param name="filters">Optional. Specify additional filters to apply.</param> | ||||
|         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <response code="200">Channel items returned.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task"/> representing the request to get the channel items. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the channel items. | ||||
|         /// </returns> | ||||
|         [HttpGet("{channelId}/Items")] | ||||
|         public async Task<ActionResult<QueryResult<BaseItemDto>>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets latest channel items. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">Optional. User Id.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="filters">Optional. Specify additional filters to apply.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param> | ||||
|         /// <response code="200">Latest channel items returned.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task"/> representing the request to get the latest channel items. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the latest channel items. | ||||
|         /// </returns> | ||||
|         [HttpGet("Items/Latest")] | ||||
|         public async Task<ActionResult<QueryResult<BaseItemDto>>> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets latest channel items. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">Optional. User Id.</param> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="filters">Optional. Specify additional filters to apply.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param> | ||||
|     /// <response code="200">Latest channel items returned.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task"/> representing the request to get the latest channel items. | ||||
|     /// The task result contains an <see cref="OkResult"/> containing the latest channel items. | ||||
|     /// </returns> | ||||
|     [HttpGet("Items/Latest")] | ||||
|     public async Task<ActionResult<QueryResult<BaseItemDto>>> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -11,71 +11,70 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Client log controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class ClientLogController : BaseJellyfinApiController | ||||
| { | ||||
|     private const int MaxDocumentSize = 1_000_000; | ||||
|     private readonly IClientEventLogger _clientEventLogger; | ||||
|     private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Client log controller. | ||||
|     /// Initializes a new instance of the <see cref="ClientLogController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class ClientLogController : BaseJellyfinApiController | ||||
|     /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param> | ||||
|     /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     public ClientLogController( | ||||
|         IClientEventLogger clientEventLogger, | ||||
|         IServerConfigurationManager serverConfigurationManager) | ||||
|     { | ||||
|         private const int MaxDocumentSize = 1_000_000; | ||||
|         private readonly IClientEventLogger _clientEventLogger; | ||||
|         private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
|         _clientEventLogger = clientEventLogger; | ||||
|         _serverConfigurationManager = serverConfigurationManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ClientLogController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         public ClientLogController( | ||||
|             IClientEventLogger clientEventLogger, | ||||
|             IServerConfigurationManager serverConfigurationManager) | ||||
|     /// <summary> | ||||
|     /// Upload a document. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Document saved.</response> | ||||
|     /// <response code="403">Event logging disabled.</response> | ||||
|     /// <response code="413">Upload size too large.</response> | ||||
|     /// <returns>Create response.</returns> | ||||
|     [HttpPost("Document")] | ||||
|     [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||
|     [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] | ||||
|     [AcceptsFile(MediaTypeNames.Text.Plain)] | ||||
|     [RequestSizeLimit(MaxDocumentSize)] | ||||
|     public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile() | ||||
|     { | ||||
|         if (!_serverConfigurationManager.Configuration.AllowClientLogUpload) | ||||
|         { | ||||
|             _clientEventLogger = clientEventLogger; | ||||
|             _serverConfigurationManager = serverConfigurationManager; | ||||
|             return Forbid(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Upload a document. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Document saved.</response> | ||||
|         /// <response code="403">Event logging disabled.</response> | ||||
|         /// <response code="413">Upload size too large.</response> | ||||
|         /// <returns>Create response.</returns> | ||||
|         [HttpPost("Document")] | ||||
|         [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||
|         [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] | ||||
|         [AcceptsFile(MediaTypeNames.Text.Plain)] | ||||
|         [RequestSizeLimit(MaxDocumentSize)] | ||||
|         public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile() | ||||
|         if (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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -11,101 +11,100 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The collection controller. | ||||
| /// </summary> | ||||
| [Route("Collections")] | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class CollectionController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ICollectionManager _collectionManager; | ||||
|     private readonly IDtoService _dtoService; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The collection controller. | ||||
|     /// Initializes a new instance of the <see cref="CollectionController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("Collections")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class CollectionController : BaseJellyfinApiController | ||||
|     /// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param> | ||||
|     public CollectionController( | ||||
|         ICollectionManager collectionManager, | ||||
|         IDtoService dtoService) | ||||
|     { | ||||
|         private readonly ICollectionManager _collectionManager; | ||||
|         private readonly IDtoService _dtoService; | ||||
|         _collectionManager = collectionManager; | ||||
|         _dtoService = dtoService; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="CollectionController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param> | ||||
|         public CollectionController( | ||||
|             ICollectionManager collectionManager, | ||||
|             IDtoService dtoService) | ||||
|     /// <summary> | ||||
|     /// Creates a new collection. | ||||
|     /// </summary> | ||||
|     /// <param name="name">The name of the collection.</param> | ||||
|     /// <param name="ids">Item Ids to add to the collection.</param> | ||||
|     /// <param name="parentId">Optional. Create the collection within a specific folder.</param> | ||||
|     /// <param name="isLocked">Whether or not to lock the new collection.</param> | ||||
|     /// <response code="200">Collection created.</response> | ||||
|     /// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns> | ||||
|     [HttpPost] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<CollectionCreationResult>> 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); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates a new collection. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the collection.</param> | ||||
|         /// <param name="ids">Item Ids to add to the collection.</param> | ||||
|         /// <param name="parentId">Optional. Create the collection within a specific folder.</param> | ||||
|         /// <param name="isLocked">Whether or not to lock the new collection.</param> | ||||
|         /// <response code="200">Collection created.</response> | ||||
|         /// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns> | ||||
|         [HttpPost] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<CollectionCreationResult>> 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); | ||||
|     /// <summary> | ||||
|     /// Adds items to a collection. | ||||
|     /// </summary> | ||||
|     /// <param name="collectionId">The collection id.</param> | ||||
|     /// <param name="ids">Item ids, comma delimited.</param> | ||||
|     /// <response code="204">Items added to collection.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("{collectionId}/Items")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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 | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Adds items to a collection. | ||||
|         /// </summary> | ||||
|         /// <param name="collectionId">The collection id.</param> | ||||
|         /// <param name="ids">Item ids, comma delimited.</param> | ||||
|         /// <response code="204">Items added to collection.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("{collectionId}/Items")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> AddToCollection( | ||||
|             [FromRoute, Required] Guid collectionId, | ||||
|             [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) | ||||
|         { | ||||
|             await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Removes items from a collection. | ||||
|         /// </summary> | ||||
|         /// <param name="collectionId">The collection id.</param> | ||||
|         /// <param name="ids">Item ids, comma delimited.</param> | ||||
|         /// <response code="204">Items removed from collection.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpDelete("{collectionId}/Items")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> RemoveFromCollection( | ||||
|             [FromRoute, Required] Guid collectionId, | ||||
|             [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) | ||||
|         { | ||||
|             await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Removes items from a collection. | ||||
|     /// </summary> | ||||
|     /// <param name="collectionId">The collection id.</param> | ||||
|     /// <param name="ids">Item ids, comma delimited.</param> | ||||
|     /// <response code="204">Items removed from collection.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpDelete("{collectionId}/Items")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> RemoveFromCollection( | ||||
|         [FromRoute, Required] Guid collectionId, | ||||
|         [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) | ||||
|     { | ||||
|         await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false); | ||||
|         return NoContent(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -13,124 +13,123 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Configuration Controller. | ||||
| /// </summary> | ||||
| [Route("System")] | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class ConfigurationController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IServerConfigurationManager _configurationManager; | ||||
|     private readonly IMediaEncoder _mediaEncoder; | ||||
| 
 | ||||
|     private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Configuration Controller. | ||||
|     /// Initializes a new instance of the <see cref="ConfigurationController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("System")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class ConfigurationController : BaseJellyfinApiController | ||||
|     /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> | ||||
|     public ConfigurationController( | ||||
|         IServerConfigurationManager configurationManager, | ||||
|         IMediaEncoder mediaEncoder) | ||||
|     { | ||||
|         private readonly IServerConfigurationManager _configurationManager; | ||||
|         private readonly IMediaEncoder _mediaEncoder; | ||||
|         _configurationManager = configurationManager; | ||||
|         _mediaEncoder = mediaEncoder; | ||||
|     } | ||||
| 
 | ||||
|         private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options; | ||||
|     /// <summary> | ||||
|     /// Gets application configuration. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Application configuration returned.</response> | ||||
|     /// <returns>Application configuration.</returns> | ||||
|     [HttpGet("Configuration")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<ServerConfiguration> GetConfiguration() | ||||
|     { | ||||
|         return _configurationManager.Configuration; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ConfigurationController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> | ||||
|         public ConfigurationController( | ||||
|             IServerConfigurationManager configurationManager, | ||||
|             IMediaEncoder mediaEncoder) | ||||
|     /// <summary> | ||||
|     /// Updates application configuration. | ||||
|     /// </summary> | ||||
|     /// <param name="configuration">Configuration.</param> | ||||
|     /// <response code="204">Configuration updated.</response> | ||||
|     /// <returns>Update status.</returns> | ||||
|     [HttpPost("Configuration")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration) | ||||
|     { | ||||
|         _configurationManager.ReplaceConfiguration(configuration); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a named configuration. | ||||
|     /// </summary> | ||||
|     /// <param name="key">Configuration key.</param> | ||||
|     /// <response code="200">Configuration returned.</response> | ||||
|     /// <returns>Configuration.</returns> | ||||
|     [HttpGet("Configuration/{key}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesFile(MediaTypeNames.Application.Json)] | ||||
|     public ActionResult<object> GetNamedConfiguration([FromRoute, Required] string key) | ||||
|     { | ||||
|         return _configurationManager.GetConfiguration(key); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Updates named configuration. | ||||
|     /// </summary> | ||||
|     /// <param name="key">Configuration key.</param> | ||||
|     /// <param name="configuration">Configuration.</param> | ||||
|     /// <response code="204">Named configuration updated.</response> | ||||
|     /// <returns>Update status.</returns> | ||||
|     [HttpPost("Configuration/{key}")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public 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"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets application configuration. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Application configuration returned.</response> | ||||
|         /// <returns>Application configuration.</returns> | ||||
|         [HttpGet("Configuration")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<ServerConfiguration> GetConfiguration() | ||||
|         { | ||||
|             return _configurationManager.Configuration; | ||||
|         } | ||||
|         _configurationManager.SaveConfiguration(key, deserializedConfiguration); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates application configuration. | ||||
|         /// </summary> | ||||
|         /// <param name="configuration">Configuration.</param> | ||||
|         /// <response code="204">Configuration updated.</response> | ||||
|         /// <returns>Update status.</returns> | ||||
|         [HttpPost("Configuration")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration) | ||||
|         { | ||||
|             _configurationManager.ReplaceConfiguration(configuration); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Gets a default MetadataOptions object. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Metadata options returned.</response> | ||||
|     /// <returns>Default MetadataOptions.</returns> | ||||
|     [HttpGet("Configuration/MetadataOptions/Default")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<MetadataOptions> GetDefaultMetadataOptions() | ||||
|     { | ||||
|         return new MetadataOptions(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a named configuration. | ||||
|         /// </summary> | ||||
|         /// <param name="key">Configuration key.</param> | ||||
|         /// <response code="200">Configuration returned.</response> | ||||
|         /// <returns>Configuration.</returns> | ||||
|         [HttpGet("Configuration/{key}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesFile(MediaTypeNames.Application.Json)] | ||||
|         public ActionResult<object> GetNamedConfiguration([FromRoute, Required] string key) | ||||
|         { | ||||
|             return _configurationManager.GetConfiguration(key); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates named configuration. | ||||
|         /// </summary> | ||||
|         /// <param name="key">Configuration key.</param> | ||||
|         /// <param name="configuration">Configuration.</param> | ||||
|         /// <response code="204">Named configuration updated.</response> | ||||
|         /// <returns>Update status.</returns> | ||||
|         [HttpPost("Configuration/{key}")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a default MetadataOptions object. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Metadata options returned.</response> | ||||
|         /// <returns>Default MetadataOptions.</returns> | ||||
|         [HttpGet("Configuration/MetadataOptions/Default")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<MetadataOptions> GetDefaultMetadataOptions() | ||||
|         { | ||||
|             return new MetadataOptions(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates the path to the media encoder. | ||||
|         /// </summary> | ||||
|         /// <param name="mediaEncoderPath">Media encoder path form body.</param> | ||||
|         /// <response code="204">Media encoder path updated.</response> | ||||
|         /// <returns>Status.</returns> | ||||
|         [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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Updates the path to the media encoder. | ||||
|     /// </summary> | ||||
|     /// <param name="mediaEncoderPath">Media encoder path form body.</param> | ||||
|     /// <response code="204">Media encoder path updated.</response> | ||||
|     /// <returns>Status.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -14,103 +14,102 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The dashboard controller. | ||||
| /// </summary> | ||||
| [Route("")] | ||||
| public class DashboardController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ILogger<DashboardController> _logger; | ||||
|     private readonly IPluginManager _pluginManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The dashboard controller. | ||||
|     /// Initializes a new instance of the <see cref="DashboardController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     public class DashboardController : BaseJellyfinApiController | ||||
|     /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param> | ||||
|     /// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param> | ||||
|     public DashboardController( | ||||
|         ILogger<DashboardController> logger, | ||||
|         IPluginManager pluginManager) | ||||
|     { | ||||
|         private readonly ILogger<DashboardController> _logger; | ||||
|         private readonly IPluginManager _pluginManager; | ||||
|         _logger = logger; | ||||
|         _pluginManager = pluginManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="DashboardController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param> | ||||
|         /// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param> | ||||
|         public DashboardController( | ||||
|             ILogger<DashboardController> logger, | ||||
|             IPluginManager pluginManager) | ||||
|     /// <summary> | ||||
|     /// Gets the configuration pages. | ||||
|     /// </summary> | ||||
|     /// <param name="enableInMainMenu">Whether to enable in the main menu.</param> | ||||
|     /// <response code="200">ConfigurationPages returned.</response> | ||||
|     /// <response code="404">Server still loading.</response> | ||||
|     /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns> | ||||
|     [HttpGet("web/ConfigurationPages")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public ActionResult<IEnumerable<ConfigurationPageInfo>> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the configuration pages. | ||||
|         /// </summary> | ||||
|         /// <param name="enableInMainMenu">Whether to enable in the main menu.</param> | ||||
|         /// <response code="200">ConfigurationPages returned.</response> | ||||
|         /// <response code="404">Server still loading.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns> | ||||
|         [HttpGet("web/ConfigurationPages")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages( | ||||
|             [FromQuery] bool? enableInMainMenu) | ||||
|         return configPages; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a dashboard configuration page. | ||||
|     /// </summary> | ||||
|     /// <param name="name">The name of the page.</param> | ||||
|     /// <response code="200">ConfigurationPage returned.</response> | ||||
|     /// <response code="404">Plugin configuration page not found.</response> | ||||
|     /// <returns>The configuration page.</returns> | ||||
|     [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a dashboard configuration page. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the page.</param> | ||||
|         /// <response code="200">ConfigurationPage returned.</response> | ||||
|         /// <response code="404">Plugin configuration page not found.</response> | ||||
|         /// <returns>The configuration page.</returns> | ||||
|         [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<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin) | ||||
|         return File(stream, MimeTypes.GetMimeType(resourcePath)); | ||||
|     } | ||||
| 
 | ||||
|     private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin) | ||||
|     { | ||||
|         return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1)); | ||||
|     } | ||||
| 
 | ||||
|     private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin) | ||||
|     { | ||||
|         if (plugin.Instance is not IHasWebPages hasWebPages) | ||||
|         { | ||||
|             return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1)); | ||||
|             return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>(); | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin) | ||||
|         { | ||||
|             if (plugin.Instance is not IHasWebPages hasWebPages) | ||||
|             { | ||||
|                 return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>(); | ||||
|             } | ||||
|         return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance)); | ||||
|     } | ||||
| 
 | ||||
|             return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance)); | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages() | ||||
|         { | ||||
|             return _pluginManager.Plugins.SelectMany(GetPluginPages); | ||||
|         } | ||||
|     private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages() | ||||
|     { | ||||
|         return _pluginManager.Plugins.SelectMany(GetPluginPages); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -13,129 +13,128 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Devices Controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.RequiresElevation)] | ||||
| public class DevicesController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IDeviceManager _deviceManager; | ||||
|     private readonly ISessionManager _sessionManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Devices Controller. | ||||
|     /// Initializes a new instance of the <see cref="DevicesController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     public class DevicesController : BaseJellyfinApiController | ||||
|     /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param> | ||||
|     /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param> | ||||
|     public DevicesController( | ||||
|         IDeviceManager deviceManager, | ||||
|         ISessionManager sessionManager) | ||||
|     { | ||||
|         private readonly IDeviceManager _deviceManager; | ||||
|         private readonly ISessionManager _sessionManager; | ||||
|         _deviceManager = deviceManager; | ||||
|         _sessionManager = sessionManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="DevicesController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param> | ||||
|         /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param> | ||||
|         public DevicesController( | ||||
|             IDeviceManager deviceManager, | ||||
|             ISessionManager sessionManager) | ||||
|     /// <summary> | ||||
|     /// Get Devices. | ||||
|     /// </summary> | ||||
|     /// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param> | ||||
|     /// <param name="userId">Gets or sets the user identifier.</param> | ||||
|     /// <response code="200">Devices retrieved.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns> | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) | ||||
|     { | ||||
|         return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get info for a device. | ||||
|     /// </summary> | ||||
|     /// <param name="id">Device Id.</param> | ||||
|     /// <response code="200">Device info retrieved.</response> | ||||
|     /// <response code="404">Device not found.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> | ||||
|     [HttpGet("Info")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id) | ||||
|     { | ||||
|         var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false); | ||||
|         if (deviceInfo is null) | ||||
|         { | ||||
|             _deviceManager = deviceManager; | ||||
|             _sessionManager = sessionManager; | ||||
|             return NotFound(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get Devices. | ||||
|         /// </summary> | ||||
|         /// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param> | ||||
|         /// <param name="userId">Gets or sets the user identifier.</param> | ||||
|         /// <response code="200">Devices retrieved.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) | ||||
|         return deviceInfo; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get options for a device. | ||||
|     /// </summary> | ||||
|     /// <param name="id">Device Id.</param> | ||||
|     /// <response code="200">Device options retrieved.</response> | ||||
|     /// <response code="404">Device not found.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> | ||||
|     [HttpGet("Options")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult<DeviceOptions>> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get info for a device. | ||||
|         /// </summary> | ||||
|         /// <param name="id">Device Id.</param> | ||||
|         /// <response code="200">Device info retrieved.</response> | ||||
|         /// <response code="404">Device not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> | ||||
|         [HttpGet("Info")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id) | ||||
|         { | ||||
|             var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false); | ||||
|             if (deviceInfo is null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
|         return deviceInfo; | ||||
|     } | ||||
| 
 | ||||
|             return deviceInfo; | ||||
|     /// <summary> | ||||
|     /// Update device options. | ||||
|     /// </summary> | ||||
|     /// <param name="id">Device Id.</param> | ||||
|     /// <param name="deviceOptions">Device Options.</param> | ||||
|     /// <response code="204">Device options updated.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Options")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> UpdateDeviceOptions( | ||||
|         [FromQuery, Required] string id, | ||||
|         [FromBody, Required] DeviceOptionsDto deviceOptions) | ||||
|     { | ||||
|         await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Deletes a device. | ||||
|     /// </summary> | ||||
|     /// <param name="id">Device Id.</param> | ||||
|     /// <response code="204">Device deleted.</response> | ||||
|     /// <response code="404">Device not found.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> | ||||
|     [HttpDelete] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id) | ||||
|     { | ||||
|         var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false); | ||||
|         if (existingDevice is null) | ||||
|         { | ||||
|             return NotFound(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get options for a device. | ||||
|         /// </summary> | ||||
|         /// <param name="id">Device Id.</param> | ||||
|         /// <response code="200">Device options retrieved.</response> | ||||
|         /// <response code="404">Device not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> | ||||
|         [HttpGet("Options")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult<DeviceOptions>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Update device options. | ||||
|         /// </summary> | ||||
|         /// <param name="id">Device Id.</param> | ||||
|         /// <param name="deviceOptions">Device Options.</param> | ||||
|         /// <response code="204">Device options updated.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Options")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> UpdateDeviceOptions( | ||||
|             [FromQuery, Required] string id, | ||||
|             [FromBody, Required] DeviceOptionsDto deviceOptions) | ||||
|         { | ||||
|             await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Deletes a device. | ||||
|         /// </summary> | ||||
|         /// <param name="id">Device Id.</param> | ||||
|         /// <response code="204">Device deleted.</response> | ||||
|         /// <response code="404">Device not found.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> | ||||
|         [HttpDelete] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -14,201 +14,200 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Display Preferences Controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class DisplayPreferencesController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IDisplayPreferencesManager _displayPreferencesManager; | ||||
|     private readonly ILogger<DisplayPreferencesController> _logger; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Display Preferences Controller. | ||||
|     /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class DisplayPreferencesController : BaseJellyfinApiController | ||||
|     /// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param> | ||||
|     /// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param> | ||||
|     public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger) | ||||
|     { | ||||
|         private readonly IDisplayPreferencesManager _displayPreferencesManager; | ||||
|         private readonly ILogger<DisplayPreferencesController> _logger; | ||||
|         _displayPreferencesManager = displayPreferencesManager; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param> | ||||
|         /// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param> | ||||
|         public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger) | ||||
|     /// <summary> | ||||
|     /// Get Display Preferences. | ||||
|     /// </summary> | ||||
|     /// <param name="displayPreferencesId">Display preferences id.</param> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="client">Client.</param> | ||||
|     /// <response code="200">Display preferences retrieved.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns> | ||||
|     [HttpGet("{displayPreferencesId}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")] | ||||
|     public ActionResult<DisplayPreferencesDto> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get Display Preferences. | ||||
|         /// </summary> | ||||
|         /// <param name="displayPreferencesId">Display preferences id.</param> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="client">Client.</param> | ||||
|         /// <response code="200">Display preferences retrieved.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns> | ||||
|         [HttpGet("{displayPreferencesId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")] | ||||
|         public ActionResult<DisplayPreferencesDto> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Update Display Preferences. | ||||
|         /// </summary> | ||||
|         /// <param name="displayPreferencesId">Display preferences id.</param> | ||||
|         /// <param name="userId">User Id.</param> | ||||
|         /// <param name="client">Client.</param> | ||||
|         /// <param name="displayPreferences">New Display Preferences object.</param> | ||||
|         /// <response code="204">Display preferences updated.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|         [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; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Update Display Preferences. | ||||
|     /// </summary> | ||||
|     /// <param name="displayPreferencesId">Display preferences id.</param> | ||||
|     /// <param name="userId">User Id.</param> | ||||
|     /// <param name="client">Client.</param> | ||||
|     /// <param name="displayPreferences">New Display Preferences object.</param> | ||||
|     /// <response code="204">Display preferences updated.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|     [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<IndexingKind>(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>(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<HomeSectionType>(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<IndexingKind>(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>(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<ViewType>(displayPreferences.CustomPrefs[key], true, out var type)) | ||||
|             { | ||||
|                 var order = int.Parse(key.AsSpan().Slice("homesection".Length), CultureInfo.InvariantCulture); | ||||
|                 if (!Enum.TryParse<HomeSectionType>(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<ViewType>(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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -7,127 +7,126 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Dlna Controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.RequiresElevation)] | ||||
| public class DlnaController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IDlnaManager _dlnaManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Dlna Controller. | ||||
|     /// Initializes a new instance of the <see cref="DlnaController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     public class DlnaController : BaseJellyfinApiController | ||||
|     /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> | ||||
|     public DlnaController(IDlnaManager dlnaManager) | ||||
|     { | ||||
|         private readonly IDlnaManager _dlnaManager; | ||||
|         _dlnaManager = dlnaManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="DlnaController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> | ||||
|         public DlnaController(IDlnaManager dlnaManager) | ||||
|     /// <summary> | ||||
|     /// Get profile infos. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Device profile infos returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns> | ||||
|     [HttpGet("ProfileInfos")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos() | ||||
|     { | ||||
|         return Ok(_dlnaManager.GetProfileInfos()); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets the default profile. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Default device profile returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the default profile.</returns> | ||||
|     [HttpGet("Profiles/Default")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<DeviceProfile> GetDefaultProfile() | ||||
|     { | ||||
|         return _dlnaManager.GetDefaultProfile(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a single profile. | ||||
|     /// </summary> | ||||
|     /// <param name="profileId">Profile Id.</param> | ||||
|     /// <response code="200">Device profile returned.</response> | ||||
|     /// <response code="404">Device profile not found.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns> | ||||
|     [HttpGet("Profiles/{profileId}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId) | ||||
|     { | ||||
|         var profile = _dlnaManager.GetProfile(profileId); | ||||
|         if (profile is null) | ||||
|         { | ||||
|             _dlnaManager = dlnaManager; | ||||
|             return NotFound(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get profile infos. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Device profile infos returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns> | ||||
|         [HttpGet("ProfileInfos")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos() | ||||
|         return profile; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Deletes a profile. | ||||
|     /// </summary> | ||||
|     /// <param name="profileId">Profile id.</param> | ||||
|     /// <response code="204">Device profile deleted.</response> | ||||
|     /// <response code="404">Device profile not found.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns> | ||||
|     [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the default profile. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Default device profile returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the default profile.</returns> | ||||
|         [HttpGet("Profiles/Default")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<DeviceProfile> GetDefaultProfile() | ||||
|         _dlnaManager.DeleteProfile(profileId); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Creates a profile. | ||||
|     /// </summary> | ||||
|     /// <param name="deviceProfile">Device profile.</param> | ||||
|     /// <response code="204">Device profile created.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Profiles")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile) | ||||
|     { | ||||
|         _dlnaManager.CreateProfile(deviceProfile); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Updates a profile. | ||||
|     /// </summary> | ||||
|     /// <param name="profileId">Profile id.</param> | ||||
|     /// <param name="deviceProfile">Device profile.</param> | ||||
|     /// <response code="204">Device profile updated.</response> | ||||
|     /// <response code="404">Device profile not found.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns> | ||||
|     [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a single profile. | ||||
|         /// </summary> | ||||
|         /// <param name="profileId">Profile Id.</param> | ||||
|         /// <response code="200">Device profile returned.</response> | ||||
|         /// <response code="404">Device profile not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns> | ||||
|         [HttpGet("Profiles/{profileId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId) | ||||
|         { | ||||
|             var profile = _dlnaManager.GetProfile(profileId); | ||||
|             if (profile is null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             return profile; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Deletes a profile. | ||||
|         /// </summary> | ||||
|         /// <param name="profileId">Profile id.</param> | ||||
|         /// <response code="204">Device profile deleted.</response> | ||||
|         /// <response code="404">Device profile not found.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates a profile. | ||||
|         /// </summary> | ||||
|         /// <param name="deviceProfile">Device profile.</param> | ||||
|         /// <response code="204">Device profile created.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Profiles")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile) | ||||
|         { | ||||
|             _dlnaManager.CreateProfile(deviceProfile); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates a profile. | ||||
|         /// </summary> | ||||
|         /// <param name="profileId">Profile id.</param> | ||||
|         /// <param name="deviceProfile">Device profile.</param> | ||||
|         /// <response code="204">Device profile updated.</response> | ||||
|         /// <response code="404">Device profile not found.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns> | ||||
|         [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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -14,311 +14,310 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Dlna Server Controller. | ||||
| /// </summary> | ||||
| [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; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Dlna Server Controller. | ||||
|     /// Initializes a new instance of the <see cref="DlnaServerController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("Dlna")] | ||||
|     [DlnaEnabled] | ||||
|     [Authorize(Policy = Policies.AnonymousLanAccessPolicy)] | ||||
|     public class DlnaServerController : BaseJellyfinApiController | ||||
|     /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="DlnaServerController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> | ||||
|         public DlnaServerController(IDlnaManager dlnaManager) | ||||
|     /// <summary> | ||||
|     /// Get Description Xml. | ||||
|     /// </summary> | ||||
|     /// <param name="serverId">Server UUID.</param> | ||||
|     /// <response code="200">Description xml returned.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the description xml.</returns> | ||||
|     [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<string> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets Dlna content directory xml. | ||||
|     /// </summary> | ||||
|     /// <param name="serverId">Server UUID.</param> | ||||
|     /// <response code="200">Dlna content directory returned.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns> | ||||
|     [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<string> GetContentDirectory([FromRoute, Required] string serverId) | ||||
|     { | ||||
|         return Ok(_contentDirectory.GetServiceXml()); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets Dlna media receiver registrar xml. | ||||
|     /// </summary> | ||||
|     /// <param name="serverId">Server UUID.</param> | ||||
|     /// <response code="200">Dlna media receiver registrar xml returned.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     /// <returns>Dlna media receiver registrar xml.</returns> | ||||
|     [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<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId) | ||||
|     { | ||||
|         return Ok(_mediaReceiverRegistrar.GetServiceXml()); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets Dlna media receiver registrar xml. | ||||
|     /// </summary> | ||||
|     /// <param name="serverId">Server UUID.</param> | ||||
|     /// <response code="200">Dlna media receiver registrar xml returned.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     /// <returns>Dlna media receiver registrar xml.</returns> | ||||
|     [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<string> GetConnectionManager([FromRoute, Required] string serverId) | ||||
|     { | ||||
|         return Ok(_connectionManager.GetServiceXml()); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Process a content directory control request. | ||||
|     /// </summary> | ||||
|     /// <param name="serverId">Server UUID.</param> | ||||
|     /// <response code="200">Request processed.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     /// <returns>Control response.</returns> | ||||
|     [HttpPost("{serverId}/ContentDirectory/Control")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] | ||||
|     [Produces(MediaTypeNames.Text.Xml)] | ||||
|     [ProducesFile(MediaTypeNames.Text.Xml)] | ||||
|     public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) | ||||
|     { | ||||
|         return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Process a connection manager control request. | ||||
|     /// </summary> | ||||
|     /// <param name="serverId">Server UUID.</param> | ||||
|     /// <response code="200">Request processed.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     /// <returns>Control response.</returns> | ||||
|     [HttpPost("{serverId}/ConnectionManager/Control")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] | ||||
|     [Produces(MediaTypeNames.Text.Xml)] | ||||
|     [ProducesFile(MediaTypeNames.Text.Xml)] | ||||
|     public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) | ||||
|     { | ||||
|         return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Process a media receiver registrar control request. | ||||
|     /// </summary> | ||||
|     /// <param name="serverId">Server UUID.</param> | ||||
|     /// <response code="200">Request processed.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     /// <returns>Control response.</returns> | ||||
|     [HttpPost("{serverId}/MediaReceiverRegistrar/Control")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] | ||||
|     [Produces(MediaTypeNames.Text.Xml)] | ||||
|     [ProducesFile(MediaTypeNames.Text.Xml)] | ||||
|     public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) | ||||
|     { | ||||
|         return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Processes an event subscription request. | ||||
|     /// </summary> | ||||
|     /// <param name="serverId">Server UUID.</param> | ||||
|     /// <response code="200">Request processed.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     /// <returns>Event subscription response.</returns> | ||||
|     [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<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) | ||||
|     { | ||||
|         return ProcessEventRequest(_mediaReceiverRegistrar); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Processes an event subscription request. | ||||
|     /// </summary> | ||||
|     /// <param name="serverId">Server UUID.</param> | ||||
|     /// <response code="200">Request processed.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     /// <returns>Event subscription response.</returns> | ||||
|     [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<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) | ||||
|     { | ||||
|         return ProcessEventRequest(_contentDirectory); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Processes an event subscription request. | ||||
|     /// </summary> | ||||
|     /// <param name="serverId">Server UUID.</param> | ||||
|     /// <response code="200">Request processed.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     /// <returns>Event subscription response.</returns> | ||||
|     [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<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) | ||||
|     { | ||||
|         return ProcessEventRequest(_connectionManager); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a server icon. | ||||
|     /// </summary> | ||||
|     /// <param name="serverId">Server UUID.</param> | ||||
|     /// <param name="fileName">The icon filename.</param> | ||||
|     /// <response code="200">Request processed.</response> | ||||
|     /// <response code="404">Not Found.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     /// <returns>Icon stream.</returns> | ||||
|     [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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a server icon. | ||||
|     /// </summary> | ||||
|     /// <param name="fileName">The icon filename.</param> | ||||
|     /// <returns>Icon stream.</returns> | ||||
|     /// <response code="200">Request processed.</response> | ||||
|     /// <response code="404">Not Found.</response> | ||||
|     /// <response code="503">DLNA is disabled.</response> | ||||
|     [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get Description Xml. | ||||
|         /// </summary> | ||||
|         /// <param name="serverId">Server UUID.</param> | ||||
|         /// <response code="200">Description xml returned.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the description xml.</returns> | ||||
|         [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<string> 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)); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets Dlna content directory xml. | ||||
|         /// </summary> | ||||
|         /// <param name="serverId">Server UUID.</param> | ||||
|         /// <response code="200">Dlna content directory returned.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns> | ||||
|         [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<string> GetContentDirectory([FromRoute, Required] string serverId) | ||||
|         { | ||||
|             return Ok(_contentDirectory.GetServiceXml()); | ||||
|         } | ||||
|     private string GetAbsoluteUri() | ||||
|     { | ||||
|         return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}"; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets Dlna media receiver registrar xml. | ||||
|         /// </summary> | ||||
|         /// <param name="serverId">Server UUID.</param> | ||||
|         /// <response code="200">Dlna media receiver registrar xml returned.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         /// <returns>Dlna media receiver registrar xml.</returns> | ||||
|         [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<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId) | ||||
|     private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service) | ||||
|     { | ||||
|         return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers) | ||||
|         { | ||||
|             return Ok(_mediaReceiverRegistrar.GetServiceXml()); | ||||
|         } | ||||
|             InputXml = requestStream, | ||||
|             TargetServerUuId = id, | ||||
|             RequestedUrl = GetAbsoluteUri() | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets Dlna media receiver registrar xml. | ||||
|         /// </summary> | ||||
|         /// <param name="serverId">Server UUID.</param> | ||||
|         /// <response code="200">Dlna media receiver registrar xml returned.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         /// <returns>Dlna media receiver registrar xml.</returns> | ||||
|         [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<string> 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"]; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Process a content directory control request. | ||||
|         /// </summary> | ||||
|         /// <param name="serverId">Server UUID.</param> | ||||
|         /// <response code="200">Request processed.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         /// <returns>Control response.</returns> | ||||
|         [HttpPost("{serverId}/ContentDirectory/Control")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] | ||||
|         [Produces(MediaTypeNames.Text.Xml)] | ||||
|         [ProducesFile(MediaTypeNames.Text.Xml)] | ||||
|         public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) | ||||
|         { | ||||
|             return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Process a connection manager control request. | ||||
|         /// </summary> | ||||
|         /// <param name="serverId">Server UUID.</param> | ||||
|         /// <response code="200">Request processed.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         /// <returns>Control response.</returns> | ||||
|         [HttpPost("{serverId}/ConnectionManager/Control")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] | ||||
|         [Produces(MediaTypeNames.Text.Xml)] | ||||
|         [ProducesFile(MediaTypeNames.Text.Xml)] | ||||
|         public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) | ||||
|         { | ||||
|             return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Process a media receiver registrar control request. | ||||
|         /// </summary> | ||||
|         /// <param name="serverId">Server UUID.</param> | ||||
|         /// <response code="200">Request processed.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         /// <returns>Control response.</returns> | ||||
|         [HttpPost("{serverId}/MediaReceiverRegistrar/Control")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] | ||||
|         [Produces(MediaTypeNames.Text.Xml)] | ||||
|         [ProducesFile(MediaTypeNames.Text.Xml)] | ||||
|         public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) | ||||
|         { | ||||
|             return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Processes an event subscription request. | ||||
|         /// </summary> | ||||
|         /// <param name="serverId">Server UUID.</param> | ||||
|         /// <response code="200">Request processed.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         /// <returns>Event subscription response.</returns> | ||||
|         [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<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) | ||||
|         { | ||||
|             return ProcessEventRequest(_mediaReceiverRegistrar); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Processes an event subscription request. | ||||
|         /// </summary> | ||||
|         /// <param name="serverId">Server UUID.</param> | ||||
|         /// <response code="200">Request processed.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         /// <returns>Event subscription response.</returns> | ||||
|         [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<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) | ||||
|         { | ||||
|             return ProcessEventRequest(_contentDirectory); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Processes an event subscription request. | ||||
|         /// </summary> | ||||
|         /// <param name="serverId">Server UUID.</param> | ||||
|         /// <response code="200">Request processed.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         /// <returns>Event subscription response.</returns> | ||||
|         [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<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) | ||||
|         { | ||||
|             return ProcessEventRequest(_connectionManager); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a server icon. | ||||
|         /// </summary> | ||||
|         /// <param name="serverId">Server UUID.</param> | ||||
|         /// <param name="fileName">The icon filename.</param> | ||||
|         /// <response code="200">Request processed.</response> | ||||
|         /// <response code="404">Not Found.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         /// <returns>Icon stream.</returns> | ||||
|         [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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a server icon. | ||||
|         /// </summary> | ||||
|         /// <param name="fileName">The icon filename.</param> | ||||
|         /// <returns>Icon stream.</returns> | ||||
|         /// <response code="200">Request processed.</response> | ||||
|         /// <response code="404">Not Found.</response> | ||||
|         /// <response code="503">DLNA is disabled.</response> | ||||
|         [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<ControlResponse> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -12,186 +12,185 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Environment Controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] | ||||
| public class EnvironmentController : BaseJellyfinApiController | ||||
| { | ||||
|     private const char UncSeparator = '\\'; | ||||
|     private const string UncStartPrefix = @"\\"; | ||||
| 
 | ||||
|     private readonly IFileSystem _fileSystem; | ||||
|     private readonly ILogger<EnvironmentController> _logger; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Environment Controller. | ||||
|     /// Initializes a new instance of the <see cref="EnvironmentController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] | ||||
|     public class EnvironmentController : BaseJellyfinApiController | ||||
|     /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|     /// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param> | ||||
|     public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger) | ||||
|     { | ||||
|         private const char UncSeparator = '\\'; | ||||
|         private const string UncStartPrefix = @"\\"; | ||||
|         _fileSystem = fileSystem; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         private readonly IFileSystem _fileSystem; | ||||
|         private readonly ILogger<EnvironmentController> _logger; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="EnvironmentController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|         /// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param> | ||||
|         public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger) | ||||
|     /// <summary> | ||||
|     /// Gets the contents of a given directory in the file system. | ||||
|     /// </summary> | ||||
|     /// <param name="path">The path.</param> | ||||
|     /// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param> | ||||
|     /// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param> | ||||
|     /// <response code="200">Directory contents returned.</response> | ||||
|     /// <returns>Directory contents.</returns> | ||||
|     [HttpGet("DirectoryContents")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public IEnumerable<FileSystemEntryInfo> 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<FileSystemEntryInfo>(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the contents of a given directory in the file system. | ||||
|         /// </summary> | ||||
|         /// <param name="path">The path.</param> | ||||
|         /// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param> | ||||
|         /// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param> | ||||
|         /// <response code="200">Directory contents returned.</response> | ||||
|         /// <returns>Directory contents.</returns> | ||||
|         [HttpGet("DirectoryContents")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public IEnumerable<FileSystemEntryInfo> 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)); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Validates path. | ||||
|     /// </summary> | ||||
|     /// <param name="validatePathDto">Validate request object.</param> | ||||
|     /// <response code="204">Path validated.</response> | ||||
|     /// <response code="404">Path not found.</response> | ||||
|     /// <returns>Validation status.</returns> | ||||
|     [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<FileSystemEntryInfo>(); | ||||
|             } | ||||
| 
 | ||||
|             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)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Validates path. | ||||
|         /// </summary> | ||||
|         /// <param name="validatePathDto">Validate request object.</param> | ||||
|         /// <response code="204">Path validated.</response> | ||||
|         /// <response code="404">Path not found.</response> | ||||
|         /// <returns>Validation status.</returns> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets network paths. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Empty array returned.</response> | ||||
|         /// <returns>List of entries.</returns> | ||||
|         [Obsolete("This endpoint is obsolete.")] | ||||
|         [HttpGet("NetworkShares")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares() | ||||
|         else | ||||
|         { | ||||
|             _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares"); | ||||
|             return Array.Empty<FileSystemEntryInfo>(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets available drives from the server's file system. | ||||
|         /// </summary> | ||||
|         /// <response code="200">List of entries returned.</response> | ||||
|         /// <returns>List of entries.</returns> | ||||
|         [HttpGet("Drives")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public IEnumerable<FileSystemEntryInfo> GetDrives() | ||||
|         { | ||||
|             return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the parent path of a given path. | ||||
|         /// </summary> | ||||
|         /// <param name="path">The path.</param> | ||||
|         /// <returns>Parent path.</returns> | ||||
|         [HttpGet("ParentPath")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<string?> 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; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get Default directory browser. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Default directory browser returned.</response> | ||||
|         /// <returns>Default directory browser.</returns> | ||||
|         [HttpGet("DefaultDirectoryBrowser")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<DefaultDirectoryBrowserInfoDto> GetDefaultDirectoryBrowser() | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets network paths. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Empty array returned.</response> | ||||
|     /// <returns>List of entries.</returns> | ||||
|     [Obsolete("This endpoint is obsolete.")] | ||||
|     [HttpGet("NetworkShares")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares() | ||||
|     { | ||||
|         _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares"); | ||||
|         return Array.Empty<FileSystemEntryInfo>(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets available drives from the server's file system. | ||||
|     /// </summary> | ||||
|     /// <response code="200">List of entries returned.</response> | ||||
|     /// <returns>List of entries.</returns> | ||||
|     [HttpGet("Drives")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public IEnumerable<FileSystemEntryInfo> GetDrives() | ||||
|     { | ||||
|         return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory)); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets the parent path of a given path. | ||||
|     /// </summary> | ||||
|     /// <param name="path">The path.</param> | ||||
|     /// <returns>Parent path.</returns> | ||||
|     [HttpGet("ParentPath")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<string?> 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; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get Default directory browser. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Default directory browser returned.</response> | ||||
|     /// <returns>Default directory browser.</returns> | ||||
|     [HttpGet("DefaultDirectoryBrowser")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<DefaultDirectoryBrowserInfoDto> GetDefaultDirectoryBrowser() | ||||
|     { | ||||
|         return new DefaultDirectoryBrowserInfoDto(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -12,205 +12,204 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Filters controller. | ||||
| /// </summary> | ||||
| [Route("")] | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class FilterController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly IUserManager _userManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Filters controller. | ||||
|     /// Initializes a new instance of the <see cref="FilterController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class FilterController : BaseJellyfinApiController | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     public FilterController(ILibraryManager libraryManager, IUserManager userManager) | ||||
|     { | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly IUserManager _userManager; | ||||
|         _libraryManager = libraryManager; | ||||
|         _userManager = userManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="FilterController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         public FilterController(ILibraryManager libraryManager, IUserManager userManager) | ||||
|     /// <summary> | ||||
|     /// Gets legacy query filters. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">Optional. User id.</param> | ||||
|     /// <param name="parentId">Optional. Parent id.</param> | ||||
|     /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|     /// <response code="200">Legacy filters retrieved.</response> | ||||
|     /// <returns>Legacy query filters.</returns> | ||||
|     [HttpGet("Items/Filters")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryFiltersLegacy> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets legacy query filters. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">Optional. User id.</param> | ||||
|         /// <param name="parentId">Optional. Parent id.</param> | ||||
|         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|         /// <response code="200">Legacy filters retrieved.</response> | ||||
|         /// <returns>Legacy query filters.</returns> | ||||
|         [HttpGet("Items/Filters")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryFiltersLegacy> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets query filters. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">Optional. User id.</param> | ||||
|         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="isAiring">Optional. Is item airing.</param> | ||||
|         /// <param name="isMovie">Optional. Is item movie.</param> | ||||
|         /// <param name="isSports">Optional. Is item sports.</param> | ||||
|         /// <param name="isKids">Optional. Is item kids.</param> | ||||
|         /// <param name="isNews">Optional. Is item news.</param> | ||||
|         /// <param name="isSeries">Optional. Is item series.</param> | ||||
|         /// <param name="recursive">Optional. Search recursive.</param> | ||||
|         /// <response code="200">Filters retrieved.</response> | ||||
|         /// <returns>Query filters.</returns> | ||||
|         [HttpGet("Items/Filters2")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryFilters> 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<ItemFields>(), | ||||
|                     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<Guid>() : 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(); | ||||
|             } | ||||
|     /// <summary> | ||||
|     /// Gets query filters. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">Optional. User id.</param> | ||||
|     /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="isAiring">Optional. Is item airing.</param> | ||||
|     /// <param name="isMovie">Optional. Is item movie.</param> | ||||
|     /// <param name="isSports">Optional. Is item sports.</param> | ||||
|     /// <param name="isKids">Optional. Is item kids.</param> | ||||
|     /// <param name="isNews">Optional. Is item news.</param> | ||||
|     /// <param name="isSeries">Optional. Is item series.</param> | ||||
|     /// <param name="recursive">Optional. Search recursive.</param> | ||||
|     /// <response code="200">Filters retrieved.</response> | ||||
|     /// <returns>Query filters.</returns> | ||||
|     [HttpGet("Items/Filters2")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryFilters> 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<ItemFields>(), | ||||
|                 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<Guid>() : 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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The genres controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class GenresController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IUserManager _userManager; | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly IDtoService _dtoService; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The genres controller. | ||||
|     /// Initializes a new instance of the <see cref="GenresController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class GenresController : BaseJellyfinApiController | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="GenresController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         public GenresController( | ||||
|             IUserManager userManager, | ||||
|             ILibraryManager libraryManager, | ||||
|             IDtoService dtoService) | ||||
|     /// <summary> | ||||
|     /// Gets all genres from a given item, folder, or the entire library. | ||||
|     /// </summary> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="searchTerm">The search term.</param> | ||||
|     /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|     /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|     /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|     /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|     /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> | ||||
|     /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> | ||||
|     /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|     /// <param name="enableTotalRecordCount">Optional. Include total record count.</param> | ||||
|     /// <response code="200">Genres returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the queryresult of genres.</returns> | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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) | ||||
|         }; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets all genres from a given item, folder, or the entire library. | ||||
|         /// </summary> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="searchTerm">The search term.</param> | ||||
|         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|         /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|         /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> | ||||
|         /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> | ||||
|         /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|         /// <param name="enableTotalRecordCount">Optional. Include total record count.</param> | ||||
|         /// <response code="200">Genres returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the queryresult of genres.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a genre, by name. | ||||
|         /// </summary> | ||||
|         /// <param name="genreName">The genre name.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <response code="200">Genres returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the genre.</returns> | ||||
|         [HttpGet("{genreName}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<BaseItemDto> 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<Genre>(_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<T>(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<T>().FirstOrDefault(); | ||||
| 
 | ||||
|             result ??= libraryManager.GetItemList(new InternalItemsQuery | ||||
|             { | ||||
|                 Name = name.Replace(BaseItem.SlugChar, '/'), | ||||
|                 IncludeItemTypes = new[] { baseItemKind }, | ||||
|                 DtoOptions = dtoOptions | ||||
|             }).OfType<T>().FirstOrDefault(); | ||||
| 
 | ||||
|             result ??= libraryManager.GetItemList(new InternalItemsQuery | ||||
|             { | ||||
|                 Name = name.Replace(BaseItem.SlugChar, '?'), | ||||
|                 IncludeItemTypes = new[] { baseItemKind }, | ||||
|                 DtoOptions = dtoOptions | ||||
|             }).OfType<T>().FirstOrDefault(); | ||||
| 
 | ||||
|             return result; | ||||
|             result = _libraryManager.GetGenres(query); | ||||
|         } | ||||
| 
 | ||||
|         var shouldIncludeItemTypes = includeItemTypes.Length != 0; | ||||
|         return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a genre, by name. | ||||
|     /// </summary> | ||||
|     /// <param name="genreName">The genre name.</param> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <response code="200">Genres returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the genre.</returns> | ||||
|     [HttpGet("{genreName}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<BaseItemDto> 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<Genre>(_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<T>(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<T>().FirstOrDefault(); | ||||
| 
 | ||||
|         result ??= libraryManager.GetItemList(new InternalItemsQuery | ||||
|         { | ||||
|             Name = name.Replace(BaseItem.SlugChar, '/'), | ||||
|             IncludeItemTypes = new[] { baseItemKind }, | ||||
|             DtoOptions = dtoOptions | ||||
|         }).OfType<T>().FirstOrDefault(); | ||||
| 
 | ||||
|         result ??= libraryManager.GetItemList(new InternalItemsQuery | ||||
|         { | ||||
|             Name = name.Replace(BaseItem.SlugChar, '?'), | ||||
|             IncludeItemTypes = new[] { baseItemKind }, | ||||
|             DtoOptions = dtoOptions | ||||
|         }).OfType<T>().FirstOrDefault(); | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -15,178 +15,177 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The hls segment controller. | ||||
| /// </summary> | ||||
| [Route("")] | ||||
| public class HlsSegmentController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IFileSystem _fileSystem; | ||||
|     private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
|     private readonly TranscodingJobHelper _transcodingJobHelper; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The hls segment controller. | ||||
|     /// Initializes a new instance of the <see cref="HlsSegmentController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     public class HlsSegmentController : BaseJellyfinApiController | ||||
|     /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|     /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     /// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="HlsSegmentController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param> | ||||
|         public HlsSegmentController( | ||||
|             IFileSystem fileSystem, | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             TranscodingJobHelper transcodingJobHelper) | ||||
|     /// <summary> | ||||
|     /// Gets the specified audio segment for an audio item. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="segmentId">The segment id.</param> | ||||
|     /// <response code="200">Hls audio segment returned.</response> | ||||
|     /// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns> | ||||
|     // 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."); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the specified audio segment for an audio item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="segmentId">The segment id.</param> | ||||
|         /// <response code="200">Hls audio segment returned.</response> | ||||
|         /// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns> | ||||
|         // 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)); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a hls video playlist. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The video id.</param> | ||||
|     /// <param name="playlistId">The playlist id.</param> | ||||
|     /// <response code="200">Hls video playlist returned.</response> | ||||
|     /// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns> | ||||
|     [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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Stops an active encoding. | ||||
|     /// </summary> | ||||
|     /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> | ||||
|     /// <param name="playSessionId">The play session id.</param> | ||||
|     /// <response code="204">Encoding stopped successfully.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a hls video segment. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="playlistId">The playlist id.</param> | ||||
|     /// <param name="segmentId">The segment id.</param> | ||||
|     /// <param name="segmentContainer">The segment container.</param> | ||||
|     /// <response code="200">Hls video segment returned.</response> | ||||
|     /// <response code="404">Hls segment not found.</response> | ||||
|     /// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns> | ||||
|     // 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; | ||||
|         }); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a hls video playlist. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The video id.</param> | ||||
|         /// <param name="playlistId">The playlist id.</param> | ||||
|         /// <response code="200">Hls video playlist returned.</response> | ||||
|         /// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns> | ||||
|         [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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Stops an active encoding. | ||||
|         /// </summary> | ||||
|         /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> | ||||
|         /// <param name="playSessionId">The play session id.</param> | ||||
|         /// <response code="204">Encoding stopped successfully.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a hls video segment. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="playlistId">The playlist id.</param> | ||||
|         /// <param name="segmentId">The segment id.</param> | ||||
|         /// <param name="segmentContainer">The segment container.</param> | ||||
|         /// <response code="200">Hls video segment returned.</response> | ||||
|         /// <response code="404">Hls segment not found.</response> | ||||
|         /// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns> | ||||
|         // 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)); | ||||
|     } | ||||
| } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -16,346 +16,345 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The instant mix controller. | ||||
| /// </summary> | ||||
| [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; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The instant mix controller. | ||||
|     /// Initializes a new instance of the <see cref="InstantMixController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class InstantMixController : BaseJellyfinApiController | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     /// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="InstantMixController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         public InstantMixController( | ||||
|             IUserManager userManager, | ||||
|             IDtoService dtoService, | ||||
|             IMusicManager musicManager, | ||||
|             ILibraryManager libraryManager) | ||||
|     /// <summary> | ||||
|     /// Creates an instant playlist based on a given song. | ||||
|     /// </summary> | ||||
|     /// <param name="id">The item id.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <response code="200">Instant playlist returned.</response> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|     [HttpGet("Songs/{id}/InstantMix")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Creates an instant playlist based on a given album. | ||||
|     /// </summary> | ||||
|     /// <param name="id">The item id.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <response code="200">Instant playlist returned.</response> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|     [HttpGet("Albums/{id}/InstantMix")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Creates an instant playlist based on a given playlist. | ||||
|     /// </summary> | ||||
|     /// <param name="id">The item id.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <response code="200">Instant playlist returned.</response> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|     [HttpGet("Playlists/{id}/InstantMix")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Creates an instant playlist based on a given genre. | ||||
|     /// </summary> | ||||
|     /// <param name="name">The genre name.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <response code="200">Instant playlist returned.</response> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|     [HttpGet("MusicGenres/{name}/InstantMix")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Creates an instant playlist based on a given artist. | ||||
|     /// </summary> | ||||
|     /// <param name="id">The item id.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <response code="200">Instant playlist returned.</response> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|     [HttpGet("Artists/{id}/InstantMix")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Creates an instant playlist based on a given item. | ||||
|     /// </summary> | ||||
|     /// <param name="id">The item id.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <response code="200">Instant playlist returned.</response> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|     [HttpGet("Items/{id}/InstantMix")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Creates an instant playlist based on a given artist. | ||||
|     /// </summary> | ||||
|     /// <param name="id">The item id.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <response code="200">Instant playlist returned.</response> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|     [HttpGet("Artists/InstantMix")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [Obsolete("Use GetInstantMixFromArtists")] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Creates an instant playlist based on a given genre. | ||||
|     /// </summary> | ||||
|     /// <param name="id">The item id.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <response code="200">Instant playlist returned.</response> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|     [HttpGet("MusicGenres/InstantMix")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto> GetResult(List<BaseItem> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates an instant playlist based on a given song. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The item id.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <response code="200">Instant playlist returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|         [HttpGet("Songs/{id}/InstantMix")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates an instant playlist based on a given album. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The item id.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <response code="200">Instant playlist returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|         [HttpGet("Albums/{id}/InstantMix")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>( | ||||
|             0, | ||||
|             totalCount, | ||||
|             returnList); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates an instant playlist based on a given playlist. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The item id.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <response code="200">Instant playlist returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|         [HttpGet("Playlists/{id}/InstantMix")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates an instant playlist based on a given genre. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The genre name.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <response code="200">Instant playlist returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|         [HttpGet("MusicGenres/{name}/InstantMix")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates an instant playlist based on a given artist. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The item id.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <response code="200">Instant playlist returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|         [HttpGet("Artists/{id}/InstantMix")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates an instant playlist based on a given item. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The item id.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <response code="200">Instant playlist returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|         [HttpGet("Items/{id}/InstantMix")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates an instant playlist based on a given artist. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The item id.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <response code="200">Instant playlist returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|         [HttpGet("Artists/InstantMix")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [Obsolete("Use GetInstantMixFromArtists")] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates an instant playlist based on a given genre. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The item id.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <response code="200">Instant playlist returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> | ||||
|         [HttpGet("MusicGenres/InstantMix")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto> GetResult(List<BaseItem> 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<BaseItemDto>( | ||||
|                 0, | ||||
|                 totalCount, | ||||
|                 returnList); | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -18,257 +18,256 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Item lookup controller. | ||||
| /// </summary> | ||||
| [Route("")] | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class ItemLookupController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IProviderManager _providerManager; | ||||
|     private readonly IFileSystem _fileSystem; | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly ILogger<ItemLookupController> _logger; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Item lookup controller. | ||||
|     /// Initializes a new instance of the <see cref="ItemLookupController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class ItemLookupController : BaseJellyfinApiController | ||||
|     /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> | ||||
|     /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param> | ||||
|     public ItemLookupController( | ||||
|         IProviderManager providerManager, | ||||
|         IFileSystem fileSystem, | ||||
|         ILibraryManager libraryManager, | ||||
|         ILogger<ItemLookupController> logger) | ||||
|     { | ||||
|         private readonly IProviderManager _providerManager; | ||||
|         private readonly IFileSystem _fileSystem; | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly ILogger<ItemLookupController> _logger; | ||||
|         _providerManager = providerManager; | ||||
|         _fileSystem = fileSystem; | ||||
|         _libraryManager = libraryManager; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ItemLookupController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> | ||||
|         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param> | ||||
|         public ItemLookupController( | ||||
|             IProviderManager providerManager, | ||||
|             IFileSystem fileSystem, | ||||
|             ILibraryManager libraryManager, | ||||
|             ILogger<ItemLookupController> logger) | ||||
|     /// <summary> | ||||
|     /// Get the item's external id info. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <response code="200">External id info retrieved.</response> | ||||
|     /// <response code="404">Item not found.</response> | ||||
|     /// <returns>List of external id info.</returns> | ||||
|     [HttpGet("Items/{itemId}/ExternalIdInfos")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId) | ||||
|     { | ||||
|         var item = _libraryManager.GetItemById(itemId); | ||||
|         if (item is null) | ||||
|         { | ||||
|             _providerManager = providerManager; | ||||
|             _fileSystem = fileSystem; | ||||
|             _libraryManager = libraryManager; | ||||
|             _logger = logger; | ||||
|             return NotFound(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get the item's external id info. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <response code="200">External id info retrieved.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>List of external id info.</returns> | ||||
|         [HttpGet("Items/{itemId}/ExternalIdInfos")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(itemId); | ||||
|             if (item is null) | ||||
|         return Ok(_providerManager.GetExternalIdInfos(item)); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get movie remote search. | ||||
|     /// </summary> | ||||
|     /// <param name="query">Remote search query.</param> | ||||
|     /// <response code="200">Movie remote search executed.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|     /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|     /// </returns> | ||||
|     [HttpPost("Items/RemoteSearch/Movie")] | ||||
|     public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MovieInfo> query) | ||||
|     { | ||||
|         var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None) | ||||
|             .ConfigureAwait(false); | ||||
|         return Ok(results); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get trailer remote search. | ||||
|     /// </summary> | ||||
|     /// <param name="query">Remote search query.</param> | ||||
|     /// <response code="200">Trailer remote search executed.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|     /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|     /// </returns> | ||||
|     [HttpPost("Items/RemoteSearch/Trailer")] | ||||
|     public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery<TrailerInfo> query) | ||||
|     { | ||||
|         var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None) | ||||
|             .ConfigureAwait(false); | ||||
|         return Ok(results); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get music video remote search. | ||||
|     /// </summary> | ||||
|     /// <param name="query">Remote search query.</param> | ||||
|     /// <response code="200">Music video remote search executed.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|     /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|     /// </returns> | ||||
|     [HttpPost("Items/RemoteSearch/MusicVideo")] | ||||
|     public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MusicVideoInfo> query) | ||||
|     { | ||||
|         var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None) | ||||
|             .ConfigureAwait(false); | ||||
|         return Ok(results); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get series remote search. | ||||
|     /// </summary> | ||||
|     /// <param name="query">Remote search query.</param> | ||||
|     /// <response code="200">Series remote search executed.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|     /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|     /// </returns> | ||||
|     [HttpPost("Items/RemoteSearch/Series")] | ||||
|     public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery<SeriesInfo> query) | ||||
|     { | ||||
|         var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None) | ||||
|             .ConfigureAwait(false); | ||||
|         return Ok(results); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get box set remote search. | ||||
|     /// </summary> | ||||
|     /// <param name="query">Remote search query.</param> | ||||
|     /// <response code="200">Box set remote search executed.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|     /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|     /// </returns> | ||||
|     [HttpPost("Items/RemoteSearch/BoxSet")] | ||||
|     public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BoxSetInfo> query) | ||||
|     { | ||||
|         var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None) | ||||
|             .ConfigureAwait(false); | ||||
|         return Ok(results); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get music artist remote search. | ||||
|     /// </summary> | ||||
|     /// <param name="query">Remote search query.</param> | ||||
|     /// <response code="200">Music artist remote search executed.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|     /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|     /// </returns> | ||||
|     [HttpPost("Items/RemoteSearch/MusicArtist")] | ||||
|     public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery<ArtistInfo> query) | ||||
|     { | ||||
|         var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None) | ||||
|             .ConfigureAwait(false); | ||||
|         return Ok(results); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get music album remote search. | ||||
|     /// </summary> | ||||
|     /// <param name="query">Remote search query.</param> | ||||
|     /// <response code="200">Music album remote search executed.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|     /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|     /// </returns> | ||||
|     [HttpPost("Items/RemoteSearch/MusicAlbum")] | ||||
|     public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery<AlbumInfo> query) | ||||
|     { | ||||
|         var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None) | ||||
|             .ConfigureAwait(false); | ||||
|         return Ok(results); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get person remote search. | ||||
|     /// </summary> | ||||
|     /// <param name="query">Remote search query.</param> | ||||
|     /// <response code="200">Person remote search executed.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|     /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|     /// </returns> | ||||
|     [HttpPost("Items/RemoteSearch/Person")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery<PersonLookupInfo> query) | ||||
|     { | ||||
|         var results = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(query, CancellationToken.None) | ||||
|             .ConfigureAwait(false); | ||||
|         return Ok(results); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get book remote search. | ||||
|     /// </summary> | ||||
|     /// <param name="query">Remote search query.</param> | ||||
|     /// <response code="200">Book remote search executed.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|     /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|     /// </returns> | ||||
|     [HttpPost("Items/RemoteSearch/Book")] | ||||
|     public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BookInfo> query) | ||||
|     { | ||||
|         var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None) | ||||
|             .ConfigureAwait(false); | ||||
|         return Ok(results); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Applies search criteria to an item and refreshes metadata. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <param name="searchResult">The remote search result.</param> | ||||
|     /// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param> | ||||
|     /// <response code="204">Item metadata refreshed.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|     /// The task result contains an <see cref="NoContentResult"/>. | ||||
|     /// </returns> | ||||
|     [HttpPost("Items/RemoteSearch/Apply/{itemId}")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get movie remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Movie remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("Items/RemoteSearch/Movie")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MovieInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get trailer remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Trailer remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("Items/RemoteSearch/Trailer")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery<TrailerInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get music video remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Music video remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("Items/RemoteSearch/MusicVideo")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MusicVideoInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get series remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Series remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("Items/RemoteSearch/Series")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery<SeriesInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get box set remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Box set remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("Items/RemoteSearch/BoxSet")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BoxSetInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get music artist remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Music artist remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("Items/RemoteSearch/MusicArtist")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery<ArtistInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get music album remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Music album remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("Items/RemoteSearch/MusicAlbum")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery<AlbumInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get person remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Person remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("Items/RemoteSearch/Person")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery<PersonLookupInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get book remote search. | ||||
|         /// </summary> | ||||
|         /// <param name="query">Remote search query.</param> | ||||
|         /// <response code="200">Book remote search executed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="OkResult"/> containing the list of remote search results. | ||||
|         /// </returns> | ||||
|         [HttpPost("Items/RemoteSearch/Book")] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BookInfo> query) | ||||
|         { | ||||
|             var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None) | ||||
|                 .ConfigureAwait(false); | ||||
|             return Ok(results); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Applies search criteria to an item and refreshes metadata. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <param name="searchResult">The remote search result.</param> | ||||
|         /// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param> | ||||
|         /// <response code="204">Item metadata refreshed.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results. | ||||
|         /// The task result contains an <see cref="NoContentResult"/>. | ||||
|         /// </returns> | ||||
|         [HttpPost("Items/RemoteSearch/Apply/{itemId}")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -9,78 +9,77 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Item Refresh Controller. | ||||
| /// </summary> | ||||
| [Route("Items")] | ||||
| [Authorize(Policy = Policies.RequiresElevation)] | ||||
| public class ItemRefreshController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly IProviderManager _providerManager; | ||||
|     private readonly IFileSystem _fileSystem; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Item Refresh Controller. | ||||
|     /// Initializes a new instance of the <see cref="ItemRefreshController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("Items")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     public class ItemRefreshController : BaseJellyfinApiController | ||||
|     /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param> | ||||
|     /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ItemRefreshController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param> | ||||
|         /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> | ||||
|         public ItemRefreshController( | ||||
|             ILibraryManager libraryManager, | ||||
|             IProviderManager providerManager, | ||||
|             IFileSystem fileSystem) | ||||
|     /// <summary> | ||||
|     /// Refreshes metadata for an item. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param> | ||||
|     /// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param> | ||||
|     /// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param> | ||||
|     /// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param> | ||||
|     /// <response code="204">Item metadata refresh queued.</response> | ||||
|     /// <response code="404">Item to refresh not found.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|     [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Refreshes metadata for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param> | ||||
|         /// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param> | ||||
|         /// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param> | ||||
|         /// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param> | ||||
|         /// <response code="204">Item metadata refresh queued.</response> | ||||
|         /// <response code="404">Item to refresh not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|         [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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -20,332 +20,332 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Item update controller. | ||||
| /// </summary> | ||||
| [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; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Item update controller. | ||||
|     /// Initializes a new instance of the <see cref="ItemUpdateController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     public class ItemUpdateController : BaseJellyfinApiController | ||||
|     /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> | ||||
|     /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param> | ||||
|     /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ItemUpdateController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> | ||||
|         /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         public ItemUpdateController( | ||||
|             IFileSystem fileSystem, | ||||
|             ILibraryManager libraryManager, | ||||
|             IProviderManager providerManager, | ||||
|             ILocalizationManager localizationManager, | ||||
|             IServerConfigurationManager serverConfigurationManager) | ||||
|     /// <summary> | ||||
|     /// Updates an item. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="request">The new item properties.</param> | ||||
|     /// <response code="204">Item updated.</response> | ||||
|     /// <response code="404">Item not found.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|     [HttpPost("Items/{itemId}")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="request">The new item properties.</param> | ||||
|         /// <response code="204">Item updated.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|         [HttpPost("Items/{itemId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult> 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()); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets metadata editor info for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <response code="200">Item metadata editor returned.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|         [HttpGet("Items/{itemId}/MetadataEditor")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<MetadataEditorInfo> 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; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates an item's content type. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="contentType">The content type of the item.</param> | ||||
|         /// <response code="204">Item content type updated.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|         [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets metadata editor info for an item. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <response code="200">Item metadata editor returned.</response> | ||||
|     /// <response code="404">Item not found.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|     [HttpGet("Items/{itemId}/MetadataEditor")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<MetadataEditorInfo> 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; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Updates an item's content type. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="contentType">The content type of the item.</param> | ||||
|     /// <response code="204">Item content type updated.</response> | ||||
|     /// <response code="404">Item not found.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns> | ||||
|     [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<NameValuePair> GetContentTypeOptions(bool isForItem) | ||||
|         { | ||||
|             var list = new List<NameValuePair>(); | ||||
| 
 | ||||
|             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<NameValuePair> GetContentTypeOptions(bool isForItem) | ||||
|     { | ||||
|         var list = new List<NameValuePair>(); | ||||
| 
 | ||||
|         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; | ||||
|     } | ||||
| } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -20,308 +20,307 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The library structure controller. | ||||
| /// </summary> | ||||
| [Route("Library/VirtualFolders")] | ||||
| [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] | ||||
| public class LibraryStructureController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IServerApplicationPaths _appPaths; | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly ILibraryMonitor _libraryMonitor; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The library structure controller. | ||||
|     /// Initializes a new instance of the <see cref="LibraryStructureController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("Library/VirtualFolders")] | ||||
|     [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] | ||||
|     public class LibraryStructureController : BaseJellyfinApiController | ||||
|     /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="LibraryStructureController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param> | ||||
|         public LibraryStructureController( | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             ILibraryManager libraryManager, | ||||
|             ILibraryMonitor libraryMonitor) | ||||
|     /// <summary> | ||||
|     /// Gets all virtual folders. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Virtual folders retrieved.</response> | ||||
|     /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns> | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders() | ||||
|     { | ||||
|         return _libraryManager.GetVirtualFolders(true); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Adds a virtual folder. | ||||
|     /// </summary> | ||||
|     /// <param name="name">The name of the virtual folder.</param> | ||||
|     /// <param name="collectionType">The type of the collection.</param> | ||||
|     /// <param name="paths">The paths of the virtual folder.</param> | ||||
|     /// <param name="libraryOptionsDto">The library options.</param> | ||||
|     /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|     /// <response code="204">Folder added.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets all virtual folders. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Virtual folders retrieved.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders() | ||||
|         await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false); | ||||
| 
 | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Removes a virtual folder. | ||||
|     /// </summary> | ||||
|     /// <param name="name">The name of the folder.</param> | ||||
|     /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|     /// <response code="204">Folder removed.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpDelete] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> RemoveVirtualFolder( | ||||
|         [FromQuery] string? name, | ||||
|         [FromQuery] bool refreshLibrary = false) | ||||
|     { | ||||
|         await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Renames a virtual folder. | ||||
|     /// </summary> | ||||
|     /// <param name="name">The name of the virtual folder.</param> | ||||
|     /// <param name="newName">The new name.</param> | ||||
|     /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|     /// <response code="204">Folder renamed.</response> | ||||
|     /// <response code="404">Library doesn't exist.</response> | ||||
|     /// <response code="409">Library already exists.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns> | ||||
|     /// <exception cref="ArgumentNullException">The new name may not be null.</exception> | ||||
|     [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)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Adds a virtual folder. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the virtual folder.</param> | ||||
|         /// <param name="collectionType">The type of the collection.</param> | ||||
|         /// <param name="paths">The paths of the virtual folder.</param> | ||||
|         /// <param name="libraryOptionsDto">The library options.</param> | ||||
|         /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|         /// <response code="204">Folder added.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Removes a virtual folder. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the folder.</param> | ||||
|         /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|         /// <response code="204">Folder removed.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpDelete] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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."); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Renames a virtual folder. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the virtual folder.</param> | ||||
|         /// <param name="newName">The new name.</param> | ||||
|         /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|         /// <response code="204">Folder renamed.</response> | ||||
|         /// <response code="404">Library doesn't exist.</response> | ||||
|         /// <response code="409">Library already exists.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns> | ||||
|         /// <exception cref="ArgumentNullException">The new name may not be null.</exception> | ||||
|         [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<double>(), 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<double>(), 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(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Add a media path to a library. | ||||
|         /// </summary> | ||||
|         /// <param name="mediaPathDto">The media path dto.</param> | ||||
|         /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         /// <response code="204">Media path added.</response> | ||||
|         /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> | ||||
|         [HttpPost("Paths")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult AddMediaPath( | ||||
|             [FromBody, Required] MediaPathDto mediaPathDto, | ||||
|             [FromQuery] bool refreshLibrary = false) | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Add a media path to a library. | ||||
|     /// </summary> | ||||
|     /// <param name="mediaPathDto">The media path dto.</param> | ||||
|     /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     /// <response code="204">Media path added.</response> | ||||
|     /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> | ||||
|     [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<double>(), 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates a media path. | ||||
|         /// </summary> | ||||
|         /// <param name="mediaPathRequestDto">The name of the library and path infos.</param> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         /// <response code="204">Media path updated.</response> | ||||
|         /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Remove a media path. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the library.</param> | ||||
|         /// <param name="path">The path to remove.</param> | ||||
|         /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         /// <response code="204">Media path removed.</response> | ||||
|         /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> | ||||
|         [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<double>(), 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<double>(), 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(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Update library options. | ||||
|         /// </summary> | ||||
|         /// <param name="request">The library name and options.</param> | ||||
|         /// <response code="204">Library updated.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("LibraryOptions")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult UpdateLibraryOptions( | ||||
|             [FromBody] UpdateLibraryOptionsDto request) | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Updates a media path. | ||||
|     /// </summary> | ||||
|     /// <param name="mediaPathRequestDto">The name of the library and path infos.</param> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     /// <response code="204">Media path updated.</response> | ||||
|     /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> | ||||
|     [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Remove a media path. | ||||
|     /// </summary> | ||||
|     /// <param name="name">The name of the library.</param> | ||||
|     /// <param name="path">The path to remove.</param> | ||||
|     /// <param name="refreshLibrary">Whether to refresh the library.</param> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     /// <response code="204">Media path removed.</response> | ||||
|     /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception> | ||||
|     [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<double>(), 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Update library options. | ||||
|     /// </summary> | ||||
|     /// <param name="request">The library name and options.</param> | ||||
|     /// <response code="204">Library updated.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("LibraryOptions")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public ActionResult UpdateLibraryOptions( | ||||
|         [FromBody] UpdateLibraryOptionsDto request) | ||||
|     { | ||||
|         var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id); | ||||
| 
 | ||||
|         collectionFolder.UpdateLibraryOptions(request.LibraryOptions); | ||||
|         return NoContent(); | ||||
|     } | ||||
| } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -6,71 +6,70 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Localization controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] | ||||
| public class LocalizationController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ILocalizationManager _localization; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Localization controller. | ||||
|     /// Initializes a new instance of the <see cref="LocalizationController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] | ||||
|     public class LocalizationController : BaseJellyfinApiController | ||||
|     /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> | ||||
|     public LocalizationController(ILocalizationManager localization) | ||||
|     { | ||||
|         private readonly ILocalizationManager _localization; | ||||
|         _localization = localization; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="LocalizationController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> | ||||
|         public LocalizationController(ILocalizationManager localization) | ||||
|         { | ||||
|             _localization = localization; | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Gets known cultures. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Known cultures returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns> | ||||
|     [HttpGet("Cultures")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<CultureDto>> GetCultures() | ||||
|     { | ||||
|         return Ok(_localization.GetCultures()); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets known cultures. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Known cultures returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns> | ||||
|         [HttpGet("Cultures")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<CultureDto>> GetCultures() | ||||
|         { | ||||
|             return Ok(_localization.GetCultures()); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Gets known countries. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Known countries returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the list of countries.</returns> | ||||
|     [HttpGet("Countries")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<CountryInfo>> GetCountries() | ||||
|     { | ||||
|         return Ok(_localization.GetCountries()); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets known countries. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Known countries returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of countries.</returns> | ||||
|         [HttpGet("Countries")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<CountryInfo>> GetCountries() | ||||
|         { | ||||
|             return Ok(_localization.GetCountries()); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Gets known parental ratings. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Known parental ratings returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns> | ||||
|     [HttpGet("ParentalRatings")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings() | ||||
|     { | ||||
|         return Ok(_localization.GetParentalRatings()); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets known parental ratings. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Known parental ratings returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns> | ||||
|         [HttpGet("ParentalRatings")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings() | ||||
|         { | ||||
|             return Ok(_localization.GetParentalRatings()); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets localization options. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Localization options returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of localization options.</returns> | ||||
|         [HttpGet("Options")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<LocalizationOption>> GetLocalizationOptions() | ||||
|         { | ||||
|             return Ok(_localization.GetLocalizationOptions()); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Gets localization options. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Localization options returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the list of localization options.</returns> | ||||
|     [HttpGet("Options")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<LocalizationOption>> GetLocalizationOptions() | ||||
|     { | ||||
|         return Ok(_localization.GetLocalizationOptions()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The media info controller. | ||||
| /// </summary> | ||||
| [Route("")] | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class MediaInfoController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IMediaSourceManager _mediaSourceManager; | ||||
|     private readonly IDeviceManager _deviceManager; | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly ILogger<MediaInfoController> _logger; | ||||
|     private readonly MediaInfoHelper _mediaInfoHelper; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The media info controller. | ||||
|     /// Initializes a new instance of the <see cref="MediaInfoController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class MediaInfoController : BaseJellyfinApiController | ||||
|     /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> | ||||
|     /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param> | ||||
|     /// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param> | ||||
|     public MediaInfoController( | ||||
|         IMediaSourceManager mediaSourceManager, | ||||
|         IDeviceManager deviceManager, | ||||
|         ILibraryManager libraryManager, | ||||
|         ILogger<MediaInfoController> logger, | ||||
|         MediaInfoHelper mediaInfoHelper) | ||||
|     { | ||||
|         private readonly IMediaSourceManager _mediaSourceManager; | ||||
|         private readonly IDeviceManager _deviceManager; | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly ILogger<MediaInfoController> _logger; | ||||
|         private readonly MediaInfoHelper _mediaInfoHelper; | ||||
|         _mediaSourceManager = mediaSourceManager; | ||||
|         _deviceManager = deviceManager; | ||||
|         _libraryManager = libraryManager; | ||||
|         _logger = logger; | ||||
|         _mediaInfoHelper = mediaInfoHelper; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="MediaInfoController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> | ||||
|         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param> | ||||
|         /// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param> | ||||
|         public MediaInfoController( | ||||
|             IMediaSourceManager mediaSourceManager, | ||||
|             IDeviceManager deviceManager, | ||||
|             ILibraryManager libraryManager, | ||||
|             ILogger<MediaInfoController> logger, | ||||
|             MediaInfoHelper mediaInfoHelper) | ||||
|     /// <summary> | ||||
|     /// Gets live playback media info for an item. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <response code="200">Playback info returned.</response> | ||||
|     /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns> | ||||
|     [HttpGet("Items/{itemId}/PlaybackInfo")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId) | ||||
|     { | ||||
|         return await _mediaInfoHelper.GetPlaybackInfo( | ||||
|                 itemId, | ||||
|                 userId) | ||||
|             .ConfigureAwait(false); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets live playback media info for an item. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. | ||||
|     /// Query parameters are obsolete. | ||||
|     /// </remarks> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param> | ||||
|     /// <param name="startTimeTicks">The start time in ticks.</param> | ||||
|     /// <param name="audioStreamIndex">The audio stream index.</param> | ||||
|     /// <param name="subtitleStreamIndex">The subtitle stream index.</param> | ||||
|     /// <param name="maxAudioChannels">The maximum number of audio channels.</param> | ||||
|     /// <param name="mediaSourceId">The media source id.</param> | ||||
|     /// <param name="liveStreamId">The livestream id.</param> | ||||
|     /// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param> | ||||
|     /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param> | ||||
|     /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param> | ||||
|     /// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param> | ||||
|     /// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param> | ||||
|     /// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param> | ||||
|     /// <param name="playbackInfoDto">The playback info.</param> | ||||
|     /// <response code="200">Playback info returned.</response> | ||||
|     /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns> | ||||
|     [HttpPost("Items/{itemId}/PlaybackInfo")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<PlaybackInfoResponse>> 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; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets live playback media info for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <response code="200">Playback info returned.</response> | ||||
|         /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns> | ||||
|         [HttpGet("Items/{itemId}/PlaybackInfo")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<PlaybackInfoResponse>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets live playback media info for an item. | ||||
|         /// </summary> | ||||
|         /// <remarks> | ||||
|         /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. | ||||
|         /// Query parameters are obsolete. | ||||
|         /// </remarks> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param> | ||||
|         /// <param name="startTimeTicks">The start time in ticks.</param> | ||||
|         /// <param name="audioStreamIndex">The audio stream index.</param> | ||||
|         /// <param name="subtitleStreamIndex">The subtitle stream index.</param> | ||||
|         /// <param name="maxAudioChannels">The maximum number of audio channels.</param> | ||||
|         /// <param name="mediaSourceId">The media source id.</param> | ||||
|         /// <param name="liveStreamId">The livestream id.</param> | ||||
|         /// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param> | ||||
|         /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param> | ||||
|         /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param> | ||||
|         /// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param> | ||||
|         /// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param> | ||||
|         /// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param> | ||||
|         /// <param name="playbackInfoDto">The playback info.</param> | ||||
|         /// <response code="200">Playback info returned.</response> | ||||
|         /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns> | ||||
|         [HttpPost("Items/{itemId}/PlaybackInfo")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<PlaybackInfoResponse>> 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; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Opens a media source. | ||||
|         /// </summary> | ||||
|         /// <param name="openToken">The open token.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="playSessionId">The play session id.</param> | ||||
|         /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param> | ||||
|         /// <param name="startTimeTicks">The start time in ticks.</param> | ||||
|         /// <param name="audioStreamIndex">The audio stream index.</param> | ||||
|         /// <param name="subtitleStreamIndex">The subtitle stream index.</param> | ||||
|         /// <param name="maxAudioChannels">The maximum number of audio channels.</param> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="openLiveStreamDto">The open live stream dto.</param> | ||||
|         /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param> | ||||
|         /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param> | ||||
|         /// <response code="200">Media source opened.</response> | ||||
|         /// <returns>A <see cref="Task"/> containing a <see cref="LiveStreamResponse"/>.</returns> | ||||
|         [HttpPost("LiveStreams/Open")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<LiveStreamResponse>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Closes a media source. | ||||
|         /// </summary> | ||||
|         /// <param name="liveStreamId">The livestream id.</param> | ||||
|         /// <response code="204">Livestream closed.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("LiveStreams/Close")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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 }; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Tests the network with a request with the size of the bitrate. | ||||
|         /// </summary> | ||||
|         /// <param name="size">The bitrate. Defaults to 102400.</param> | ||||
|         /// <response code="200">Test buffer returned.</response> | ||||
|         /// <returns>A <see cref="FileResult"/> with specified bitrate.</returns> | ||||
|         [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; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Opens a media source. | ||||
|     /// </summary> | ||||
|     /// <param name="openToken">The open token.</param> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <param name="playSessionId">The play session id.</param> | ||||
|     /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param> | ||||
|     /// <param name="startTimeTicks">The start time in ticks.</param> | ||||
|     /// <param name="audioStreamIndex">The audio stream index.</param> | ||||
|     /// <param name="subtitleStreamIndex">The subtitle stream index.</param> | ||||
|     /// <param name="maxAudioChannels">The maximum number of audio channels.</param> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="openLiveStreamDto">The open live stream dto.</param> | ||||
|     /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param> | ||||
|     /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param> | ||||
|     /// <response code="200">Media source opened.</response> | ||||
|     /// <returns>A <see cref="Task"/> containing a <see cref="LiveStreamResponse"/>.</returns> | ||||
|     [HttpPost("LiveStreams/Open")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<LiveStreamResponse>> 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<byte>.Shared.Rent(size); | ||||
|             try | ||||
|             { | ||||
|                 Random.Shared.NextBytes(buffer); | ||||
|                 return File(buffer, MediaTypeNames.Application.Octet); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 ArrayPool<byte>.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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Closes a media source. | ||||
|     /// </summary> | ||||
|     /// <param name="liveStreamId">The livestream id.</param> | ||||
|     /// <response code="204">Livestream closed.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("LiveStreams/Close")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> CloseLiveStream([FromQuery, Required] string liveStreamId) | ||||
|     { | ||||
|         await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Tests the network with a request with the size of the bitrate. | ||||
|     /// </summary> | ||||
|     /// <param name="size">The bitrate. Defaults to 102400.</param> | ||||
|     /// <response code="200">Test buffer returned.</response> | ||||
|     /// <returns>A <see cref="FileResult"/> with specified bitrate.</returns> | ||||
|     [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<byte>.Shared.Rent(size); | ||||
|         try | ||||
|         { | ||||
|             Random.Shared.NextBytes(buffer); | ||||
|             return File(buffer, MediaTypeNames.Application.Octet); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             ArrayPool<byte>.Shared.Return(buffer); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -18,122 +18,122 @@ using MediaBrowser.Model.Querying; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Movies controller. | ||||
|     /// </summary> | ||||
|     [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; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="MoviesController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         public MoviesController( | ||||
|             IUserManager userManager, | ||||
|             ILibraryManager libraryManager, | ||||
|             IDtoService dtoService, | ||||
|             IServerConfigurationManager serverConfigurationManager) | ||||
| /// <summary> | ||||
| /// Movies controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class MoviesController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IUserManager _userManager; | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly IDtoService _dtoService; | ||||
|     private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initializes a new instance of the <see cref="MoviesController"/> class. | ||||
|     /// </summary> | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     public MoviesController( | ||||
|         IUserManager userManager, | ||||
|         ILibraryManager libraryManager, | ||||
|         IDtoService dtoService, | ||||
|         IServerConfigurationManager serverConfigurationManager) | ||||
|     { | ||||
|         _userManager = userManager; | ||||
|         _libraryManager = libraryManager; | ||||
|         _dtoService = dtoService; | ||||
|         _serverConfigurationManager = serverConfigurationManager; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets movie recommendations. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="fields">Optional. The fields to return.</param> | ||||
|     /// <param name="categoryLimit">The max number of categories to return.</param> | ||||
|     /// <param name="itemLimit">The max number of items to return per category.</param> | ||||
|     /// <response code="200">Movie recommendations returned.</response> | ||||
|     /// <returns>The list of movie recommendations.</returns> | ||||
|     [HttpGet("Recommendations")] | ||||
|     public ActionResult<IEnumerable<RecommendationDto>> 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<RecommendationDto>(); | ||||
| 
 | ||||
|         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> { BaseItemKind.Movie }; | ||||
|         if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) | ||||
|         { | ||||
|             itemTypes.Add(BaseItemKind.Trailer); | ||||
|             itemTypes.Add(BaseItemKind.LiveTvProgram); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets movie recommendations. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="fields">Optional. The fields to return.</param> | ||||
|         /// <param name="categoryLimit">The max number of categories to return.</param> | ||||
|         /// <param name="itemLimit">The max number of items to return per category.</param> | ||||
|         /// <response code="200">Movie recommendations returned.</response> | ||||
|         /// <returns>The list of movie recommendations.</returns> | ||||
|         [HttpGet("Recommendations")] | ||||
|         public ActionResult<IEnumerable<RecommendationDto>> 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<RecommendationDto>(); | ||||
|         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> { 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<IEnumerator<RecommendationDto>> | ||||
|         var categoryTypes = new List<IEnumerator<RecommendationDto>> | ||||
|             { | ||||
|                 // 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<RecommendationDto> GetWithDirector( | ||||
|             User? user, | ||||
|             IEnumerable<string> names, | ||||
|             int itemLimit, | ||||
|             DtoOptions dtoOptions, | ||||
|             RecommendationType type) | ||||
|         { | ||||
|             var itemTypes = new List<BaseItemKind> { 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<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) | ||||
|         { | ||||
|             var itemTypes = new List<BaseItemKind> { 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<RecommendationDto> GetWithDirector( | ||||
|         User? user, | ||||
|         IEnumerable<string> names, | ||||
|         int itemLimit, | ||||
|         DtoOptions dtoOptions, | ||||
|         RecommendationType type) | ||||
|     { | ||||
|         var itemTypes = new List<BaseItemKind> { 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<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) | ||||
|         { | ||||
|             var itemTypes = new List<BaseItemKind> { 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<string> GetActors(IEnumerable<BaseItem> items) | ||||
|         { | ||||
|             var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), 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<string> GetDirectors(IEnumerable<BaseItem> items) | ||||
|         { | ||||
|             var people = _libraryManager.GetPeople(new InternalPeopleQuery( | ||||
|                 new[] { PersonType.Director }, | ||||
|                 Array.Empty<string>())); | ||||
| 
 | ||||
|             var itemIds = items.Select(i => i.Id).ToList(); | ||||
| 
 | ||||
|             return people | ||||
|                 .Where(i => itemIds.Contains(i.ItemId)) | ||||
|                 .Select(i => i.Name) | ||||
|                 .DistinctNames(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) | ||||
|     { | ||||
|         var itemTypes = new List<BaseItemKind> { 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<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) | ||||
|     { | ||||
|         var itemTypes = new List<BaseItemKind> { 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<string> GetActors(IEnumerable<BaseItem> items) | ||||
|     { | ||||
|         var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), 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<string> GetDirectors(IEnumerable<BaseItem> items) | ||||
|     { | ||||
|         var people = _libraryManager.GetPeople(new InternalPeopleQuery( | ||||
|             new[] { PersonType.Director }, | ||||
|             Array.Empty<string>())); | ||||
| 
 | ||||
|         var itemIds = items.Select(i => i.Id).ToList(); | ||||
| 
 | ||||
|         return people | ||||
|             .Where(i => itemIds.Contains(i.ItemId)) | ||||
|             .Select(i => i.Name) | ||||
|             .DistinctNames(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -18,181 +18,180 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The music genres controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class MusicGenresController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly IDtoService _dtoService; | ||||
|     private readonly IUserManager _userManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The music genres controller. | ||||
|     /// Initializes a new instance of the <see cref="MusicGenresController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class MusicGenresController : BaseJellyfinApiController | ||||
|     /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="MusicGenresController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param> | ||||
|         public MusicGenresController( | ||||
|             ILibraryManager libraryManager, | ||||
|             IUserManager userManager, | ||||
|             IDtoService dtoService) | ||||
|     /// <summary> | ||||
|     /// Gets all music genres from a given item, folder, or the entire library. | ||||
|     /// </summary> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="searchTerm">The search term.</param> | ||||
|     /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|     /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|     /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|     /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|     /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> | ||||
|     /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> | ||||
|     /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|     /// <param name="enableTotalRecordCount">Optional. Include total record count.</param> | ||||
|     /// <response code="200">Music genres returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns> | ||||
|     [HttpGet] | ||||
|     [Obsolete("Use GetGenres instead")] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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) | ||||
|         }; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets all music genres from a given item, folder, or the entire library. | ||||
|         /// </summary> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="searchTerm">The search term.</param> | ||||
|         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|         /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|         /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param> | ||||
|         /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> | ||||
|         /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|         /// <param name="enableTotalRecordCount">Optional. Include total record count.</param> | ||||
|         /// <response code="200">Music genres returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns> | ||||
|         [HttpGet] | ||||
|         [Obsolete("Use GetGenres instead")] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a music genre, by name. | ||||
|         /// </summary> | ||||
|         /// <param name="genreName">The genre name.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <returns>An <see cref="OkResult"/> containing a <see cref="BaseItemDto"/> with the music genre.</returns> | ||||
|         [HttpGet("{genreName}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<BaseItemDto> 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<MusicGenre>(_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<T>(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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a music genre, by name. | ||||
|     /// </summary> | ||||
|     /// <param name="genreName">The genre name.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <returns>An <see cref="OkResult"/> containing a <see cref="BaseItemDto"/> with the music genre.</returns> | ||||
|     [HttpGet("{genreName}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<BaseItemDto> 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<T>().FirstOrDefault(); | ||||
| 
 | ||||
|             result ??= libraryManager.GetItemList(new InternalItemsQuery | ||||
|             { | ||||
|                 Name = name.Replace(BaseItem.SlugChar, '/'), | ||||
|                 IncludeItemTypes = new[] { baseItemKind }, | ||||
|                 DtoOptions = dtoOptions | ||||
|             }).OfType<T>().FirstOrDefault(); | ||||
| 
 | ||||
|             result ??= libraryManager.GetItemList(new InternalItemsQuery | ||||
|             { | ||||
|                 Name = name.Replace(BaseItem.SlugChar, '?'), | ||||
|                 IncludeItemTypes = new[] { baseItemKind }, | ||||
|                 DtoOptions = dtoOptions | ||||
|             }).OfType<T>().FirstOrDefault(); | ||||
| 
 | ||||
|             return result; | ||||
|             item = GetItemFromSlugName<MusicGenre>(_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<T>(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<T>().FirstOrDefault(); | ||||
| 
 | ||||
|         result ??= libraryManager.GetItemList(new InternalItemsQuery | ||||
|         { | ||||
|             Name = name.Replace(BaseItem.SlugChar, '/'), | ||||
|             IncludeItemTypes = new[] { baseItemKind }, | ||||
|             DtoOptions = dtoOptions | ||||
|         }).OfType<T>().FirstOrDefault(); | ||||
| 
 | ||||
|         result ??= libraryManager.GetItemList(new InternalItemsQuery | ||||
|         { | ||||
|             Name = name.Replace(BaseItem.SlugChar, '?'), | ||||
|             IncludeItemTypes = new[] { baseItemKind }, | ||||
|             DtoOptions = dtoOptions | ||||
|         }).OfType<T>().FirstOrDefault(); | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -11,157 +11,156 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Package Controller. | ||||
| /// </summary> | ||||
| [Route("")] | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class PackageController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IInstallationManager _installationManager; | ||||
|     private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Package Controller. | ||||
|     /// Initializes a new instance of the <see cref="PackageController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class PackageController : BaseJellyfinApiController | ||||
|     /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> | ||||
|     /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager) | ||||
|     { | ||||
|         private readonly IInstallationManager _installationManager; | ||||
|         private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
|         _installationManager = installationManager; | ||||
|         _serverConfigurationManager = serverConfigurationManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="PackageController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager) | ||||
|     /// <summary> | ||||
|     /// Gets a package by name or assembly GUID. | ||||
|     /// </summary> | ||||
|     /// <param name="name">The name of the package.</param> | ||||
|     /// <param name="assemblyGuid">The GUID of the associated assembly.</param> | ||||
|     /// <response code="200">Package retrieved.</response> | ||||
|     /// <returns>A <see cref="PackageInfo"/> containing package information.</returns> | ||||
|     [HttpGet("Packages/{name}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<PackageInfo>> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a package by name or assembly GUID. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the package.</param> | ||||
|         /// <param name="assemblyGuid">The GUID of the associated assembly.</param> | ||||
|         /// <response code="200">Package retrieved.</response> | ||||
|         /// <returns>A <see cref="PackageInfo"/> containing package information.</returns> | ||||
|         [HttpGet("Packages/{name}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<PackageInfo>> GetPackageInfo( | ||||
|             [FromRoute, Required] string name, | ||||
|             [FromQuery] Guid? assemblyGuid) | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets available packages. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Available packages returned.</response> | ||||
|     /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns> | ||||
|     [HttpGet("Packages")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<IEnumerable<PackageInfo>> GetPackages() | ||||
|     { | ||||
|         IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); | ||||
| 
 | ||||
|         return packages; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Installs a package. | ||||
|     /// </summary> | ||||
|     /// <param name="name">Package name.</param> | ||||
|     /// <param name="assemblyGuid">GUID of the associated assembly.</param> | ||||
|     /// <param name="version">Optional version. Defaults to latest version.</param> | ||||
|     /// <param name="repositoryUrl">Optional. Specify the repository to install from.</param> | ||||
|     /// <response code="204">Package found.</response> | ||||
|     /// <response code="404">Package not found.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns> | ||||
|     [HttpPost("Packages/Installed/{name}")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets available packages. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Available packages returned.</response> | ||||
|         /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns> | ||||
|         [HttpGet("Packages")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<IEnumerable<PackageInfo>> GetPackages() | ||||
|         { | ||||
|             IEnumerable<PackageInfo> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Installs a package. | ||||
|         /// </summary> | ||||
|         /// <param name="name">Package name.</param> | ||||
|         /// <param name="assemblyGuid">GUID of the associated assembly.</param> | ||||
|         /// <param name="version">Optional version. Defaults to latest version.</param> | ||||
|         /// <param name="repositoryUrl">Optional. Specify the repository to install from.</param> | ||||
|         /// <response code="204">Package found.</response> | ||||
|         /// <response code="404">Package not found.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns> | ||||
|         [HttpPost("Packages/Installed/{name}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|             } | ||||
|     /// <summary> | ||||
|     /// Cancels a package installation. | ||||
|     /// </summary> | ||||
|     /// <param name="packageId">Installation Id.</param> | ||||
|     /// <response code="204">Installation cancelled.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns> | ||||
|     [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); | ||||
|     /// <summary> | ||||
|     /// Gets all package repositories. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Package repositories returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the list of package repositories.</returns> | ||||
|     [HttpGet("Repositories")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories() | ||||
|     { | ||||
|         return Ok(_serverConfigurationManager.Configuration.PluginRepositories.AsEnumerable()); | ||||
|     } | ||||
| 
 | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Cancels a package installation. | ||||
|         /// </summary> | ||||
|         /// <param name="packageId">Installation Id.</param> | ||||
|         /// <response code="204">Installation cancelled.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns> | ||||
|         [HttpDelete("Packages/Installing/{packageId}")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult CancelPackageInstallation( | ||||
|             [FromRoute, Required] Guid packageId) | ||||
|         { | ||||
|             _installationManager.CancelInstallation(packageId); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets all package repositories. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Package repositories returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the list of package repositories.</returns> | ||||
|         [HttpGet("Repositories")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories() | ||||
|         { | ||||
|             return Ok(_serverConfigurationManager.Configuration.PluginRepositories.AsEnumerable()); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Sets the enabled and existing package repositories. | ||||
|         /// </summary> | ||||
|         /// <param name="repositoryInfos">The list of package repositories.</param> | ||||
|         /// <response code="204">Package repositories saved.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Repositories")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos) | ||||
|         { | ||||
|             _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos; | ||||
|             _serverConfigurationManager.SaveConfiguration(); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Sets the enabled and existing package repositories. | ||||
|     /// </summary> | ||||
|     /// <param name="repositoryInfos">The list of package repositories.</param> | ||||
|     /// <response code="204">Package repositories saved.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Repositories")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos) | ||||
|     { | ||||
|         _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos; | ||||
|         _serverConfigurationManager.SaveConfiguration(); | ||||
|         return NoContent(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -15,125 +15,124 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Persons controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class PersonsController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly IDtoService _dtoService; | ||||
|     private readonly IUserManager _userManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Persons controller. | ||||
|     /// Initializes a new instance of the <see cref="PersonsController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class PersonsController : BaseJellyfinApiController | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="PersonsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         public PersonsController( | ||||
|             ILibraryManager libraryManager, | ||||
|             IDtoService dtoService, | ||||
|             IUserManager userManager) | ||||
|     /// <summary> | ||||
|     /// Gets all persons. | ||||
|     /// </summary> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="searchTerm">The search term.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="filters">Optional. Specify additional filters to apply.</param> | ||||
|     /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param> | ||||
|     /// <param name="enableUserData">Optional, include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param> | ||||
|     /// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param> | ||||
|     /// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|     /// <response code="200">Persons returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns> | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>( | ||||
|             peopleItems | ||||
|             .Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)) | ||||
|             .ToArray()); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get person by name. | ||||
|     /// </summary> | ||||
|     /// <param name="name">Person name.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <response code="200">Person returned.</response> | ||||
|     /// <response code="404">Person not found.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the person on success, | ||||
|     /// or a <see cref="NotFoundResult"/> if person not found.</returns> | ||||
|     [HttpGet("{name}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<BaseItemDto> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets all persons. | ||||
|         /// </summary> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="searchTerm">The search term.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="filters">Optional. Specify additional filters to apply.</param> | ||||
|         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param> | ||||
|         /// <param name="enableUserData">Optional, include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param> | ||||
|         /// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param> | ||||
|         /// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|         /// <response code="200">Persons returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>( | ||||
|                 peopleItems | ||||
|                 .Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)) | ||||
|                 .ToArray()); | ||||
|             var user = _userManager.GetUserById(userId.Value); | ||||
|             return _dtoService.GetBaseItemDto(item, dtoOptions, user); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get person by name. | ||||
|         /// </summary> | ||||
|         /// <param name="name">Person name.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <response code="200">Person returned.</response> | ||||
|         /// <response code="404">Person not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the person on success, | ||||
|         /// or a <see cref="NotFoundResult"/> if person not found.</returns> | ||||
|         [HttpGet("{name}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<BaseItemDto> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Playlists controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class PlaylistsController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IPlaylistManager _playlistManager; | ||||
|     private readonly IDtoService _dtoService; | ||||
|     private readonly IUserManager _userManager; | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Playlists controller. | ||||
|     /// Initializes a new instance of the <see cref="PlaylistsController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class PlaylistsController : BaseJellyfinApiController | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="PlaylistsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         public PlaylistsController( | ||||
|             IDtoService dtoService, | ||||
|             IPlaylistManager playlistManager, | ||||
|             IUserManager userManager, | ||||
|             ILibraryManager libraryManager) | ||||
|     /// <summary> | ||||
|     /// Creates a new playlist. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. | ||||
|     /// Query parameters are obsolete. | ||||
|     /// </remarks> | ||||
|     /// <param name="name">The playlist name.</param> | ||||
|     /// <param name="ids">The item ids.</param> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <param name="mediaType">The media type.</param> | ||||
|     /// <param name="createPlaylistRequest">The create playlist payload.</param> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist. | ||||
|     /// The task result contains an <see cref="OkResult"/> indicating success. | ||||
|     /// </returns> | ||||
|     [HttpPost] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( | ||||
|         [FromQuery, ParameterObsolete] string? name, | ||||
|         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> 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<Guid>(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Creates a new playlist. | ||||
|         /// </summary> | ||||
|         /// <remarks> | ||||
|         /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence. | ||||
|         /// Query parameters are obsolete. | ||||
|         /// </remarks> | ||||
|         /// <param name="name">The playlist name.</param> | ||||
|         /// <param name="ids">The item ids.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="mediaType">The media type.</param> | ||||
|         /// <param name="createPlaylistRequest">The create playlist payload.</param> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist. | ||||
|         /// The task result contains an <see cref="OkResult"/> indicating success. | ||||
|         /// </returns> | ||||
|         [HttpPost] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( | ||||
|             [FromQuery, ParameterObsolete] string? name, | ||||
|             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> 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<Guid>(); | ||||
|             } | ||||
|             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; | ||||
|     /// <summary> | ||||
|     /// Adds items to a playlist. | ||||
|     /// </summary> | ||||
|     /// <param name="playlistId">The playlist id.</param> | ||||
|     /// <param name="ids">Item id, comma delimited.</param> | ||||
|     /// <param name="userId">The userId.</param> | ||||
|     /// <response code="204">Items added to playlist.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|     [HttpPost("{playlistId}/Items")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Moves a playlist item. | ||||
|     /// </summary> | ||||
|     /// <param name="playlistId">The playlist id.</param> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="newIndex">The new index.</param> | ||||
|     /// <response code="204">Item moved to new index.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|     [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> MoveItem( | ||||
|         [FromRoute, Required] string playlistId, | ||||
|         [FromRoute, Required] string itemId, | ||||
|         [FromRoute, Required] int newIndex) | ||||
|     { | ||||
|         await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Removes items from a playlist. | ||||
|     /// </summary> | ||||
|     /// <param name="playlistId">The playlist id.</param> | ||||
|     /// <param name="entryIds">The item ids, comma delimited.</param> | ||||
|     /// <response code="204">Items removed.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|     [HttpDelete("{playlistId}/Items")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> RemoveFromPlaylist( | ||||
|         [FromRoute, Required] string playlistId, | ||||
|         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds) | ||||
|     { | ||||
|         await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets the original items of a playlist. | ||||
|     /// </summary> | ||||
|     /// <param name="playlistId">The playlist id.</param> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <response code="200">Original playlist returned.</response> | ||||
|     /// <response code="404">Playlist not found.</response> | ||||
|     /// <returns>The original playlist items.</returns> | ||||
|     [HttpGet("{playlistId}/Items")] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Adds items to a playlist. | ||||
|         /// </summary> | ||||
|         /// <param name="playlistId">The playlist id.</param> | ||||
|         /// <param name="ids">Item id, comma delimited.</param> | ||||
|         /// <param name="userId">The userId.</param> | ||||
|         /// <response code="204">Items added to playlist.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|         [HttpPost("{playlistId}/Items")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Moves a playlist item. | ||||
|         /// </summary> | ||||
|         /// <param name="playlistId">The playlist id.</param> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="newIndex">The new index.</param> | ||||
|         /// <response code="204">Item moved to new index.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|         [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Removes items from a playlist. | ||||
|         /// </summary> | ||||
|         /// <param name="playlistId">The playlist id.</param> | ||||
|         /// <param name="entryIds">The item ids, comma delimited.</param> | ||||
|         /// <response code="204">Items removed.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success.</returns> | ||||
|         [HttpDelete("{playlistId}/Items")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the original items of a playlist. | ||||
|         /// </summary> | ||||
|         /// <param name="playlistId">The playlist id.</param> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <response code="200">Original playlist returned.</response> | ||||
|         /// <response code="404">Playlist not found.</response> | ||||
|         /// <returns>The original playlist items.</returns> | ||||
|         [HttpGet("{playlistId}/Items")] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>( | ||||
|             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<BaseItemDto>( | ||||
|                 startIndex, | ||||
|                 count, | ||||
|                 dtos); | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -17,366 +17,365 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Playstate controller. | ||||
| /// </summary> | ||||
| [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<PlaystateController> _logger; | ||||
|     private readonly TranscodingJobHelper _transcodingJobHelper; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Playstate controller. | ||||
|     /// Initializes a new instance of the <see cref="PlaystateController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class PlaystateController : BaseJellyfinApiController | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> | ||||
|     /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> | ||||
|     /// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param> | ||||
|     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<PlaystateController> _logger; | ||||
|         private readonly TranscodingJobHelper _transcodingJobHelper; | ||||
|         _userManager = userManager; | ||||
|         _userDataRepository = userDataRepository; | ||||
|         _libraryManager = libraryManager; | ||||
|         _sessionManager = sessionManager; | ||||
|         _logger = loggerFactory.CreateLogger<PlaystateController>(); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="PlaystateController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> | ||||
|         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> | ||||
|         /// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param> | ||||
|         public PlaystateController( | ||||
|             IUserManager userManager, | ||||
|             IUserDataManager userDataRepository, | ||||
|             ILibraryManager libraryManager, | ||||
|             ISessionManager sessionManager, | ||||
|             ILoggerFactory loggerFactory, | ||||
|             TranscodingJobHelper transcodingJobHelper) | ||||
|         _transcodingJobHelper = transcodingJobHelper; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Marks an item as played for user. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <param name="datePlayed">Optional. The date the item was played.</param> | ||||
|     /// <response code="200">Item marked as played.</response> | ||||
|     /// <response code="404">Item not found.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns> | ||||
|     [HttpPost("Users/{userId}/PlayedItems/{itemId}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem( | ||||
|         [FromRoute, Required] Guid userId, | ||||
|         [FromRoute, Required] Guid itemId, | ||||
|         [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed) | ||||
|     { | ||||
|         var user = _userManager.GetUserById(userId); | ||||
|         var session = 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<PlaystateController>(); | ||||
| 
 | ||||
|             _transcodingJobHelper = transcodingJobHelper; | ||||
|             return NotFound(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Marks an item as played for user. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <param name="datePlayed">Optional. The date the item was played.</param> | ||||
|         /// <response code="200">Item marked as played.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns> | ||||
|         [HttpPost("Users/{userId}/PlayedItems/{itemId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult<UserItemDataDto>> 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; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Marks an item as unplayed for user. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <response code="200">Item marked as unplayed.</response> | ||||
|     /// <response code="404">Item not found.</response> | ||||
|     /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns> | ||||
|     [HttpDelete("Users/{userId}/PlayedItems/{itemId}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) | ||||
|     { | ||||
|         var user = _userManager.GetUserById(userId); | ||||
|         var session = await RequestHelpers.GetSession(_sessionManager, _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; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Reports playback has started within a session. | ||||
|     /// </summary> | ||||
|     /// <param name="playbackStartInfo">The playback start info.</param> | ||||
|     /// <response code="204">Playback start recorded.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/Playing")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Reports playback progress within a session. | ||||
|     /// </summary> | ||||
|     /// <param name="playbackProgressInfo">The playback progress info.</param> | ||||
|     /// <response code="204">Playback progress recorded.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/Playing/Progress")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Pings a playback session. | ||||
|     /// </summary> | ||||
|     /// <param name="playSessionId">Playback session id.</param> | ||||
|     /// <response code="204">Playback session pinged.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/Playing/Ping")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId) | ||||
|     { | ||||
|         _transcodingJobHelper.PingTranscodingJob(playSessionId, null); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Reports playback has stopped within a session. | ||||
|     /// </summary> | ||||
|     /// <param name="playbackStopInfo">The playback stop info.</param> | ||||
|     /// <response code="204">Playback stop recorded.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/Playing/Stopped")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Reports that a user has begun playing an item. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <param name="mediaSourceId">The id of the MediaSource.</param> | ||||
|     /// <param name="audioStreamIndex">The audio stream index.</param> | ||||
|     /// <param name="subtitleStreamIndex">The subtitle stream index.</param> | ||||
|     /// <param name="playMethod">The play method.</param> | ||||
|     /// <param name="liveStreamId">The live stream id.</param> | ||||
|     /// <param name="playSessionId">The play session id.</param> | ||||
|     /// <param name="canSeek">Indicates if the client can seek.</param> | ||||
|     /// <response code="204">Play start recorded.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Users/{userId}/PlayingItems/{itemId}")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Reports a user's playback progress. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <param name="mediaSourceId">The id of the MediaSource.</param> | ||||
|     /// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param> | ||||
|     /// <param name="audioStreamIndex">The audio stream index.</param> | ||||
|     /// <param name="subtitleStreamIndex">The subtitle stream index.</param> | ||||
|     /// <param name="volumeLevel">Scale of 0-100.</param> | ||||
|     /// <param name="playMethod">The play method.</param> | ||||
|     /// <param name="liveStreamId">The live stream id.</param> | ||||
|     /// <param name="playSessionId">The play session id.</param> | ||||
|     /// <param name="repeatMode">The repeat mode.</param> | ||||
|     /// <param name="isPaused">Indicates if the player is paused.</param> | ||||
|     /// <param name="isMuted">Indicates if the player is muted.</param> | ||||
|     /// <response code="204">Play progress recorded.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Reports that a user has stopped playing an item. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <param name="mediaSourceId">The id of the MediaSource.</param> | ||||
|     /// <param name="nextMediaType">The next media type that will play.</param> | ||||
|     /// <param name="positionTicks">Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.</param> | ||||
|     /// <param name="liveStreamId">The live stream id.</param> | ||||
|     /// <param name="playSessionId">The play session id.</param> | ||||
|     /// <response code="204">Playback stop recorded.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpDelete("Users/{userId}/PlayingItems/{itemId}")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Updates the played status. | ||||
|     /// </summary> | ||||
|     /// <param name="user">The user.</param> | ||||
|     /// <param name="item">The item.</param> | ||||
|     /// <param name="wasPlayed">if set to <c>true</c> [was played].</param> | ||||
|     /// <param name="datePlayed">The date played.</param> | ||||
|     /// <returns>Task.</returns> | ||||
|     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; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Marks an item as unplayed for user. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <response code="200">Item marked as unplayed.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns> | ||||
|         [HttpDelete("Users/{userId}/PlayedItems/{itemId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) | ||||
|         { | ||||
|             var user = _userManager.GetUserById(userId); | ||||
|             var session = await RequestHelpers.GetSession(_sessionManager, _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; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Reports playback has started within a session. | ||||
|         /// </summary> | ||||
|         /// <param name="playbackStartInfo">The playback start info.</param> | ||||
|         /// <response code="204">Playback start recorded.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/Playing")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Reports playback progress within a session. | ||||
|         /// </summary> | ||||
|         /// <param name="playbackProgressInfo">The playback progress info.</param> | ||||
|         /// <response code="204">Playback progress recorded.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/Playing/Progress")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Pings a playback session. | ||||
|         /// </summary> | ||||
|         /// <param name="playSessionId">Playback session id.</param> | ||||
|         /// <response code="204">Playback session pinged.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/Playing/Ping")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId) | ||||
|         { | ||||
|             _transcodingJobHelper.PingTranscodingJob(playSessionId, null); | ||||
|             return NoContent(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Reports playback has stopped within a session. | ||||
|         /// </summary> | ||||
|         /// <param name="playbackStopInfo">The playback stop info.</param> | ||||
|         /// <response code="204">Playback stop recorded.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/Playing/Stopped")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Reports that a user has begun playing an item. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <param name="mediaSourceId">The id of the MediaSource.</param> | ||||
|         /// <param name="audioStreamIndex">The audio stream index.</param> | ||||
|         /// <param name="subtitleStreamIndex">The subtitle stream index.</param> | ||||
|         /// <param name="playMethod">The play method.</param> | ||||
|         /// <param name="liveStreamId">The live stream id.</param> | ||||
|         /// <param name="playSessionId">The play session id.</param> | ||||
|         /// <param name="canSeek">Indicates if the client can seek.</param> | ||||
|         /// <response code="204">Play start recorded.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Users/{userId}/PlayingItems/{itemId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Reports a user's playback progress. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <param name="mediaSourceId">The id of the MediaSource.</param> | ||||
|         /// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param> | ||||
|         /// <param name="audioStreamIndex">The audio stream index.</param> | ||||
|         /// <param name="subtitleStreamIndex">The subtitle stream index.</param> | ||||
|         /// <param name="volumeLevel">Scale of 0-100.</param> | ||||
|         /// <param name="playMethod">The play method.</param> | ||||
|         /// <param name="liveStreamId">The live stream id.</param> | ||||
|         /// <param name="playSessionId">The play session id.</param> | ||||
|         /// <param name="repeatMode">The repeat mode.</param> | ||||
|         /// <param name="isPaused">Indicates if the player is paused.</param> | ||||
|         /// <param name="isMuted">Indicates if the player is muted.</param> | ||||
|         /// <response code="204">Play progress recorded.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Reports that a user has stopped playing an item. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <param name="mediaSourceId">The id of the MediaSource.</param> | ||||
|         /// <param name="nextMediaType">The next media type that will play.</param> | ||||
|         /// <param name="positionTicks">Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.</param> | ||||
|         /// <param name="liveStreamId">The live stream id.</param> | ||||
|         /// <param name="playSessionId">The play session id.</param> | ||||
|         /// <response code="204">Playback stop recorded.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpDelete("Users/{userId}/PlayingItems/{itemId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates the played status. | ||||
|         /// </summary> | ||||
|         /// <param name="user">The user.</param> | ||||
|         /// <param name="item">The item.</param> | ||||
|         /// <param name="wasPlayed">if set to <c>true</c> [was played].</param> | ||||
|         /// <param name="datePlayed">The date played.</param> | ||||
|         /// <returns>Task.</returns> | ||||
|         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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -17,250 +17,249 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Plugins controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class PluginsController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IInstallationManager _installationManager; | ||||
|     private readonly IPluginManager _pluginManager; | ||||
|     private readonly JsonSerializerOptions _serializerOptions; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Plugins controller. | ||||
|     /// Initializes a new instance of the <see cref="PluginsController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class PluginsController : BaseJellyfinApiController | ||||
|     /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> | ||||
|     /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="PluginsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> | ||||
|         /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param> | ||||
|         public PluginsController( | ||||
|             IInstallationManager installationManager, | ||||
|             IPluginManager pluginManager) | ||||
|     /// <summary> | ||||
|     /// Gets a list of currently installed plugins. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Installed plugins returned.</response> | ||||
|     /// <returns>List of currently installed plugins.</returns> | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<PluginInfo>> GetPlugins() | ||||
|     { | ||||
|         return Ok(_pluginManager.Plugins | ||||
|             .OrderBy(p => p.Name) | ||||
|             .Select(p => p.GetPluginInfo())); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Enables a disabled plugin. | ||||
|     /// </summary> | ||||
|     /// <param name="pluginId">Plugin id.</param> | ||||
|     /// <param name="version">Plugin version.</param> | ||||
|     /// <response code="204">Plugin enabled.</response> | ||||
|     /// <response code="404">Plugin not found.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|     [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of currently installed plugins. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Installed plugins returned.</response> | ||||
|         /// <returns>List of currently installed plugins.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<PluginInfo>> GetPlugins() | ||||
|         _pluginManager.EnablePlugin(plugin); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Disable a plugin. | ||||
|     /// </summary> | ||||
|     /// <param name="pluginId">Plugin id.</param> | ||||
|     /// <param name="version">Plugin version.</param> | ||||
|     /// <response code="204">Plugin disabled.</response> | ||||
|     /// <response code="404">Plugin not found.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|     [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Enables a disabled plugin. | ||||
|         /// </summary> | ||||
|         /// <param name="pluginId">Plugin id.</param> | ||||
|         /// <param name="version">Plugin version.</param> | ||||
|         /// <response code="204">Plugin enabled.</response> | ||||
|         /// <response code="404">Plugin not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|         [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(); | ||||
|     /// <summary> | ||||
|     /// Uninstalls a plugin by version. | ||||
|     /// </summary> | ||||
|     /// <param name="pluginId">Plugin id.</param> | ||||
|     /// <param name="version">Plugin version.</param> | ||||
|     /// <response code="204">Plugin uninstalled.</response> | ||||
|     /// <response code="404">Plugin not found.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|     [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Disable a plugin. | ||||
|         /// </summary> | ||||
|         /// <param name="pluginId">Plugin id.</param> | ||||
|         /// <param name="version">Plugin version.</param> | ||||
|         /// <response code="204">Plugin disabled.</response> | ||||
|         /// <response code="404">Plugin not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|         [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Uninstalls a plugin. | ||||
|     /// </summary> | ||||
|     /// <param name="pluginId">Plugin id.</param> | ||||
|     /// <response code="204">Plugin uninstalled.</response> | ||||
|     /// <response code="404">Plugin not found.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|     [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Uninstalls a plugin by version. | ||||
|         /// </summary> | ||||
|         /// <param name="pluginId">Plugin id.</param> | ||||
|         /// <param name="version">Plugin version.</param> | ||||
|         /// <response code="204">Plugin uninstalled.</response> | ||||
|         /// <response code="404">Plugin not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Uninstalls a plugin. | ||||
|         /// </summary> | ||||
|         /// <param name="pluginId">Plugin id.</param> | ||||
|         /// <response code="204">Plugin uninstalled.</response> | ||||
|         /// <response code="404">Plugin not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|         [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets plugin configuration. | ||||
|     /// </summary> | ||||
|     /// <param name="pluginId">Plugin id.</param> | ||||
|     /// <response code="200">Plugin configuration returned.</response> | ||||
|     /// <response code="404">Plugin not found or plugin configuration not found.</response> | ||||
|     /// <returns>Plugin configuration.</returns> | ||||
|     [HttpGet("{pluginId}/Configuration")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<BasePluginConfiguration> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Updates plugin configuration. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// Accepts plugin configuration as JSON body. | ||||
|     /// </remarks> | ||||
|     /// <param name="pluginId">Plugin id.</param> | ||||
|     /// <response code="204">Plugin configuration updated.</response> | ||||
|     /// <response code="404">Plugin not found or plugin does not have configuration.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|     [HttpPost("{pluginId}/Configuration")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId) | ||||
|     { | ||||
|         var plugin = _pluginManager.GetPlugin(pluginId); | ||||
|         if (plugin?.Instance is not IHasPluginConfiguration configPlugin) | ||||
|         { | ||||
|             return NotFound(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets plugin configuration. | ||||
|         /// </summary> | ||||
|         /// <param name="pluginId">Plugin id.</param> | ||||
|         /// <response code="200">Plugin configuration returned.</response> | ||||
|         /// <response code="404">Plugin not found or plugin configuration not found.</response> | ||||
|         /// <returns>Plugin configuration.</returns> | ||||
|         [HttpGet("{pluginId}/Configuration")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<BasePluginConfiguration> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a plugin's image. | ||||
|     /// </summary> | ||||
|     /// <param name="pluginId">Plugin id.</param> | ||||
|     /// <param name="version">Plugin version.</param> | ||||
|     /// <response code="200">Plugin image returned.</response> | ||||
|     /// <returns>Plugin's image.</returns> | ||||
|     [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates plugin configuration. | ||||
|         /// </summary> | ||||
|         /// <remarks> | ||||
|         /// Accepts plugin configuration as JSON body. | ||||
|         /// </remarks> | ||||
|         /// <param name="pluginId">Plugin id.</param> | ||||
|         /// <response code="204">Plugin configuration updated.</response> | ||||
|         /// <response code="404">Plugin not found or plugin does not have configuration.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|         [HttpPost("{pluginId}/Configuration")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a plugin's image. | ||||
|         /// </summary> | ||||
|         /// <param name="pluginId">Plugin id.</param> | ||||
|         /// <param name="version">Plugin version.</param> | ||||
|         /// <response code="200">Plugin image returned.</response> | ||||
|         /// <returns>Plugin's image.</returns> | ||||
|         [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)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a plugin's manifest. | ||||
|         /// </summary> | ||||
|         /// <param name="pluginId">Plugin id.</param> | ||||
|         /// <response code="204">Plugin manifest returned.</response> | ||||
|         /// <response code="404">Plugin not found.</response> | ||||
|         /// <returns>A <see cref="PluginManifest"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|         [HttpPost("{pluginId}/Manifest")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<PluginManifest> 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)); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a plugin's manifest. | ||||
|     /// </summary> | ||||
|     /// <param name="pluginId">Plugin id.</param> | ||||
|     /// <response code="204">Plugin manifest returned.</response> | ||||
|     /// <response code="404">Plugin not found.</response> | ||||
|     /// <returns>A <see cref="PluginManifest"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns> | ||||
|     [HttpPost("{pluginId}/Manifest")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<PluginManifest> GetPluginManifest([FromRoute, Required] Guid pluginId) | ||||
|     { | ||||
|         var plugin = _pluginManager.GetPlugin(pluginId); | ||||
| 
 | ||||
|         if (plugin is not null) | ||||
|         { | ||||
|             return plugin.Manifest; | ||||
|         } | ||||
| 
 | ||||
|         return NotFound(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -13,126 +13,125 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Quick connect controller. | ||||
| /// </summary> | ||||
| public class QuickConnectController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IQuickConnect _quickConnect; | ||||
|     private readonly IAuthorizationContext _authContext; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Quick connect controller. | ||||
|     /// Initializes a new instance of the <see cref="QuickConnectController"/> class. | ||||
|     /// </summary> | ||||
|     public class QuickConnectController : BaseJellyfinApiController | ||||
|     /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param> | ||||
|     /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> | ||||
|     public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext) | ||||
|     { | ||||
|         private readonly IQuickConnect _quickConnect; | ||||
|         private readonly IAuthorizationContext _authContext; | ||||
|         _quickConnect = quickConnect; | ||||
|         _authContext = authContext; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="QuickConnectController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param> | ||||
|         /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> | ||||
|         public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext) | ||||
|     /// <summary> | ||||
|     /// Gets the current quick connect state. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Quick connect state returned.</response> | ||||
|     /// <returns>Whether Quick Connect is enabled on the server or not.</returns> | ||||
|     [HttpGet("Enabled")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<bool> GetQuickConnectEnabled() | ||||
|     { | ||||
|         return _quickConnect.IsEnabled; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initiate a new quick connect request. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Quick connect request successfully created.</response> | ||||
|     /// <response code="401">Quick connect is not active on this server.</response> | ||||
|     /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns> | ||||
|     [HttpPost("Initiate")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<QuickConnectResult>> 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"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Old version of <see cref="InitiateQuickConnect" /> using a GET method. | ||||
|     /// Still available to avoid breaking compatibility. | ||||
|     /// </summary> | ||||
|     /// <returns>The result of <see cref="InitiateQuickConnect" />.</returns> | ||||
|     [Obsolete("Use POST request instead")] | ||||
|     [HttpGet("Initiate")] | ||||
|     [ApiExplorerSettings(IgnoreApi = true)] | ||||
|     public Task<ActionResult<QuickConnectResult>> InitiateQuickConnectLegacy() => InitiateQuickConnect(); | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Attempts to retrieve authentication information. | ||||
|     /// </summary> | ||||
|     /// <param name="secret">Secret previously returned from the Initiate endpoint.</param> | ||||
|     /// <response code="200">Quick connect result returned.</response> | ||||
|     /// <response code="404">Unknown quick connect secret.</response> | ||||
|     /// <returns>An updated <see cref="QuickConnectResult"/>.</returns> | ||||
|     [HttpGet("Connect")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<QuickConnectResult> GetQuickConnectState([FromQuery, Required] string secret) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             return _quickConnect.CheckRequestStatus(secret); | ||||
|         } | ||||
|         catch (ResourceNotFoundException) | ||||
|         { | ||||
|             return NotFound("Unknown secret"); | ||||
|         } | ||||
|         catch (AuthenticationException) | ||||
|         { | ||||
|             return Unauthorized("Quick connect is disabled"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Authorizes a pending quick connect request. | ||||
|     /// </summary> | ||||
|     /// <param name="code">Quick connect code to authorize.</param> | ||||
|     /// <param name="userId">The user the authorize. Access to the requested user is required.</param> | ||||
|     /// <response code="200">Quick connect result authorized successfully.</response> | ||||
|     /// <response code="403">Unknown user id.</response> | ||||
|     /// <returns>Boolean indicating if the authorization was successful.</returns> | ||||
|     [HttpPost("Authorize")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||
|     public async Task<ActionResult<bool>> 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"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the current quick connect state. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Quick connect state returned.</response> | ||||
|         /// <returns>Whether Quick Connect is enabled on the server or not.</returns> | ||||
|         [HttpGet("Enabled")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<bool> GetQuickConnectEnabled() | ||||
|         try | ||||
|         { | ||||
|             return _quickConnect.IsEnabled; | ||||
|             return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initiate a new quick connect request. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Quick connect request successfully created.</response> | ||||
|         /// <response code="401">Quick connect is not active on this server.</response> | ||||
|         /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns> | ||||
|         [HttpPost("Initiate")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect() | ||||
|         catch (AuthenticationException) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); | ||||
|                 return _quickConnect.TryConnect(auth); | ||||
|             } | ||||
|             catch (AuthenticationException) | ||||
|             { | ||||
|                 return Unauthorized("Quick connect is disabled"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Old version of <see cref="InitiateQuickConnect" /> using a GET method. | ||||
|         /// Still available to avoid breaking compatibility. | ||||
|         /// </summary> | ||||
|         /// <returns>The result of <see cref="InitiateQuickConnect" />.</returns> | ||||
|         [Obsolete("Use POST request instead")] | ||||
|         [HttpGet("Initiate")] | ||||
|         [ApiExplorerSettings(IgnoreApi = true)] | ||||
|         public Task<ActionResult<QuickConnectResult>> InitiateQuickConnectLegacy() => InitiateQuickConnect(); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Attempts to retrieve authentication information. | ||||
|         /// </summary> | ||||
|         /// <param name="secret">Secret previously returned from the Initiate endpoint.</param> | ||||
|         /// <response code="200">Quick connect result returned.</response> | ||||
|         /// <response code="404">Unknown quick connect secret.</response> | ||||
|         /// <returns>An updated <see cref="QuickConnectResult"/>.</returns> | ||||
|         [HttpGet("Connect")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<QuickConnectResult> GetQuickConnectState([FromQuery, Required] string secret) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 return _quickConnect.CheckRequestStatus(secret); | ||||
|             } | ||||
|             catch (ResourceNotFoundException) | ||||
|             { | ||||
|                 return NotFound("Unknown secret"); | ||||
|             } | ||||
|             catch (AuthenticationException) | ||||
|             { | ||||
|                 return Unauthorized("Quick connect is disabled"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Authorizes a pending quick connect request. | ||||
|         /// </summary> | ||||
|         /// <param name="code">Quick connect code to authorize.</param> | ||||
|         /// <param name="userId">The user the authorize. Access to the requested user is required.</param> | ||||
|         /// <response code="200">Quick connect result authorized successfully.</response> | ||||
|         /// <response code="403">Unknown user id.</response> | ||||
|         /// <returns>Boolean indicating if the authorization was successful.</returns> | ||||
|         [HttpPost("Authorize")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status403Forbidden)] | ||||
|         public async Task<ActionResult<bool>> 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"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -15,165 +15,164 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Remote Images Controller. | ||||
| /// </summary> | ||||
| [Route("")] | ||||
| public class RemoteImageController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IProviderManager _providerManager; | ||||
|     private readonly IServerApplicationPaths _applicationPaths; | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Remote Images Controller. | ||||
|     /// Initializes a new instance of the <see cref="RemoteImageController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     public class RemoteImageController : BaseJellyfinApiController | ||||
|     /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> | ||||
|     /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="RemoteImageController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> | ||||
|         /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         public RemoteImageController( | ||||
|             IProviderManager providerManager, | ||||
|             IServerApplicationPaths applicationPaths, | ||||
|             ILibraryManager libraryManager) | ||||
|     /// <summary> | ||||
|     /// Gets available remote images for an item. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">Item Id.</param> | ||||
|     /// <param name="type">The image type.</param> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="providerName">Optional. The image provider to use.</param> | ||||
|     /// <param name="includeAllLanguages">Optional. Include all languages.</param> | ||||
|     /// <response code="200">Remote Images returned.</response> | ||||
|     /// <response code="404">Item not found.</response> | ||||
|     /// <returns>Remote Image Result.</returns> | ||||
|     [HttpGet("Items/{itemId}/RemoteImages")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult<RemoteImageResult>> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets available remote images for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item Id.</param> | ||||
|         /// <param name="type">The image type.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="providerName">Optional. The image provider to use.</param> | ||||
|         /// <param name="includeAllLanguages">Optional. Include all languages.</param> | ||||
|         /// <response code="200">Remote Images returned.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>Remote Image Result.</returns> | ||||
|         [HttpGet("Items/{itemId}/RemoteImages")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult<RemoteImageResult>> 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)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets available remote image providers for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item Id.</param> | ||||
|         /// <response code="200">Returned remote image providers.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>List of remote image providers.</returns> | ||||
|         [HttpGet("Items/{itemId}/RemoteImages/Providers")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<IEnumerable<ImageProviderInfo>> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Downloads a remote image for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">Item Id.</param> | ||||
|         /// <param name="type">The image type.</param> | ||||
|         /// <param name="imageUrl">The image url.</param> | ||||
|         /// <response code="204">Remote image downloaded.</response> | ||||
|         /// <response code="404">Remote image not found.</response> | ||||
|         /// <returns>Download status.</returns> | ||||
|         [HttpPost("Items/{itemId}/RemoteImages/Download")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the full cache path. | ||||
|         /// </summary> | ||||
|         /// <param name="filename">The filename.</param> | ||||
|         /// <returns>System.String.</returns> | ||||
|         private string GetFullCachePath(string filename) | ||||
|         result.Images = imageArray; | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets available remote image providers for an item. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">Item Id.</param> | ||||
|     /// <response code="200">Returned remote image providers.</response> | ||||
|     /// <response code="404">Item not found.</response> | ||||
|     /// <returns>List of remote image providers.</returns> | ||||
|     [HttpGet("Items/{itemId}/RemoteImages/Providers")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<IEnumerable<ImageProviderInfo>> 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)); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Downloads a remote image for an item. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">Item Id.</param> | ||||
|     /// <param name="type">The image type.</param> | ||||
|     /// <param name="imageUrl">The image url.</param> | ||||
|     /// <response code="204">Remote image downloaded.</response> | ||||
|     /// <response code="404">Remote image not found.</response> | ||||
|     /// <returns>Download status.</returns> | ||||
|     [HttpPost("Items/{itemId}/RemoteImages/Download")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets the full cache path. | ||||
|     /// </summary> | ||||
|     /// <param name="filename">The filename.</param> | ||||
|     /// <returns>System.String.</returns> | ||||
|     private string GetFullCachePath(string filename) | ||||
|     { | ||||
|         return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -8,154 +8,153 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Scheduled Tasks Controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.RequiresElevation)] | ||||
| public class ScheduledTasksController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ITaskManager _taskManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Scheduled Tasks Controller. | ||||
|     /// Initializes a new instance of the <see cref="ScheduledTasksController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     public class ScheduledTasksController : BaseJellyfinApiController | ||||
|     /// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param> | ||||
|     public ScheduledTasksController(ITaskManager taskManager) | ||||
|     { | ||||
|         private readonly ITaskManager _taskManager; | ||||
|         _taskManager = taskManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ScheduledTasksController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param> | ||||
|         public ScheduledTasksController(ITaskManager taskManager) | ||||
|     /// <summary> | ||||
|     /// Get tasks. | ||||
|     /// </summary> | ||||
|     /// <param name="isHidden">Optional filter tasks that are hidden, or not.</param> | ||||
|     /// <param name="isEnabled">Optional filter tasks that are enabled, or not.</param> | ||||
|     /// <response code="200">Scheduled tasks retrieved.</response> | ||||
|     /// <returns>The list of scheduled tasks.</returns> | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public IEnumerable<TaskInfo> GetTasks( | ||||
|         [FromQuery] bool? isHidden, | ||||
|         [FromQuery] bool? isEnabled) | ||||
|     { | ||||
|         IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name); | ||||
| 
 | ||||
|         foreach (var task in tasks) | ||||
|         { | ||||
|             _taskManager = taskManager; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get tasks. | ||||
|         /// </summary> | ||||
|         /// <param name="isHidden">Optional filter tasks that are hidden, or not.</param> | ||||
|         /// <param name="isEnabled">Optional filter tasks that are enabled, or not.</param> | ||||
|         /// <response code="200">Scheduled tasks retrieved.</response> | ||||
|         /// <returns>The list of scheduled tasks.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public IEnumerable<TaskInfo> GetTasks( | ||||
|             [FromQuery] bool? isHidden, | ||||
|             [FromQuery] bool? isEnabled) | ||||
|         { | ||||
|             IEnumerable<IScheduledTaskWorker> 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); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get task by id. | ||||
|         /// </summary> | ||||
|         /// <param name="taskId">Task Id.</param> | ||||
|         /// <response code="200">Task retrieved.</response> | ||||
|         /// <response code="404">Task not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the task on success, or a <see cref="NotFoundResult"/> if the task could not be found.</returns> | ||||
|         [HttpGet("{taskId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<TaskInfo> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Start specified task. | ||||
|         /// </summary> | ||||
|         /// <param name="taskId">Task Id.</param> | ||||
|         /// <response code="204">Task started.</response> | ||||
|         /// <response code="404">Task not found.</response> | ||||
|         /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Stop specified task. | ||||
|         /// </summary> | ||||
|         /// <param name="taskId">Task Id.</param> | ||||
|         /// <response code="204">Task stopped.</response> | ||||
|         /// <response code="404">Task not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Update specified task triggers. | ||||
|         /// </summary> | ||||
|         /// <param name="taskId">Task Id.</param> | ||||
|         /// <param name="triggerInfos">Triggers.</param> | ||||
|         /// <response code="204">Task triggers updated.</response> | ||||
|         /// <response code="404">Task not found.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns> | ||||
|         [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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get task by id. | ||||
|     /// </summary> | ||||
|     /// <param name="taskId">Task Id.</param> | ||||
|     /// <response code="200">Task retrieved.</response> | ||||
|     /// <response code="404">Task not found.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the task on success, or a <see cref="NotFoundResult"/> if the task could not be found.</returns> | ||||
|     [HttpGet("{taskId}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<TaskInfo> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Start specified task. | ||||
|     /// </summary> | ||||
|     /// <param name="taskId">Task Id.</param> | ||||
|     /// <response code="204">Task started.</response> | ||||
|     /// <response code="404">Task not found.</response> | ||||
|     /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Stop specified task. | ||||
|     /// </summary> | ||||
|     /// <param name="taskId">Task Id.</param> | ||||
|     /// <response code="204">Task stopped.</response> | ||||
|     /// <response code="404">Task not found.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Update specified task triggers. | ||||
|     /// </summary> | ||||
|     /// <param name="taskId">Task Id.</param> | ||||
|     /// <param name="triggerInfos">Triggers.</param> | ||||
|     /// <response code="204">Task triggers updated.</response> | ||||
|     /// <response code="404">Task not found.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -20,247 +20,246 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Search controller. | ||||
| /// </summary> | ||||
| [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; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Search controller. | ||||
|     /// Initializes a new instance of the <see cref="SearchController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("Search/Hints")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class SearchController : BaseJellyfinApiController | ||||
|     /// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param> | ||||
|     /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SearchController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param> | ||||
|         public SearchController( | ||||
|             ISearchEngine searchEngine, | ||||
|             ILibraryManager libraryManager, | ||||
|             IDtoService dtoService, | ||||
|             IImageProcessor imageProcessor) | ||||
|     /// <summary> | ||||
|     /// Gets the search hint result. | ||||
|     /// </summary> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param> | ||||
|     /// <param name="searchTerm">The search term to filter on.</param> | ||||
|     /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="parentId">If specified, only children of the parent are returned.</param> | ||||
|     /// <param name="isMovie">Optional filter for movies.</param> | ||||
|     /// <param name="isSeries">Optional filter for series.</param> | ||||
|     /// <param name="isNews">Optional filter for news.</param> | ||||
|     /// <param name="isKids">Optional filter for kids.</param> | ||||
|     /// <param name="isSports">Optional filter for sports.</param> | ||||
|     /// <param name="includePeople">Optional filter whether to include people.</param> | ||||
|     /// <param name="includeMedia">Optional filter whether to include media.</param> | ||||
|     /// <param name="includeGenres">Optional filter whether to include genres.</param> | ||||
|     /// <param name="includeStudios">Optional filter whether to include studios.</param> | ||||
|     /// <param name="includeArtists">Optional filter whether to include artists.</param> | ||||
|     /// <response code="200">Search hint returned.</response> | ||||
|     /// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns> | ||||
|     [HttpGet] | ||||
|     [Description("Gets search hints based on a search term")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<SearchHintResult> 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, | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the search hint result. | ||||
|         /// </summary> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param> | ||||
|         /// <param name="searchTerm">The search term to filter on.</param> | ||||
|         /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="parentId">If specified, only children of the parent are returned.</param> | ||||
|         /// <param name="isMovie">Optional filter for movies.</param> | ||||
|         /// <param name="isSeries">Optional filter for series.</param> | ||||
|         /// <param name="isNews">Optional filter for news.</param> | ||||
|         /// <param name="isKids">Optional filter for kids.</param> | ||||
|         /// <param name="isSports">Optional filter for sports.</param> | ||||
|         /// <param name="includePeople">Optional filter whether to include people.</param> | ||||
|         /// <param name="includeMedia">Optional filter whether to include media.</param> | ||||
|         /// <param name="includeGenres">Optional filter whether to include genres.</param> | ||||
|         /// <param name="includeStudios">Optional filter whether to include studios.</param> | ||||
|         /// <param name="includeArtists">Optional filter whether to include artists.</param> | ||||
|         /// <response code="200">Search hint returned.</response> | ||||
|         /// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns> | ||||
|         [HttpGet] | ||||
|         [Description("Gets search hints based on a search term")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<SearchHintResult> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets the search hint result. | ||||
|     /// </summary> | ||||
|     /// <param name="hintInfo">The hint info.</param> | ||||
|     /// <returns>SearchHintResult.</returns> | ||||
|     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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the search hint result. | ||||
|         /// </summary> | ||||
|         /// <param name="hintInfo">The hint info.</param> | ||||
|         /// <returns>SearchHintResult.</returns> | ||||
|         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<Series>(item, ImageType.Thumb); | ||||
|             } | ||||
|         SetThumbImageInfo(result, item); | ||||
|         SetBackdropImageInfo(result, item); | ||||
| 
 | ||||
|             itemWithImage ??= GetParentWithImage<BaseItem>(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<BaseItem>(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<T>(BaseItem item, ImageType type) | ||||
|             where T : BaseItem | ||||
|         if (!item.ChannelId.Equals(default)) | ||||
|         { | ||||
|             return item.GetParents().OfType<T>().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<Series>(item, ImageType.Thumb); | ||||
|         } | ||||
| 
 | ||||
|         itemWithImage ??= GetParentWithImage<BaseItem>(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<BaseItem>(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<T>(BaseItem item, ImageType type) | ||||
|         where T : BaseItem | ||||
|     { | ||||
|         return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -19,480 +19,479 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The session controller. | ||||
| /// </summary> | ||||
| [Route("")] | ||||
| public class SessionController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ISessionManager _sessionManager; | ||||
|     private readonly IUserManager _userManager; | ||||
|     private readonly IDeviceManager _deviceManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The session controller. | ||||
|     /// Initializes a new instance of the <see cref="SessionController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     public class SessionController : BaseJellyfinApiController | ||||
|     /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SessionController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param> | ||||
|         public SessionController( | ||||
|             ISessionManager sessionManager, | ||||
|             IUserManager userManager, | ||||
|             IDeviceManager deviceManager) | ||||
|     /// <summary> | ||||
|     /// Gets a list of sessions. | ||||
|     /// </summary> | ||||
|     /// <param name="controllableByUserId">Filter by sessions that a given user is allowed to remote control.</param> | ||||
|     /// <param name="deviceId">Filter by device Id.</param> | ||||
|     /// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param> | ||||
|     /// <response code="200">List of sessions returned.</response> | ||||
|     /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns> | ||||
|     [HttpGet("Sessions")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<SessionInfo>> 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)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of sessions. | ||||
|         /// </summary> | ||||
|         /// <param name="controllableByUserId">Filter by sessions that a given user is allowed to remote control.</param> | ||||
|         /// <param name="deviceId">Filter by device Id.</param> | ||||
|         /// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param> | ||||
|         /// <response code="200">List of sessions returned.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns> | ||||
|         [HttpGet("Sessions")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<SessionInfo>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Instructs a session to browse to an item or view. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session Id.</param> | ||||
|         /// <param name="itemType">The type of item to browse to.</param> | ||||
|         /// <param name="itemId">The Id of the item.</param> | ||||
|         /// <param name="itemName">The name of the item.</param> | ||||
|         /// <response code="204">Instruction sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/{sessionId}/Viewing")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Instructs a session to play an item. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param> | ||||
|         /// <param name="itemIds">The ids of the items to play, comma delimited.</param> | ||||
|         /// <param name="startPositionTicks">The starting position of the first item.</param> | ||||
|         /// <param name="mediaSourceId">Optional. The media source id.</param> | ||||
|         /// <param name="audioStreamIndex">Optional. The index of the audio stream to play.</param> | ||||
|         /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to play.</param> | ||||
|         /// <param name="startIndex">Optional. The start index.</param> | ||||
|         /// <response code="204">Instruction sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/{sessionId}/Playing")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Issues a playstate command to a client. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="command">The <see cref="PlaystateCommand"/>.</param> | ||||
|         /// <param name="seekPositionTicks">The optional position ticks.</param> | ||||
|         /// <param name="controllingUserId">The optional controlling user id.</param> | ||||
|         /// <response code="204">Playstate command sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/{sessionId}/Playing/{command}")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Issues a system command to a client. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="command">The command to send.</param> | ||||
|         /// <response code="204">System command sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/{sessionId}/System/{command}")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Issues a general command to a client. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="command">The command to send.</param> | ||||
|         /// <response code="204">General command sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/{sessionId}/Command/{command}")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Issues a full general command to a client. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="command">The <see cref="GeneralCommand"/>.</param> | ||||
|         /// <response code="204">Full general command sent to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/{sessionId}/Command")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Issues a command to a client to display a message to the user. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="command">The <see cref="MessageCommand" /> object containing Header, Message Text, and TimeoutMs.</param> | ||||
|         /// <response code="204">Message sent.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/{sessionId}/Message")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Adds an additional user to a session. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <response code="204">User added to session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Removes an additional user from a session. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <response code="204">User removed from session.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates capabilities for a device. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The session id.</param> | ||||
|         /// <param name="playableMediaTypes">A list of playable media types, comma delimited. Audio, Video, Book, Photo.</param> | ||||
|         /// <param name="supportedCommands">A list of supported remote control commands, comma delimited.</param> | ||||
|         /// <param name="supportsMediaControl">Determines whether media can be played remotely..</param> | ||||
|         /// <param name="supportsSync">Determines whether sync is supported.</param> | ||||
|         /// <param name="supportsPersistentIdentifier">Determines whether the device supports a unique identifier.</param> | ||||
|         /// <response code="204">Capabilities posted.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/Capabilities")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates capabilities for a device. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The session id.</param> | ||||
|         /// <param name="capabilities">The <see cref="ClientCapabilities"/>.</param> | ||||
|         /// <response code="204">Capabilities updated.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/Capabilities/Full")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> PostFullCapabilities( | ||||
|             [FromQuery] string? id, | ||||
|             [FromBody, Required] ClientCapabilitiesDto capabilities) | ||||
|         return Ok(result); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Instructs a session to browse to an item or view. | ||||
|     /// </summary> | ||||
|     /// <param name="sessionId">The session Id.</param> | ||||
|     /// <param name="itemType">The type of item to browse to.</param> | ||||
|     /// <param name="itemId">The Id of the item.</param> | ||||
|     /// <param name="itemName">The name of the item.</param> | ||||
|     /// <response code="204">Instruction sent to session.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/{sessionId}/Viewing")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Instructs a session to play an item. | ||||
|     /// </summary> | ||||
|     /// <param name="sessionId">The session id.</param> | ||||
|     /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param> | ||||
|     /// <param name="itemIds">The ids of the items to play, comma delimited.</param> | ||||
|     /// <param name="startPositionTicks">The starting position of the first item.</param> | ||||
|     /// <param name="mediaSourceId">Optional. The media source id.</param> | ||||
|     /// <param name="audioStreamIndex">Optional. The index of the audio stream to play.</param> | ||||
|     /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to play.</param> | ||||
|     /// <param name="startIndex">Optional. The start index.</param> | ||||
|     /// <response code="204">Instruction sent to session.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/{sessionId}/Playing")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Issues a playstate command to a client. | ||||
|     /// </summary> | ||||
|     /// <param name="sessionId">The session id.</param> | ||||
|     /// <param name="command">The <see cref="PlaystateCommand"/>.</param> | ||||
|     /// <param name="seekPositionTicks">The optional position ticks.</param> | ||||
|     /// <param name="controllingUserId">The optional controlling user id.</param> | ||||
|     /// <response code="204">Playstate command sent to session.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/{sessionId}/Playing/{command}")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Reports that a session is viewing an item. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionId">The session id.</param> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <response code="204">Session reported to server.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/Viewing")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> ReportViewing( | ||||
|             [FromQuery] string? sessionId, | ||||
|             [FromQuery, Required] string? itemId) | ||||
|     /// <summary> | ||||
|     /// Issues a system command to a client. | ||||
|     /// </summary> | ||||
|     /// <param name="sessionId">The session id.</param> | ||||
|     /// <param name="command">The command to send.</param> | ||||
|     /// <response code="204">System command sent to session.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/{sessionId}/System/{command}")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Reports that a session has ended. | ||||
|         /// </summary> | ||||
|         /// <response code="204">Session end reported to server.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Sessions/Logout")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> ReportSessionEnded() | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Issues a general command to a client. | ||||
|     /// </summary> | ||||
|     /// <param name="sessionId">The session id.</param> | ||||
|     /// <param name="command">The command to send.</param> | ||||
|     /// <response code="204">General command sent to session.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/{sessionId}/Command/{command}")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Issues a full general command to a client. | ||||
|     /// </summary> | ||||
|     /// <param name="sessionId">The session id.</param> | ||||
|     /// <param name="command">The <see cref="GeneralCommand"/>.</param> | ||||
|     /// <response code="204">Full general command sent to session.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/{sessionId}/Command")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Issues a command to a client to display a message to the user. | ||||
|     /// </summary> | ||||
|     /// <param name="sessionId">The session id.</param> | ||||
|     /// <param name="command">The <see cref="MessageCommand" /> object containing Header, Message Text, and TimeoutMs.</param> | ||||
|     /// <response code="204">Message sent.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/{sessionId}/Message")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> SendMessageCommand( | ||||
|         [FromRoute, Required] string sessionId, | ||||
|         [FromBody, Required] MessageCommand command) | ||||
|     { | ||||
|         if (string.IsNullOrWhiteSpace(command.Header)) | ||||
|         { | ||||
|             command.Header = "Message from Server"; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get all auth providers. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Auth providers retrieved.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns> | ||||
|         [HttpGet("Auth/Providers")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders() | ||||
|         await _sessionManager.SendMessageCommand( | ||||
|             await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false), | ||||
|             sessionId, | ||||
|             command, | ||||
|             CancellationToken.None) | ||||
|             .ConfigureAwait(false); | ||||
| 
 | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Adds an additional user to a session. | ||||
|     /// </summary> | ||||
|     /// <param name="sessionId">The session id.</param> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <response code="204">User added to session.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Removes an additional user from a session. | ||||
|     /// </summary> | ||||
|     /// <param name="sessionId">The session id.</param> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <response code="204">User removed from session.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Updates capabilities for a device. | ||||
|     /// </summary> | ||||
|     /// <param name="id">The session id.</param> | ||||
|     /// <param name="playableMediaTypes">A list of playable media types, comma delimited. Audio, Video, Book, Photo.</param> | ||||
|     /// <param name="supportedCommands">A list of supported remote control commands, comma delimited.</param> | ||||
|     /// <param name="supportsMediaControl">Determines whether media can be played remotely..</param> | ||||
|     /// <param name="supportsSync">Determines whether sync is supported.</param> | ||||
|     /// <param name="supportsPersistentIdentifier">Determines whether the device supports a unique identifier.</param> | ||||
|     /// <response code="204">Capabilities posted.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/Capabilities")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get all password reset providers. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Password reset providers retrieved.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns> | ||||
|         [HttpGet("Auth/PasswordResetProviders")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders() | ||||
|         _sessionManager.ReportCapabilities(id, new ClientCapabilities | ||||
|         { | ||||
|             return _userManager.GetPasswordResetProviders(); | ||||
|             PlayableMediaTypes = playableMediaTypes, | ||||
|             SupportedCommands = supportedCommands, | ||||
|             SupportsMediaControl = supportsMediaControl, | ||||
|             SupportsSync = supportsSync, | ||||
|             SupportsPersistentIdentifier = supportsPersistentIdentifier | ||||
|         }); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Updates capabilities for a device. | ||||
|     /// </summary> | ||||
|     /// <param name="id">The session id.</param> | ||||
|     /// <param name="capabilities">The <see cref="ClientCapabilities"/>.</param> | ||||
|     /// <response code="204">Capabilities updated.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/Capabilities/Full")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Reports that a session is viewing an item. | ||||
|     /// </summary> | ||||
|     /// <param name="sessionId">The session id.</param> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <response code="204">Session reported to server.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/Viewing")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Reports that a session has ended. | ||||
|     /// </summary> | ||||
|     /// <response code="204">Session end reported to server.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Sessions/Logout")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> ReportSessionEnded() | ||||
|     { | ||||
|         await _sessionManager.Logout(User.GetToken()).ConfigureAwait(false); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get all auth providers. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Auth providers retrieved.</response> | ||||
|     /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns> | ||||
|     [HttpGet("Auth/Providers")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders() | ||||
|     { | ||||
|         return _userManager.GetAuthenticationProviders(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get all password reset providers. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Password reset providers retrieved.</response> | ||||
|     /// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns> | ||||
|     [HttpGet("Auth/PasswordResetProviders")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders() | ||||
|     { | ||||
|         return _userManager.GetPasswordResetProviders(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -10,141 +10,140 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The startup wizard controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] | ||||
| public class StartupController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IServerConfigurationManager _config; | ||||
|     private readonly IUserManager _userManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The startup wizard controller. | ||||
|     /// Initializes a new instance of the <see cref="StartupController" /> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] | ||||
|     public class StartupController : BaseJellyfinApiController | ||||
|     /// <param name="config">The server configuration manager.</param> | ||||
|     /// <param name="userManager">The user manager.</param> | ||||
|     public StartupController(IServerConfigurationManager config, IUserManager userManager) | ||||
|     { | ||||
|         private readonly IServerConfigurationManager _config; | ||||
|         private readonly IUserManager _userManager; | ||||
|         _config = config; | ||||
|         _userManager = userManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="StartupController" /> class. | ||||
|         /// </summary> | ||||
|         /// <param name="config">The server configuration manager.</param> | ||||
|         /// <param name="userManager">The user manager.</param> | ||||
|         public StartupController(IServerConfigurationManager config, IUserManager userManager) | ||||
|     /// <summary> | ||||
|     /// Completes the startup wizard. | ||||
|     /// </summary> | ||||
|     /// <response code="204">Startup wizard completed.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("Complete")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public ActionResult CompleteWizard() | ||||
|     { | ||||
|         _config.Configuration.IsStartupWizardCompleted = true; | ||||
|         _config.SaveConfiguration(); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets the initial startup wizard configuration. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Initial startup wizard configuration retrieved.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the initial startup wizard configuration.</returns> | ||||
|     [HttpGet("Configuration")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<StartupConfigurationDto> GetStartupConfiguration() | ||||
|     { | ||||
|         return new StartupConfigurationDto | ||||
|         { | ||||
|             _config = config; | ||||
|             _userManager = userManager; | ||||
|             UICulture = _config.Configuration.UICulture, | ||||
|             MetadataCountryCode = _config.Configuration.MetadataCountryCode, | ||||
|             PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Sets the initial startup wizard configuration. | ||||
|     /// </summary> | ||||
|     /// <param name="startupConfiguration">The updated startup configuration.</param> | ||||
|     /// <response code="204">Configuration saved.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Sets remote access and UPnP. | ||||
|     /// </summary> | ||||
|     /// <param name="startupRemoteAccessDto">The startup remote access dto.</param> | ||||
|     /// <response code="204">Configuration saved.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets the first user. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Initial user retrieved.</response> | ||||
|     /// <returns>The first user.</returns> | ||||
|     [HttpGet("User")] | ||||
|     [HttpGet("FirstUser", Name = "GetFirstUser_2")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<StartupUserDto> 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 | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Sets the user name and password. | ||||
|     /// </summary> | ||||
|     /// <param name="startupUserDto">The DTO containing username and password.</param> | ||||
|     /// <response code="204">Updated user name and password.</response> | ||||
|     /// <returns> | ||||
|     /// A <see cref="Task" /> that represents the asynchronous update operation. | ||||
|     /// The task result contains a <see cref="NoContentResult"/> indicating success. | ||||
|     /// </returns> | ||||
|     [HttpPost("User")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto) | ||||
|     { | ||||
|         var user = _userManager.Users.First(); | ||||
| 
 | ||||
|         if (startupUserDto.Name is not null) | ||||
|         { | ||||
|             user.Username = startupUserDto.Name; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Completes the startup wizard. | ||||
|         /// </summary> | ||||
|         /// <response code="204">Startup wizard completed.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the initial startup wizard configuration. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Initial startup wizard configuration retrieved.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the initial startup wizard configuration.</returns> | ||||
|         [HttpGet("Configuration")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<StartupConfigurationDto> GetStartupConfiguration() | ||||
|         { | ||||
|             return new StartupConfigurationDto | ||||
|             { | ||||
|                 UICulture = _config.Configuration.UICulture, | ||||
|                 MetadataCountryCode = _config.Configuration.MetadataCountryCode, | ||||
|                 PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Sets the initial startup wizard configuration. | ||||
|         /// </summary> | ||||
|         /// <param name="startupConfiguration">The updated startup configuration.</param> | ||||
|         /// <response code="204">Configuration saved.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Sets remote access and UPnP. | ||||
|         /// </summary> | ||||
|         /// <param name="startupRemoteAccessDto">The startup remote access dto.</param> | ||||
|         /// <response code="204">Configuration saved.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the first user. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Initial user retrieved.</response> | ||||
|         /// <returns>The first user.</returns> | ||||
|         [HttpGet("User")] | ||||
|         [HttpGet("FirstUser", Name = "GetFirstUser_2")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<StartupUserDto> 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 | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Sets the user name and password. | ||||
|         /// </summary> | ||||
|         /// <param name="startupUserDto">The DTO containing username and password.</param> | ||||
|         /// <response code="204">Updated user name and password.</response> | ||||
|         /// <returns> | ||||
|         /// A <see cref="Task" /> that represents the asynchronous update operation. | ||||
|         /// The task result contains a <see cref="NoContentResult"/> indicating success. | ||||
|         /// </returns> | ||||
|         [HttpPost("User")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -16,141 +16,140 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Studios controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class StudiosController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly IUserManager _userManager; | ||||
|     private readonly IDtoService _dtoService; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Studios controller. | ||||
|     /// Initializes a new instance of the <see cref="StudiosController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class StudiosController : BaseJellyfinApiController | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="StudiosController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         public StudiosController( | ||||
|             ILibraryManager libraryManager, | ||||
|             IUserManager userManager, | ||||
|             IDtoService dtoService) | ||||
|     /// <summary> | ||||
|     /// Gets all studios from a given item, folder, or the entire library. | ||||
|     /// </summary> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="searchTerm">Optional. Search term.</param> | ||||
|     /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|     /// <param name="enableUserData">Optional, include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|     /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|     /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|     /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|     /// <param name="enableTotalRecordCount">Total record count.</param> | ||||
|     /// <response code="200">Studios returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the studios.</returns> | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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 | ||||
|         }; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets all studios from a given item, folder, or the entire library. | ||||
|         /// </summary> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="searchTerm">Optional. Search term.</param> | ||||
|         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|         /// <param name="enableUserData">Optional, include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|         /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|         /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|         /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|         /// <param name="enableTotalRecordCount">Total record count.</param> | ||||
|         /// <response code="200">Studios returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the studios.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a studio by name. | ||||
|         /// </summary> | ||||
|         /// <param name="name">Studio name.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <response code="200">Studio returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the studio.</returns> | ||||
|         [HttpGet("{name}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<BaseItemDto> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a studio by name. | ||||
|     /// </summary> | ||||
|     /// <param name="name">Studio name.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <response code="200">Studio returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the studio.</returns> | ||||
|     [HttpGet("{name}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<BaseItemDto> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -30,522 +30,521 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Subtitle controller. | ||||
| /// </summary> | ||||
| [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<SubtitleController> _logger; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Subtitle controller. | ||||
|     /// Initializes a new instance of the <see cref="SubtitleController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     public class SubtitleController : BaseJellyfinApiController | ||||
|     /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param> | ||||
|     /// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param> | ||||
|     /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param> | ||||
|     /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param> | ||||
|     /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> | ||||
|     /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param> | ||||
|     public SubtitleController( | ||||
|         IServerConfigurationManager serverConfigurationManager, | ||||
|         ILibraryManager libraryManager, | ||||
|         ISubtitleManager subtitleManager, | ||||
|         ISubtitleEncoder subtitleEncoder, | ||||
|         IMediaSourceManager mediaSourceManager, | ||||
|         IProviderManager providerManager, | ||||
|         IFileSystem fileSystem, | ||||
|         ILogger<SubtitleController> 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<SubtitleController> _logger; | ||||
|         _serverConfigurationManager = serverConfigurationManager; | ||||
|         _libraryManager = libraryManager; | ||||
|         _subtitleManager = subtitleManager; | ||||
|         _subtitleEncoder = subtitleEncoder; | ||||
|         _mediaSourceManager = mediaSourceManager; | ||||
|         _providerManager = providerManager; | ||||
|         _fileSystem = fileSystem; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SubtitleController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param> | ||||
|         /// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param> | ||||
|         /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param> | ||||
|         /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param> | ||||
|         /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> | ||||
|         /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param> | ||||
|         public SubtitleController( | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             ILibraryManager libraryManager, | ||||
|             ISubtitleManager subtitleManager, | ||||
|             ISubtitleEncoder subtitleEncoder, | ||||
|             IMediaSourceManager mediaSourceManager, | ||||
|             IProviderManager providerManager, | ||||
|             IFileSystem fileSystem, | ||||
|             ILogger<SubtitleController> logger) | ||||
|     /// <summary> | ||||
|     /// Deletes an external subtitle file. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="index">The index of the subtitle file.</param> | ||||
|     /// <response code="204">Subtitle deleted.</response> | ||||
|     /// <response code="404">Item not found.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpDelete("Videos/{itemId}/Subtitles/{index}")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<Task> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Deletes an external subtitle file. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="index">The index of the subtitle file.</param> | ||||
|         /// <response code="204">Subtitle deleted.</response> | ||||
|         /// <response code="404">Item not found.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpDelete("Videos/{itemId}/Subtitles/{index}")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<Task> DeleteSubtitle( | ||||
|             [FromRoute, Required] Guid itemId, | ||||
|             [FromRoute, Required] int index) | ||||
|         _subtitleManager.DeleteSubtitles(item, index); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Search remote subtitles. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="language">The language of the subtitles.</param> | ||||
|     /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param> | ||||
|     /// <response code="200">Subtitles retrieved.</response> | ||||
|     /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns> | ||||
|     [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Downloads a remote subtitle. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="subtitleId">The subtitle id.</param> | ||||
|     /// <response code="204">Subtitle downloaded.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Search remote subtitles. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="language">The language of the subtitles.</param> | ||||
|         /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param> | ||||
|         /// <response code="200">Subtitles retrieved.</response> | ||||
|         /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns> | ||||
|         [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> 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); | ||||
|     /// <summary> | ||||
|     /// Gets the remote subtitles. | ||||
|     /// </summary> | ||||
|     /// <param name="id">The item id.</param> | ||||
|     /// <response code="200">File returned.</response> | ||||
|     /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns> | ||||
|     [HttpGet("Providers/Subtitles/Subtitles/{id}")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [Produces(MediaTypeNames.Application.Octet)] | ||||
|     [ProducesFile("text/*")] | ||||
|     public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string id) | ||||
|     { | ||||
|         var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false); | ||||
| 
 | ||||
|         return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format)); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets subtitles in a specified format. | ||||
|     /// </summary> | ||||
|     /// <param name="routeItemId">The (route) item id.</param> | ||||
|     /// <param name="routeMediaSourceId">The (route) media source id.</param> | ||||
|     /// <param name="routeIndex">The (route) subtitle stream index.</param> | ||||
|     /// <param name="routeFormat">The (route) format of the returned subtitle.</param> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="mediaSourceId">The media source id.</param> | ||||
|     /// <param name="index">The subtitle stream index.</param> | ||||
|     /// <param name="format">The format of the returned subtitle.</param> | ||||
|     /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param> | ||||
|     /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param> | ||||
|     /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param> | ||||
|     /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param> | ||||
|     /// <response code="200">File returned.</response> | ||||
|     /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns> | ||||
|     [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesFile("text/*")] | ||||
|     public async Task<ActionResult> 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"; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Downloads a remote subtitle. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="subtitleId">The subtitle id.</param> | ||||
|         /// <response code="204">Subtitle downloaded.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the remote subtitles. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The item id.</param> | ||||
|         /// <response code="200">File returned.</response> | ||||
|         /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns> | ||||
|         [HttpGet("Providers/Subtitles/Subtitles/{id}")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [Produces(MediaTypeNames.Application.Octet)] | ||||
|         [ProducesFile("text/*")] | ||||
|         public async Task<ActionResult> 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)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets subtitles in a specified format. | ||||
|         /// </summary> | ||||
|         /// <param name="routeItemId">The (route) item id.</param> | ||||
|         /// <param name="routeMediaSourceId">The (route) media source id.</param> | ||||
|         /// <param name="routeIndex">The (route) subtitle stream index.</param> | ||||
|         /// <param name="routeFormat">The (route) format of the returned subtitle.</param> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="mediaSourceId">The media source id.</param> | ||||
|         /// <param name="index">The subtitle stream index.</param> | ||||
|         /// <param name="format">The format of the returned subtitle.</param> | ||||
|         /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param> | ||||
|         /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param> | ||||
|         /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param> | ||||
|         /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param> | ||||
|         /// <response code="200">File returned.</response> | ||||
|         /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns> | ||||
|         [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesFile("text/*")] | ||||
|         public async Task<ActionResult> 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)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets subtitles in a specified format. | ||||
|         /// </summary> | ||||
|         /// <param name="routeItemId">The (route) item id.</param> | ||||
|         /// <param name="routeMediaSourceId">The (route) media source id.</param> | ||||
|         /// <param name="routeIndex">The (route) subtitle stream index.</param> | ||||
|         /// <param name="routeStartPositionTicks">The (route) start position of the subtitle in ticks.</param> | ||||
|         /// <param name="routeFormat">The (route) format of the returned subtitle.</param> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="mediaSourceId">The media source id.</param> | ||||
|         /// <param name="index">The subtitle stream index.</param> | ||||
|         /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param> | ||||
|         /// <param name="format">The format of the returned subtitle.</param> | ||||
|         /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param> | ||||
|         /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param> | ||||
|         /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param> | ||||
|         /// <response code="200">File returned.</response> | ||||
|         /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns> | ||||
|         [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesFile("text/*")] | ||||
|         public Task<ActionResult> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an HLS subtitle playlist. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="index">The subtitle stream index.</param> | ||||
|         /// <param name="mediaSourceId">The media source id.</param> | ||||
|         /// <param name="segmentLength">The subtitle segment length.</param> | ||||
|         /// <response code="200">Subtitle playlist retrieved.</response> | ||||
|         /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns> | ||||
|         [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<ActionResult> 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")); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Upload an external subtitle file. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item the subtitle belongs to.</param> | ||||
|         /// <param name="body">The request body.</param> | ||||
|         /// <response code="204">Subtitle uploaded.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|         [HttpPost("Videos/{itemId}/Subtitles")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Encodes a subtitle in the specified format. | ||||
|         /// </summary> | ||||
|         /// <param name="id">The media id.</param> | ||||
|         /// <param name="mediaSourceId">The source media id.</param> | ||||
|         /// <param name="index">The subtitle index.</param> | ||||
|         /// <param name="format">The format to convert to.</param> | ||||
|         /// <param name="startPositionTicks">The start position in ticks.</param> | ||||
|         /// <param name="endPositionTicks">The end position in ticks.</param> | ||||
|         /// <param name="copyTimestamps">Whether to copy the timestamps.</param> | ||||
|         /// <returns>A <see cref="Task{Stream}"/> with the new subtitle file.</returns> | ||||
|         private Task<Stream> 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)); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets subtitles in a specified format. | ||||
|     /// </summary> | ||||
|     /// <param name="routeItemId">The (route) item id.</param> | ||||
|     /// <param name="routeMediaSourceId">The (route) media source id.</param> | ||||
|     /// <param name="routeIndex">The (route) subtitle stream index.</param> | ||||
|     /// <param name="routeStartPositionTicks">The (route) start position of the subtitle in ticks.</param> | ||||
|     /// <param name="routeFormat">The (route) format of the returned subtitle.</param> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="mediaSourceId">The media source id.</param> | ||||
|     /// <param name="index">The subtitle stream index.</param> | ||||
|     /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param> | ||||
|     /// <param name="format">The format of the returned subtitle.</param> | ||||
|     /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param> | ||||
|     /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param> | ||||
|     /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param> | ||||
|     /// <response code="200">File returned.</response> | ||||
|     /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns> | ||||
|     [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesFile("text/*")] | ||||
|     public Task<ActionResult> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets an HLS subtitle playlist. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="index">The subtitle stream index.</param> | ||||
|     /// <param name="mediaSourceId">The media source id.</param> | ||||
|     /// <param name="segmentLength">The subtitle segment length.</param> | ||||
|     /// <response code="200">Subtitle playlist retrieved.</response> | ||||
|     /// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns> | ||||
|     [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<ActionResult> 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."); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of available fallback font files. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns>An array of <see cref="FontFile"/> with the available font files.</returns> | ||||
|         [HttpGet("FallbackFont/Fonts")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public IEnumerable<FontFile> 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)"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a fallback font file. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the fallback font file to get.</param> | ||||
|         /// <response code="200">Fallback font file retrieved.</response> | ||||
|         /// <returns>The fallback font file.</returns> | ||||
|         [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")); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Upload an external subtitle file. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item the subtitle belongs to.</param> | ||||
|     /// <param name="body">The request body.</param> | ||||
|     /// <response code="204">Subtitle uploaded.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/>.</returns> | ||||
|     [HttpPost("Videos/{itemId}/Subtitles")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Encodes a subtitle in the specified format. | ||||
|     /// </summary> | ||||
|     /// <param name="id">The media id.</param> | ||||
|     /// <param name="mediaSourceId">The source media id.</param> | ||||
|     /// <param name="index">The subtitle index.</param> | ||||
|     /// <param name="format">The format to convert to.</param> | ||||
|     /// <param name="startPositionTicks">The start position in ticks.</param> | ||||
|     /// <param name="endPositionTicks">The end position in ticks.</param> | ||||
|     /// <param name="copyTimestamps">Whether to copy the timestamps.</param> | ||||
|     /// <returns>A <see cref="Task{Stream}"/> with the new subtitle file.</returns> | ||||
|     private Task<Stream> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a list of available fallback font files. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Information retrieved.</response> | ||||
|     /// <returns>An array of <see cref="FontFile"/> with the available font files.</returns> | ||||
|     [HttpGet("FallbackFont/Fonts")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public IEnumerable<FontFile> 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; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a fallback font file. | ||||
|     /// </summary> | ||||
|     /// <param name="name">The name of the fallback font file to get.</param> | ||||
|     /// <response code="200">Fallback font file retrieved.</response> | ||||
|     /// <returns>The fallback font file.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -13,80 +13,79 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The suggestions controller. | ||||
| /// </summary> | ||||
| [Route("")] | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class SuggestionsController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IDtoService _dtoService; | ||||
|     private readonly IUserManager _userManager; | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The suggestions controller. | ||||
|     /// Initializes a new instance of the <see cref="SuggestionsController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class SuggestionsController : BaseJellyfinApiController | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SuggestionsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         public SuggestionsController( | ||||
|             IDtoService dtoService, | ||||
|             IUserManager userManager, | ||||
|             ILibraryManager libraryManager) | ||||
|     /// <summary> | ||||
|     /// Gets suggestions. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <param name="mediaType">The media types.</param> | ||||
|     /// <param name="type">The type.</param> | ||||
|     /// <param name="startIndex">Optional. The start index.</param> | ||||
|     /// <param name="limit">Optional. The limit.</param> | ||||
|     /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param> | ||||
|     /// <response code="200">Suggestions returned.</response> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns> | ||||
|     [HttpGet("Users/{userId}/Suggestions")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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 | ||||
|         }); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets suggestions. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="mediaType">The media types.</param> | ||||
|         /// <param name="type">The type.</param> | ||||
|         /// <param name="startIndex">Optional. The start index.</param> | ||||
|         /// <param name="limit">Optional. The limit.</param> | ||||
|         /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param> | ||||
|         /// <response code="200">Suggestions returned.</response> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns> | ||||
|         [HttpGet("Users/{userId}/Suggestions")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>( | ||||
|                 startIndex, | ||||
|                 result.TotalRecordCount, | ||||
|                 dtoList); | ||||
|         } | ||||
|         return new QueryResult<BaseItemDto>( | ||||
|             startIndex, | ||||
|             result.TotalRecordCount, | ||||
|             dtoList); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -16,409 +16,408 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The sync play controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.SyncPlayHasAccess)] | ||||
| public class SyncPlayController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ISessionManager _sessionManager; | ||||
|     private readonly ISyncPlayManager _syncPlayManager; | ||||
|     private readonly IUserManager _userManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The sync play controller. | ||||
|     /// Initializes a new instance of the <see cref="SyncPlayController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.SyncPlayHasAccess)] | ||||
|     public class SyncPlayController : BaseJellyfinApiController | ||||
|     /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> | ||||
|     /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     public SyncPlayController( | ||||
|         ISessionManager sessionManager, | ||||
|         ISyncPlayManager syncPlayManager, | ||||
|         IUserManager userManager) | ||||
|     { | ||||
|         private readonly ISessionManager _sessionManager; | ||||
|         private readonly ISyncPlayManager _syncPlayManager; | ||||
|         private readonly IUserManager _userManager; | ||||
|         _sessionManager = sessionManager; | ||||
|         _syncPlayManager = syncPlayManager; | ||||
|         _userManager = userManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SyncPlayController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> | ||||
|         /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         public SyncPlayController( | ||||
|             ISessionManager sessionManager, | ||||
|             ISyncPlayManager syncPlayManager, | ||||
|             IUserManager userManager) | ||||
|         { | ||||
|             _sessionManager = sessionManager; | ||||
|             _syncPlayManager = syncPlayManager; | ||||
|             _userManager = userManager; | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Create a new SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The settings of the new group.</param> | ||||
|     /// <response code="204">New group created.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("New")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayCreateGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Create a new SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The settings of the new group.</param> | ||||
|         /// <response code="204">New group created.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("New")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayCreateGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Join an existing SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The group to join.</param> | ||||
|     /// <response code="204">Group join successful.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("Join")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayJoinGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Join an existing SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The group to join.</param> | ||||
|         /// <response code="204">Group join successful.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Join")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayJoinGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Leave the joined SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <response code="204">Group leave successful.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("Leave")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> SyncPlayLeaveGroup() | ||||
|     { | ||||
|         var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); | ||||
|         var syncPlayRequest = new LeaveGroupRequest(); | ||||
|         _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Leave the joined SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <response code="204">Group leave successful.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Leave")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> SyncPlayLeaveGroup() | ||||
|         { | ||||
|             var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); | ||||
|             var syncPlayRequest = new LeaveGroupRequest(); | ||||
|             _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Gets all SyncPlay groups. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Groups returned.</response> | ||||
|     /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns> | ||||
|     [HttpGet("List")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [Authorize(Policy = Policies.SyncPlayJoinGroup)] | ||||
|     public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups() | ||||
|     { | ||||
|         var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); | ||||
|         var syncPlayRequest = new ListGroupsRequest(); | ||||
|         return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable()); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets all SyncPlay groups. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Groups returned.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns> | ||||
|         [HttpGet("List")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [Authorize(Policy = Policies.SyncPlayJoinGroup)] | ||||
|         public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups() | ||||
|         { | ||||
|             var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); | ||||
|             var syncPlayRequest = new ListGroupsRequest(); | ||||
|             return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable()); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request to set new playlist in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The new playlist to play in the group.</param> | ||||
|     /// <response code="204">Queue update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("SetNewQueue")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request to set new playlist in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The new playlist to play in the group.</param> | ||||
|         /// <response code="204">Queue update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("SetNewQueue")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request to change playlist item in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The new item to play.</param> | ||||
|     /// <response code="204">Queue update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("SetPlaylistItem")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request to change playlist item in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The new item to play.</param> | ||||
|         /// <response code="204">Queue update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("SetPlaylistItem")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request to remove items from the playlist in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The items to remove.</param> | ||||
|     /// <response code="204">Queue update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("RemoveFromPlaylist")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request to remove items from the playlist in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The items to remove.</param> | ||||
|         /// <response code="204">Queue update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("RemoveFromPlaylist")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request to move an item in the playlist in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The new position for the item.</param> | ||||
|     /// <response code="204">Queue update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("MovePlaylistItem")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request to move an item in the playlist in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The new position for the item.</param> | ||||
|         /// <response code="204">Queue update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("MovePlaylistItem")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request to queue items to the playlist of a SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The items to add.</param> | ||||
|     /// <response code="204">Queue update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("Queue")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request to queue items to the playlist of a SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The items to add.</param> | ||||
|         /// <response code="204">Queue update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Queue")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request unpause in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <response code="204">Unpause update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("Unpause")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> SyncPlayUnpause() | ||||
|     { | ||||
|         var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); | ||||
|         var syncPlayRequest = new UnpauseGroupRequest(); | ||||
|         _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request unpause in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <response code="204">Unpause update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Unpause")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> SyncPlayUnpause() | ||||
|         { | ||||
|             var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); | ||||
|             var syncPlayRequest = new UnpauseGroupRequest(); | ||||
|             _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request pause in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <response code="204">Pause update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("Pause")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> SyncPlayPause() | ||||
|     { | ||||
|         var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); | ||||
|         var syncPlayRequest = new PauseGroupRequest(); | ||||
|         _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request pause in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <response code="204">Pause update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Pause")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> SyncPlayPause() | ||||
|         { | ||||
|             var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); | ||||
|             var syncPlayRequest = new PauseGroupRequest(); | ||||
|             _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request stop in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <response code="204">Stop update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("Stop")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> SyncPlayStop() | ||||
|     { | ||||
|         var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); | ||||
|         var syncPlayRequest = new StopGroupRequest(); | ||||
|         _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request stop in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <response code="204">Stop update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Stop")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> SyncPlayStop() | ||||
|         { | ||||
|             var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); | ||||
|             var syncPlayRequest = new StopGroupRequest(); | ||||
|             _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); | ||||
|             return NoContent(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request seek in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The new playback position.</param> | ||||
|     /// <response code="204">Seek update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("Seek")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request seek in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The new playback position.</param> | ||||
|         /// <response code="204">Seek update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Seek")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Notify SyncPlay group that member is buffering. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The player status.</param> | ||||
|     /// <response code="204">Group state update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("Buffering")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Notify SyncPlay group that member is buffering. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The player status.</param> | ||||
|         /// <response code="204">Group state update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Buffering")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Notify SyncPlay group that member is ready for playback. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The player status.</param> | ||||
|     /// <response code="204">Group state update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("Ready")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Notify SyncPlay group that member is ready for playback. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The player status.</param> | ||||
|         /// <response code="204">Group state update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Ready")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request SyncPlay group to ignore member during group-wait. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The settings to set.</param> | ||||
|     /// <response code="204">Member state updated.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("SetIgnoreWait")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request SyncPlay group to ignore member during group-wait. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The settings to set.</param> | ||||
|         /// <response code="204">Member state updated.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("SetIgnoreWait")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request next item in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The current item information.</param> | ||||
|     /// <response code="204">Next item update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("NextItem")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request next item in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The current item information.</param> | ||||
|         /// <response code="204">Next item update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("NextItem")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request previous item in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The current item information.</param> | ||||
|     /// <response code="204">Previous item update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("PreviousItem")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request previous item in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The current item information.</param> | ||||
|         /// <response code="204">Previous item update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("PreviousItem")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request to set repeat mode in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The new repeat mode.</param> | ||||
|     /// <response code="204">Play queue update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("SetRepeatMode")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request to set repeat mode in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The new repeat mode.</param> | ||||
|         /// <response code="204">Play queue update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("SetRepeatMode")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Request to set shuffle mode in SyncPlay group. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The new shuffle mode.</param> | ||||
|     /// <response code="204">Play queue update sent to all group members.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("SetShuffleMode")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Request to set shuffle mode in SyncPlay group. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The new shuffle mode.</param> | ||||
|         /// <response code="204">Play queue update sent to all group members.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("SetShuffleMode")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         [Authorize(Policy = Policies.SyncPlayIsInGroup)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Update session ping. | ||||
|         /// </summary> | ||||
|         /// <param name="requestData">The new ping.</param> | ||||
|         /// <response code="204">Ping updated.</response> | ||||
|         /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|         [HttpPost("Ping")] | ||||
|         [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|         public async Task<ActionResult> 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(); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Update session ping. | ||||
|     /// </summary> | ||||
|     /// <param name="requestData">The new ping.</param> | ||||
|     /// <response code="204">Ping updated.</response> | ||||
|     /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> | ||||
|     [HttpPost("Ping")] | ||||
|     [ProducesResponseType(StatusCodes.Status204NoContent)] | ||||
|     public async Task<ActionResult> 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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -20,204 +20,203 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The system controller. | ||||
| /// </summary> | ||||
| public class SystemController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly IServerApplicationHost _appHost; | ||||
|     private readonly IApplicationPaths _appPaths; | ||||
|     private readonly IFileSystem _fileSystem; | ||||
|     private readonly INetworkManager _network; | ||||
|     private readonly ILogger<SystemController> _logger; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The system controller. | ||||
|     /// Initializes a new instance of the <see cref="SystemController"/> class. | ||||
|     /// </summary> | ||||
|     public class SystemController : BaseJellyfinApiController | ||||
|     /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> | ||||
|     /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> | ||||
|     /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param> | ||||
|     /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param> | ||||
|     public SystemController( | ||||
|         IServerConfigurationManager serverConfigurationManager, | ||||
|         IServerApplicationHost appHost, | ||||
|         IFileSystem fileSystem, | ||||
|         INetworkManager network, | ||||
|         ILogger<SystemController> logger) | ||||
|     { | ||||
|         private readonly IServerApplicationHost _appHost; | ||||
|         private readonly IApplicationPaths _appPaths; | ||||
|         private readonly IFileSystem _fileSystem; | ||||
|         private readonly INetworkManager _network; | ||||
|         private readonly ILogger<SystemController> _logger; | ||||
|         _appPaths = serverConfigurationManager.ApplicationPaths; | ||||
|         _appHost = appHost; | ||||
|         _fileSystem = fileSystem; | ||||
|         _network = network; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SystemController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> | ||||
|         /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> | ||||
|         /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param> | ||||
|         /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param> | ||||
|         public SystemController( | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             IServerApplicationHost appHost, | ||||
|             IFileSystem fileSystem, | ||||
|             INetworkManager network, | ||||
|             ILogger<SystemController> logger) | ||||
|     /// <summary> | ||||
|     /// Gets information about the server. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Information retrieved.</response> | ||||
|     /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns> | ||||
|     [HttpGet("Info")] | ||||
|     [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<SystemInfo> GetSystemInfo() | ||||
|     { | ||||
|         return _appHost.GetSystemInfo(Request); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets public information about the server. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Information retrieved.</response> | ||||
|     /// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns> | ||||
|     [HttpGet("Info/Public")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<PublicSystemInfo> GetPublicSystemInfo() | ||||
|     { | ||||
|         return _appHost.GetPublicSystemInfo(Request); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Pings the system. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Information retrieved.</response> | ||||
|     /// <returns>The server name.</returns> | ||||
|     [HttpGet("Ping", Name = "GetPingSystem")] | ||||
|     [HttpPost("Ping", Name = "PostPingSystem")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<string> PingSystem() | ||||
|     { | ||||
|         return _appHost.Name; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Restarts the application. | ||||
|     /// </summary> | ||||
|     /// <response code="204">Server restarted.</response> | ||||
|     /// <returns>No content. Server restarted.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Shuts down the application. | ||||
|     /// </summary> | ||||
|     /// <response code="204">Server shut down.</response> | ||||
|     /// <returns>No content. Server shut down.</returns> | ||||
|     [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(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a list of available server log files. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Information retrieved.</response> | ||||
|     /// <returns>An array of <see cref="LogFile"/> with the available log files.</returns> | ||||
|     [HttpGet("Logs")] | ||||
|     [Authorize(Policy = Policies.RequiresElevation)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<LogFile[]> GetServerLogs() | ||||
|     { | ||||
|         IEnumerable<FileSystemMetadata> files; | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false); | ||||
|         } | ||||
|         catch (IOException ex) | ||||
|         { | ||||
|             _logger.LogError(ex, "Error getting logs"); | ||||
|             files = Enumerable.Empty<FileSystemMetadata>(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets information about the server. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns> | ||||
|         [HttpGet("Info")] | ||||
|         [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<SystemInfo> 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(); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets public information about the server. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns> | ||||
|         [HttpGet("Info/Public")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<PublicSystemInfo> GetPublicSystemInfo() | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets information about the request endpoint. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Information retrieved.</response> | ||||
|     /// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns> | ||||
|     [HttpGet("Endpoint")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<EndPointInfo> GetEndpointInfo() | ||||
|     { | ||||
|         return new EndPointInfo | ||||
|         { | ||||
|             return _appHost.GetPublicSystemInfo(Request); | ||||
|         } | ||||
|             IsLocal = HttpContext.IsLocal(), | ||||
|             IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()) | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Pings the system. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns>The server name.</returns> | ||||
|         [HttpGet("Ping", Name = "GetPingSystem")] | ||||
|         [HttpPost("Ping", Name = "PostPingSystem")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<string> PingSystem() | ||||
|         { | ||||
|             return _appHost.Name; | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Gets a log file. | ||||
|     /// </summary> | ||||
|     /// <param name="name">The name of the log file to get.</param> | ||||
|     /// <response code="200">Log file retrieved.</response> | ||||
|     /// <returns>The log file.</returns> | ||||
|     [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)); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Restarts the application. | ||||
|         /// </summary> | ||||
|         /// <response code="204">Server restarted.</response> | ||||
|         /// <returns>No content. Server restarted.</returns> | ||||
|         [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"); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Shuts down the application. | ||||
|         /// </summary> | ||||
|         /// <response code="204">Server shut down.</response> | ||||
|         /// <returns>No content. Server shut down.</returns> | ||||
|         [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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of available server log files. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns>An array of <see cref="LogFile"/> with the available log files.</returns> | ||||
|         [HttpGet("Logs")] | ||||
|         [Authorize(Policy = Policies.RequiresElevation)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<LogFile[]> GetServerLogs() | ||||
|         { | ||||
|             IEnumerable<FileSystemMetadata> files; | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false); | ||||
|             } | ||||
|             catch (IOException ex) | ||||
|             { | ||||
|                 _logger.LogError(ex, "Error getting logs"); | ||||
|                 files = Enumerable.Empty<FileSystemMetadata>(); | ||||
|             } | ||||
| 
 | ||||
|             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; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets information about the request endpoint. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns> | ||||
|         [HttpGet("Endpoint")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<EndPointInfo> GetEndpointInfo() | ||||
|         { | ||||
|             return new EndPointInfo | ||||
|             { | ||||
|                 IsLocal = HttpContext.IsLocal(), | ||||
|                 IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()) | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a log file. | ||||
|         /// </summary> | ||||
|         /// <param name="name">The name of the log file to get.</param> | ||||
|         /// <response code="200">Log file retrieved.</response> | ||||
|         /// <returns>The log file.</returns> | ||||
|         [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"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets wake on lan information. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Information retrieved.</response> | ||||
|         /// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns> | ||||
|         [HttpGet("WakeOnLanInfo")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [Obsolete("This endpoint is obsolete.")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo() | ||||
|         { | ||||
|             var result = _network.GetMacAddresses() | ||||
|                 .Select(i => new WakeOnLanInfo(i)); | ||||
|             return Ok(result); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Gets wake on lan information. | ||||
|     /// </summary> | ||||
|     /// <response code="200">Information retrieved.</response> | ||||
|     /// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns> | ||||
|     [HttpGet("WakeOnLanInfo")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [Obsolete("This endpoint is obsolete.")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo() | ||||
|     { | ||||
|         var result = _network.GetMacAddresses() | ||||
|             .Select(i => new WakeOnLanInfo(i)); | ||||
|         return Ok(result); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -3,32 +3,31 @@ using MediaBrowser.Model.SyncPlay; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The time sync controller. | ||||
| /// </summary> | ||||
| [Route("")] | ||||
| public class TimeSyncController : BaseJellyfinApiController | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The time sync controller. | ||||
|     /// Gets the current UTC time. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     public class TimeSyncController : BaseJellyfinApiController | ||||
|     /// <response code="200">Time returned.</response> | ||||
|     /// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns> | ||||
|     [HttpGet("GetUtcTime")] | ||||
|     [ProducesResponseType(statusCode: StatusCodes.Status200OK)] | ||||
|     public ActionResult<UtcTimeResponse> GetUtcTime() | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Gets the current UTC time. | ||||
|         /// </summary> | ||||
|         /// <response code="200">Time returned.</response> | ||||
|         /// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns> | ||||
|         [HttpGet("GetUtcTime")] | ||||
|         [ProducesResponseType(statusCode: StatusCodes.Status200OK)] | ||||
|         public ActionResult<UtcTimeResponse> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -10,290 +10,289 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The trailers controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class TrailersController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ItemsController _itemsController; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The trailers controller. | ||||
|     /// Initializes a new instance of the <see cref="TrailersController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class TrailersController : BaseJellyfinApiController | ||||
|     /// <param name="itemsController">Instance of <see cref="ItemsController"/>.</param> | ||||
|     public TrailersController(ItemsController itemsController) | ||||
|     { | ||||
|         private readonly ItemsController _itemsController; | ||||
|         _itemsController = itemsController; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="TrailersController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="itemsController">Instance of <see cref="ItemsController"/>.</param> | ||||
|         public TrailersController(ItemsController itemsController) | ||||
|         { | ||||
|             _itemsController = itemsController; | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Finds movies and trailers similar to a given trailer. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param> | ||||
|     /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param> | ||||
|     /// <param name="hasThemeSong">Optional filter by items with theme songs.</param> | ||||
|     /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param> | ||||
|     /// <param name="hasSubtitles">Optional filter by items with subtitles.</param> | ||||
|     /// <param name="hasSpecialFeature">Optional filter by items with special features.</param> | ||||
|     /// <param name="hasTrailer">Optional filter by items with trailers.</param> | ||||
|     /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param> | ||||
|     /// <param name="parentIndexNumber">Optional filter by parent index number.</param> | ||||
|     /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param> | ||||
|     /// <param name="isHd">Optional filter by items that are HD or not.</param> | ||||
|     /// <param name="is4K">Optional filter by items that are 4K or not.</param> | ||||
|     /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param> | ||||
|     /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param> | ||||
|     /// <param name="minCommunityRating">Optional filter by minimum community rating.</param> | ||||
|     /// <param name="minCriticRating">Optional filter by minimum critic rating.</param> | ||||
|     /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param> | ||||
|     /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param> | ||||
|     /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param> | ||||
|     /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param> | ||||
|     /// <param name="hasOverview">Optional filter by items that have an overview or not.</param> | ||||
|     /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param> | ||||
|     /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param> | ||||
|     /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param> | ||||
|     /// <param name="isMovie">Optional filter for live tv movies.</param> | ||||
|     /// <param name="isSeries">Optional filter for live tv series.</param> | ||||
|     /// <param name="isNews">Optional filter for live tv news.</param> | ||||
|     /// <param name="isKids">Optional filter for live tv kids.</param> | ||||
|     /// <param name="isSports">Optional filter for live tv sports.</param> | ||||
|     /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param> | ||||
|     /// <param name="searchTerm">Optional. Filter based on a search term.</param> | ||||
|     /// <param name="sortOrder">Sort Order - Ascending, Descending.</param> | ||||
|     /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> | ||||
|     /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param> | ||||
|     /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|     /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|     /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> | ||||
|     /// <param name="isPlayed">Optional filter by items that are played, or not.</param> | ||||
|     /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="enableUserData">Optional, include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param> | ||||
|     /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param> | ||||
|     /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param> | ||||
|     /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param> | ||||
|     /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param> | ||||
|     /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param> | ||||
|     /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param> | ||||
|     /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param> | ||||
|     /// <param name="isLocked">Optional filter by items that are locked.</param> | ||||
|     /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param> | ||||
|     /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param> | ||||
|     /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param> | ||||
|     /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param> | ||||
|     /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param> | ||||
|     /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param> | ||||
|     /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param> | ||||
|     /// <param name="is3D">Optional filter by items that are 3D, or not.</param> | ||||
|     /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param> | ||||
|     /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|     /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|     /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|     /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param> | ||||
|     /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param> | ||||
|     /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns> | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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 }; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Finds movies and trailers similar to a given trailer. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param> | ||||
|         /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param> | ||||
|         /// <param name="hasThemeSong">Optional filter by items with theme songs.</param> | ||||
|         /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param> | ||||
|         /// <param name="hasSubtitles">Optional filter by items with subtitles.</param> | ||||
|         /// <param name="hasSpecialFeature">Optional filter by items with special features.</param> | ||||
|         /// <param name="hasTrailer">Optional filter by items with trailers.</param> | ||||
|         /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param> | ||||
|         /// <param name="parentIndexNumber">Optional filter by parent index number.</param> | ||||
|         /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param> | ||||
|         /// <param name="isHd">Optional filter by items that are HD or not.</param> | ||||
|         /// <param name="is4K">Optional filter by items that are 4K or not.</param> | ||||
|         /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param> | ||||
|         /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param> | ||||
|         /// <param name="minCommunityRating">Optional filter by minimum community rating.</param> | ||||
|         /// <param name="minCriticRating">Optional filter by minimum critic rating.</param> | ||||
|         /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param> | ||||
|         /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param> | ||||
|         /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param> | ||||
|         /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param> | ||||
|         /// <param name="hasOverview">Optional filter by items that have an overview or not.</param> | ||||
|         /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param> | ||||
|         /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param> | ||||
|         /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param> | ||||
|         /// <param name="isMovie">Optional filter for live tv movies.</param> | ||||
|         /// <param name="isSeries">Optional filter for live tv series.</param> | ||||
|         /// <param name="isNews">Optional filter for live tv news.</param> | ||||
|         /// <param name="isKids">Optional filter for live tv kids.</param> | ||||
|         /// <param name="isSports">Optional filter for live tv sports.</param> | ||||
|         /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param> | ||||
|         /// <param name="searchTerm">Optional. Filter based on a search term.</param> | ||||
|         /// <param name="sortOrder">Sort Order - Ascending, Descending.</param> | ||||
|         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> | ||||
|         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param> | ||||
|         /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> | ||||
|         /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|         /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> | ||||
|         /// <param name="isPlayed">Optional filter by items that are played, or not.</param> | ||||
|         /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="enableUserData">Optional, include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param> | ||||
|         /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param> | ||||
|         /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param> | ||||
|         /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param> | ||||
|         /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param> | ||||
|         /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param> | ||||
|         /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param> | ||||
|         /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param> | ||||
|         /// <param name="isLocked">Optional filter by items that are locked.</param> | ||||
|         /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param> | ||||
|         /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param> | ||||
|         /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param> | ||||
|         /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param> | ||||
|         /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param> | ||||
|         /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param> | ||||
|         /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param> | ||||
|         /// <param name="is3D">Optional filter by items that are 3D, or not.</param> | ||||
|         /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param> | ||||
|         /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> | ||||
|         /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> | ||||
|         /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> | ||||
|         /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param> | ||||
|         /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param> | ||||
|         /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -19,366 +19,365 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The tv shows controller. | ||||
| /// </summary> | ||||
| [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; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// The tv shows controller. | ||||
|     /// Initializes a new instance of the <see cref="TvShowsController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("Shows")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class TvShowsController : BaseJellyfinApiController | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     /// <param name="tvSeriesManager">Instance of the <see cref="ITVSeriesManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="TvShowsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="tvSeriesManager">Instance of the <see cref="ITVSeriesManager"/> interface.</param> | ||||
|         public TvShowsController( | ||||
|             IUserManager userManager, | ||||
|             ILibraryManager libraryManager, | ||||
|             IDtoService dtoService, | ||||
|             ITVSeriesManager tvSeriesManager) | ||||
|         { | ||||
|             _userManager = userManager; | ||||
|             _libraryManager = libraryManager; | ||||
|             _dtoService = dtoService; | ||||
|             _tvSeriesManager = tvSeriesManager; | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Gets a list of next up episodes. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">The user id of the user to get the next up episodes for.</param> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="seriesId">Optional. Filter by series id.</param> | ||||
|     /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param> | ||||
|     /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param> | ||||
|     /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param> | ||||
|     /// <param name="enableRewatching">Whether to include watched episode in next up results.</param> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> | ||||
|     [HttpGet("NextUp")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of next up episodes. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id of the user to get the next up episodes for.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="seriesId">Optional. Filter by series id.</param> | ||||
|         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param> | ||||
|         /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param> | ||||
|         /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param> | ||||
|         /// <param name="enableRewatching">Whether to include watched episode in next up results.</param> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> | ||||
|         [HttpGet("NextUp")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>( | ||||
|                 startIndex, | ||||
|                 result.TotalRecordCount, | ||||
|                 returnItems); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a list of upcoming episodes. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id of the user to get the upcoming episodes for.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> | ||||
|         [HttpGet("Upcoming")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>( | ||||
|                 startIndex, | ||||
|                 itemsResult.Count, | ||||
|                 returnItems); | ||||
|         } | ||||
|         var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets episodes for a tv season. | ||||
|         /// </summary> | ||||
|         /// <param name="seriesId">The series id.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> | ||||
|         /// <param name="season">Optional filter by season number.</param> | ||||
|         /// <param name="seasonId">Optional. Filter by season id.</param> | ||||
|         /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param> | ||||
|         /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param> | ||||
|         /// <param name="startItemId">Optional. Skip through the list until a given item is found.</param> | ||||
|         /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|         /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns> | ||||
|         [HttpGet("{seriesId}/Episodes")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>( | ||||
|             startIndex, | ||||
|             result.TotalRecordCount, | ||||
|             returnItems); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a list of upcoming episodes. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">The user id of the user to get the upcoming episodes for.</param> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> | ||||
|     [HttpGet("Upcoming")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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<BaseItem> episodes; | ||||
|         var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user); | ||||
| 
 | ||||
|             var dtoOptions = new DtoOptions { Fields = fields } | ||||
|                 .AddClientFields(User) | ||||
|                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); | ||||
|         return new QueryResult<BaseItemDto>( | ||||
|             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); | ||||
|                 } | ||||
|     /// <summary> | ||||
|     /// Gets episodes for a tv season. | ||||
|     /// </summary> | ||||
|     /// <param name="seriesId">The series id.</param> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> | ||||
|     /// <param name="season">Optional filter by season number.</param> | ||||
|     /// <param name="seasonId">Optional. Filter by season id.</param> | ||||
|     /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param> | ||||
|     /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param> | ||||
|     /// <param name="startItemId">Optional. Skip through the list until a given item is found.</param> | ||||
|     /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="enableImages">Optional, include image information in output.</param> | ||||
|     /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns> | ||||
|     [HttpGet("{seriesId}/Episodes")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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<BaseItem> 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<BaseItem>() | ||||
|                     : ((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<BaseItemDto>( | ||||
|                 startIndex, | ||||
|                 episodes.Count, | ||||
|                 dtos); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets seasons for a tv series. | ||||
|         /// </summary> | ||||
|         /// <param name="seriesId">The series id.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> | ||||
|         /// <param name="isSpecialSeason">Optional. Filter by special season.</param> | ||||
|         /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param> | ||||
|         /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> on success or a <see cref="NotFoundResult"/> if the series was not found.</returns> | ||||
|         [HttpGet("{seriesId}/Seasons")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>(returnItems); | ||||
|             episodes = seasonItem is null ? | ||||
|                 new List<BaseItem>() | ||||
|                 : ((Season)seasonItem).GetEpisodes(user, dtoOptions); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Applies the paging. | ||||
|         /// </summary> | ||||
|         /// <param name="items">The items.</param> | ||||
|         /// <param name="startIndex">The start index.</param> | ||||
|         /// <param name="limit">The limit.</param> | ||||
|         /// <returns>IEnumerable{BaseItem}.</returns> | ||||
|         private IEnumerable<BaseItem> ApplyPaging(IEnumerable<BaseItem> 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<BaseItemDto>( | ||||
|             startIndex, | ||||
|             episodes.Count, | ||||
|             dtos); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets seasons for a tv series. | ||||
|     /// </summary> | ||||
|     /// <param name="seriesId">The series id.</param> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> | ||||
|     /// <param name="isSpecialSeason">Optional. Filter by special season.</param> | ||||
|     /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param> | ||||
|     /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <returns>A <see cref="QueryResult{BaseItemDto}"/> on success or a <see cref="NotFoundResult"/> if the series was not found.</returns> | ||||
|     [HttpGet("{seriesId}/Seasons")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> 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<BaseItemDto>(returnItems); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Applies the paging. | ||||
|     /// </summary> | ||||
|     /// <param name="items">The items.</param> | ||||
|     /// <param name="startIndex">The start index.</param> | ||||
|     /// <param name="limit">The limit.</param> | ||||
|     /// <returns>IEnumerable{BaseItem}.</returns> | ||||
|     private IEnumerable<BaseItem> ApplyPaging(IEnumerable<BaseItem> 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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -20,197 +20,164 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The universal audio controller. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     public class UniversalAudioController : BaseJellyfinApiController | ||||
|     { | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly ILogger<UniversalAudioController> _logger; | ||||
|         private readonly MediaInfoHelper _mediaInfoHelper; | ||||
|         private readonly AudioHelper _audioHelper; | ||||
|         private readonly DynamicHlsHelper _dynamicHlsHelper; | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="UniversalAudioController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param> | ||||
|         /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param> | ||||
|         /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param> | ||||
|         /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> | ||||
|         public UniversalAudioController( | ||||
|             ILibraryManager libraryManager, | ||||
|             ILogger<UniversalAudioController> logger, | ||||
|             MediaInfoHelper mediaInfoHelper, | ||||
|             AudioHelper audioHelper, | ||||
|             DynamicHlsHelper dynamicHlsHelper) | ||||
| /// <summary> | ||||
| /// The universal audio controller. | ||||
| /// </summary> | ||||
| [Route("")] | ||||
| public class UniversalAudioController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly ILogger<UniversalAudioController> _logger; | ||||
|     private readonly MediaInfoHelper _mediaInfoHelper; | ||||
|     private readonly AudioHelper _audioHelper; | ||||
|     private readonly DynamicHlsHelper _dynamicHlsHelper; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initializes a new instance of the <see cref="UniversalAudioController"/> class. | ||||
|     /// </summary> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param> | ||||
|     /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param> | ||||
|     /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param> | ||||
|     /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> | ||||
|     public UniversalAudioController( | ||||
|         ILibraryManager libraryManager, | ||||
|         ILogger<UniversalAudioController> logger, | ||||
|         MediaInfoHelper mediaInfoHelper, | ||||
|         AudioHelper audioHelper, | ||||
|         DynamicHlsHelper dynamicHlsHelper) | ||||
|     { | ||||
|         _libraryManager = libraryManager; | ||||
|         _logger = logger; | ||||
|         _mediaInfoHelper = mediaInfoHelper; | ||||
|         _audioHelper = audioHelper; | ||||
|         _dynamicHlsHelper = dynamicHlsHelper; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets an audio stream. | ||||
|     /// </summary> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="container">Optional. The audio container.</param> | ||||
|     /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> | ||||
|     /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> | ||||
|     /// <param name="userId">Optional. The user id.</param> | ||||
|     /// <param name="audioCodec">Optional. The audio codec to transcode to.</param> | ||||
|     /// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param> | ||||
|     /// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param> | ||||
|     /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param> | ||||
|     /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> | ||||
|     /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param> | ||||
|     /// <param name="transcodingContainer">Optional. The container to transcode to.</param> | ||||
|     /// <param name="transcodingProtocol">Optional. The transcoding protocol.</param> | ||||
|     /// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param> | ||||
|     /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> | ||||
|     /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param> | ||||
|     /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> | ||||
|     /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param> | ||||
|     /// <response code="200">Audio stream returned.</response> | ||||
|     /// <response code="302">Redirected to remote audio stream.</response> | ||||
|     /// <returns>A <see cref="Task"/> containing the audio file.</returns> | ||||
|     [HttpGet("Audio/{itemId}/universal")] | ||||
|     [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status302Found)] | ||||
|     [ProducesAudioFile] | ||||
|     public async Task<ActionResult> 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(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an audio stream. | ||||
|         /// </summary> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="container">Optional. The audio container.</param> | ||||
|         /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> | ||||
|         /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> | ||||
|         /// <param name="userId">Optional. The user id.</param> | ||||
|         /// <param name="audioCodec">Optional. The audio codec to transcode to.</param> | ||||
|         /// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param> | ||||
|         /// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param> | ||||
|         /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param> | ||||
|         /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> | ||||
|         /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param> | ||||
|         /// <param name="transcodingContainer">Optional. The container to transcode to.</param> | ||||
|         /// <param name="transcodingProtocol">Optional. The transcoding protocol.</param> | ||||
|         /// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param> | ||||
|         /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> | ||||
|         /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param> | ||||
|         /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> | ||||
|         /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param> | ||||
|         /// <response code="200">Audio stream returned.</response> | ||||
|         /// <response code="302">Redirected to remote audio stream.</response> | ||||
|         /// <returns>A <see cref="Task"/> containing the audio file.</returns> | ||||
|         [HttpGet("Audio/{itemId}/universal")] | ||||
|         [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] | ||||
|         [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status302Found)] | ||||
|         [ProducesAudioFile] | ||||
|         public async Task<ActionResult> 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<string, string>(), | ||||
|                     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<string, string>(), | ||||
|                 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<CodecProfile>(); | ||||
|             var conditions = new List<ProfileCondition>(); | ||||
| 
 | ||||
|             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<CodecProfile>(); | ||||
|         var conditions = new List<ProfileCondition>(); | ||||
| 
 | ||||
|         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; | ||||
|     } | ||||
| } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -23,406 +23,405 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// User library controller. | ||||
| /// </summary> | ||||
| [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; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// User library controller. | ||||
|     /// Initializes a new instance of the <see cref="UserLibraryController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class UserLibraryController : BaseJellyfinApiController | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param> | ||||
|     /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|     /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="UserLibraryController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param> | ||||
|         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> | ||||
|         /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param> | ||||
|         public UserLibraryController( | ||||
|             IUserManager userManager, | ||||
|             IUserDataManager userDataRepository, | ||||
|             ILibraryManager libraryManager, | ||||
|             IDtoService dtoService, | ||||
|             IUserViewManager userViewManager, | ||||
|             IFileSystem fileSystem, | ||||
|             ILyricManager lyricManager) | ||||
|     /// <summary> | ||||
|     /// Gets an item from a user's library. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <response code="200">Item returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the d item.</returns> | ||||
|     [HttpGet("Users/{userId}/Items/{itemId}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<BaseItemDto>> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets the root folder from a user's library. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <response code="200">Root folder returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns> | ||||
|     [HttpGet("Users/{userId}/Items/Root")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<BaseItemDto> 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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets intros to play before the main media item plays. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <response code="200">Intros returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns> | ||||
|     [HttpGet("Users/{userId}/Items/{itemId}/Intros")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<QueryResult<BaseItemDto>>> 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<BaseItemDto>(dtos); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Marks an item as a favorite. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <response code="200">Item marked as favorite.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> | ||||
|     [HttpPost("Users/{userId}/FavoriteItems/{itemId}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) | ||||
|     { | ||||
|         return MarkFavorite(userId, itemId, true); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Unmarks item as a favorite. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <response code="200">Item unmarked as favorite.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> | ||||
|     [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) | ||||
|     { | ||||
|         return MarkFavorite(userId, itemId, false); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Deletes a user's saved personal rating for an item. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <response code="200">Personal rating removed.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> | ||||
|     [HttpDelete("Users/{userId}/Items/{itemId}/Rating")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) | ||||
|     { | ||||
|         return UpdateUserItemRatingInternal(userId, itemId, null); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Updates a user's rating for an item. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param> | ||||
|     /// <response code="200">Item rating updated.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> | ||||
|     [HttpPost("Users/{userId}/Items/{itemId}/Rating")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes) | ||||
|     { | ||||
|         return UpdateUserItemRatingInternal(userId, itemId, likes); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets local trailers for an item. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response> | ||||
|     /// <returns>The items local trailers.</returns> | ||||
|     [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<BaseItemDto>> 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()); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an item from a user's library. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <response code="200">Item returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the d item.</returns> | ||||
|         [HttpGet("Users/{userId}/Items/{itemId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<BaseItemDto>> 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))); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets special features for an item. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <response code="200">Special features returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the special features.</returns> | ||||
|     [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<BaseItemDto>> 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))); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets latest media. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="isPlayed">Filter by items that are played, or not.</param> | ||||
|     /// <param name="enableImages">Optional. include image information in output.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="enableUserData">Optional. include user data.</param> | ||||
|     /// <param name="limit">Return item limit.</param> | ||||
|     /// <param name="groupItems">Whether or not to group items into a parent container.</param> | ||||
|     /// <response code="200">Latest media returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the latest media.</returns> | ||||
|     [HttpGet("Users/{userId}/Items/Latest")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<IEnumerable<BaseItemDto>> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the root folder from a user's library. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <response code="200">Root folder returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns> | ||||
|         [HttpGet("Users/{userId}/Items/Root")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<BaseItemDto> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets intros to play before the main media item plays. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <response code="200">Intros returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the intros to play.</returns> | ||||
|         [HttpGet("Users/{userId}/Items/{itemId}/Intros")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<QueryResult<BaseItemDto>>> 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<BaseItemDto>(dtos); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Marks an item as a favorite. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <response code="200">Item marked as favorite.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> | ||||
|         [HttpPost("Users/{userId}/FavoriteItems/{itemId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) | ||||
|         { | ||||
|             return MarkFavorite(userId, itemId, true); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Unmarks item as a favorite. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <response code="200">Item unmarked as favorite.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> | ||||
|         [HttpDelete("Users/{userId}/FavoriteItems/{itemId}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) | ||||
|         { | ||||
|             return MarkFavorite(userId, itemId, false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Deletes a user's saved personal rating for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <response code="200">Personal rating removed.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> | ||||
|         [HttpDelete("Users/{userId}/Items/{itemId}/Rating")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId) | ||||
|         { | ||||
|             return UpdateUserItemRatingInternal(userId, itemId, null); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates a user's rating for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param> | ||||
|         /// <response code="200">Item rating updated.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns> | ||||
|         [HttpPost("Users/{userId}/Items/{itemId}/Rating")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes) | ||||
|         { | ||||
|             return UpdateUserItemRatingInternal(userId, itemId, likes); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets local trailers for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response> | ||||
|         /// <returns>The items local trailers.</returns> | ||||
|         [HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<BaseItemDto>> 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); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets special features for an item. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <response code="200">Special features returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the special features.</returns> | ||||
|         [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<BaseItemDto>> 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))); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets latest media. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="isPlayed">Filter by items that are played, or not.</param> | ||||
|         /// <param name="enableImages">Optional. include image information in output.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="enableUserData">Optional. include user data.</param> | ||||
|         /// <param name="limit">Return item limit.</param> | ||||
|         /// <param name="groupItems">Whether or not to group items into a parent container.</param> | ||||
|         /// <response code="200">Latest media returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the latest media.</returns> | ||||
|         [HttpGet("Users/{userId}/Items/Latest")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<IEnumerable<BaseItemDto>> 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); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Marks the favorite. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param> | ||||
|         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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Updates the user item rating. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="itemId">The item id.</param> | ||||
|         /// <param name="likes">if set to <c>true</c> [likes].</param> | ||||
|         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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an item's lyrics. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="itemId">Item id.</param> | ||||
|         /// <response code="200">Lyrics returned.</response> | ||||
|         /// <response code="404">Something went wrong. No Lyrics will be returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the item's lyrics.</returns> | ||||
|         [HttpGet("Users/{userId}/Items/{itemId}/Lyrics")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public async Task<ActionResult<LyricResponse>> 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(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Marks the favorite. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="isFavorite">if set to <c>true</c> [is favorite].</param> | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Updates the user item rating. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <param name="itemId">The item id.</param> | ||||
|     /// <param name="likes">if set to <c>true</c> [likes].</param> | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets an item's lyrics. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="itemId">Item id.</param> | ||||
|     /// <response code="200">Lyrics returned.</response> | ||||
|     /// <response code="404">Something went wrong. No Lyrics will be returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the item's lyrics.</returns> | ||||
|     [HttpGet("Users/{userId}/Items/{itemId}/Lyrics")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public async Task<ActionResult<LyricResponse>> 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(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -17,122 +17,121 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// User views controller. | ||||
| /// </summary> | ||||
| [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; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// User views controller. | ||||
|     /// Initializes a new instance of the <see cref="UserViewsController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("")] | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class UserViewsController : BaseJellyfinApiController | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="UserViewsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         public UserViewsController( | ||||
|             IUserManager userManager, | ||||
|             IUserViewManager userViewManager, | ||||
|             IDtoService dtoService, | ||||
|             ILibraryManager libraryManager) | ||||
|     /// <summary> | ||||
|     /// Get user views. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="includeExternalContent">Whether or not to include external views such as channels or live tv.</param> | ||||
|     /// <param name="presetViews">Preset views.</param> | ||||
|     /// <param name="includeHidden">Whether or not to include hidden content.</param> | ||||
|     /// <response code="200">User views returned.</response> | ||||
|     /// <returns>An <see cref="OkResult"/> containing the user views.</returns> | ||||
|     [HttpGet("Users/{userId}/Views")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public QueryResult<BaseItemDto> 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; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get user views. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="includeExternalContent">Whether or not to include external views such as channels or live tv.</param> | ||||
|         /// <param name="presetViews">Preset views.</param> | ||||
|         /// <param name="includeHidden">Whether or not to include hidden content.</param> | ||||
|         /// <response code="200">User views returned.</response> | ||||
|         /// <returns>An <see cref="OkResult"/> containing the user views.</returns> | ||||
|         [HttpGet("Users/{userId}/Views")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public QueryResult<BaseItemDto> 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<BaseItemDto>(dtos); | ||||
|             query.PresetViews = presetViews; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get user view grouping options. | ||||
|         /// </summary> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <response code="200">User view grouping options returned.</response> | ||||
|         /// <response code="404">User not found.</response> | ||||
|         /// <returns> | ||||
|         /// An <see cref="OkResult"/> containing the user view grouping options | ||||
|         /// or a <see cref="NotFoundResult"/> if user not found. | ||||
|         /// </returns> | ||||
|         [HttpGet("Users/{userId}/GroupingOptions")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<IEnumerable<SpecialViewOptionDto>> 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<Folder>() | ||||
|                 .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<BaseItemDto>(dtos); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Get user view grouping options. | ||||
|     /// </summary> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <response code="200">User view grouping options returned.</response> | ||||
|     /// <response code="404">User not found.</response> | ||||
|     /// <returns> | ||||
|     /// An <see cref="OkResult"/> containing the user view grouping options | ||||
|     /// or a <see cref="NotFoundResult"/> if user not found. | ||||
|     /// </returns> | ||||
|     [HttpGet("Users/{userId}/GroupingOptions")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromRoute, Required] Guid userId) | ||||
|     { | ||||
|         var user = _userManager.GetUserById(userId); | ||||
|         if (user is null) | ||||
|         { | ||||
|             return NotFound(); | ||||
|         } | ||||
| 
 | ||||
|         return Ok(_libraryManager.GetUserRootFolder() | ||||
|             .GetChildren(user, true) | ||||
|             .OfType<Folder>() | ||||
|             .Where(UserView.IsEligibleForGrouping) | ||||
|             .Select(i => new SpecialViewOptionDto | ||||
|             { | ||||
|                 Name = i.Name, | ||||
|                 Id = i.Id.ToString("N", CultureInfo.InvariantCulture) | ||||
|             }) | ||||
|             .OrderBy(i => i.Name) | ||||
|             .AsEnumerable()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -10,73 +10,72 @@ using MediaBrowser.Controller.MediaEncoding; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Attachments controller. | ||||
| /// </summary> | ||||
| [Route("Videos")] | ||||
| public class VideoAttachmentsController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly IAttachmentExtractor _attachmentExtractor; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Attachments controller. | ||||
|     /// Initializes a new instance of the <see cref="VideoAttachmentsController"/> class. | ||||
|     /// </summary> | ||||
|     [Route("Videos")] | ||||
|     public class VideoAttachmentsController : BaseJellyfinApiController | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param> | ||||
|     public VideoAttachmentsController( | ||||
|         ILibraryManager libraryManager, | ||||
|         IAttachmentExtractor attachmentExtractor) | ||||
|     { | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly IAttachmentExtractor _attachmentExtractor; | ||||
|         _libraryManager = libraryManager; | ||||
|         _attachmentExtractor = attachmentExtractor; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="VideoAttachmentsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param> | ||||
|         public VideoAttachmentsController( | ||||
|             ILibraryManager libraryManager, | ||||
|             IAttachmentExtractor attachmentExtractor) | ||||
|     /// <summary> | ||||
|     /// Get video attachment. | ||||
|     /// </summary> | ||||
|     /// <param name="videoId">Video ID.</param> | ||||
|     /// <param name="mediaSourceId">Media Source ID.</param> | ||||
|     /// <param name="index">Attachment Index.</param> | ||||
|     /// <response code="200">Attachment retrieved.</response> | ||||
|     /// <response code="404">Video or attachment not found.</response> | ||||
|     /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns> | ||||
|     [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")] | ||||
|     [ProducesFile(MediaTypeNames.Application.Octet)] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public async Task<ActionResult> 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); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get video attachment. | ||||
|         /// </summary> | ||||
|         /// <param name="videoId">Video ID.</param> | ||||
|         /// <param name="mediaSourceId">Media Source ID.</param> | ||||
|         /// <param name="index">Attachment Index.</param> | ||||
|         /// <response code="200">Attachment retrieved.</response> | ||||
|         /// <response code="404">Video or attachment not found.</response> | ||||
|         /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns> | ||||
|         [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")] | ||||
|         [ProducesFile(MediaTypeNames.Application.Octet)] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public async Task<ActionResult> 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -19,208 +19,207 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Controllers | ||||
| namespace Jellyfin.Api.Controllers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Years controller. | ||||
| /// </summary> | ||||
| [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
| public class YearsController : BaseJellyfinApiController | ||||
| { | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly IUserManager _userManager; | ||||
|     private readonly IDtoService _dtoService; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Years controller. | ||||
|     /// Initializes a new instance of the <see cref="YearsController"/> class. | ||||
|     /// </summary> | ||||
|     [Authorize(Policy = Policies.DefaultAuthorization)] | ||||
|     public class YearsController : BaseJellyfinApiController | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|     public YearsController( | ||||
|         ILibraryManager libraryManager, | ||||
|         IUserManager userManager, | ||||
|         IDtoService dtoService) | ||||
|     { | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly IUserManager _userManager; | ||||
|         private readonly IDtoService _dtoService; | ||||
|         _libraryManager = libraryManager; | ||||
|         _userManager = userManager; | ||||
|         _dtoService = dtoService; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="YearsController"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> | ||||
|         public YearsController( | ||||
|             ILibraryManager libraryManager, | ||||
|             IUserManager userManager, | ||||
|             IDtoService dtoService) | ||||
|     /// <summary> | ||||
|     /// Get years. | ||||
|     /// </summary> | ||||
|     /// <param name="startIndex">Skips over a given number of items within the results. Use for paging.</param> | ||||
|     /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|     /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> | ||||
|     /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|     /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|     /// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="includeItemTypes">Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.</param> | ||||
|     /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|     /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> | ||||
|     /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|     /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|     /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|     /// <param name="userId">User Id.</param> | ||||
|     /// <param name="recursive">Search recursively.</param> | ||||
|     /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|     /// <response code="200">Year query returned.</response> | ||||
|     /// <returns> A <see cref="QueryResult{BaseItemDto}"/> containing the year result.</returns> | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     public ActionResult<QueryResult<BaseItemDto>> GetYears( | ||||
|         [FromQuery] int? startIndex, | ||||
|         [FromQuery] int? limit, | ||||
|         [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))] string[] mediaTypes, | ||||
|         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, | ||||
|         [FromQuery] bool? enableUserData, | ||||
|         [FromQuery] int? imageTypeLimit, | ||||
|         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, | ||||
|         [FromQuery] Guid? userId, | ||||
|         [FromQuery] bool recursive = true, | ||||
|         [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); | ||||
|         BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); | ||||
| 
 | ||||
|         var query = new InternalItemsQuery(user) | ||||
|         { | ||||
|             _libraryManager = libraryManager; | ||||
|             _userManager = userManager; | ||||
|             _dtoService = dtoService; | ||||
|         } | ||||
|             ExcludeItemTypes = excludeItemTypes, | ||||
|             IncludeItemTypes = includeItemTypes, | ||||
|             MediaTypes = mediaTypes, | ||||
|             DtoOptions = dtoOptions | ||||
|         }; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get years. | ||||
|         /// </summary> | ||||
|         /// <param name="startIndex">Skips over a given number of items within the results. Use for paging.</param> | ||||
|         /// <param name="limit">Optional. The maximum number of records to return.</param> | ||||
|         /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> | ||||
|         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> | ||||
|         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> | ||||
|         /// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="includeItemTypes">Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.</param> | ||||
|         /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param> | ||||
|         /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> | ||||
|         /// <param name="enableUserData">Optional. Include user data.</param> | ||||
|         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> | ||||
|         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> | ||||
|         /// <param name="userId">User Id.</param> | ||||
|         /// <param name="recursive">Search recursively.</param> | ||||
|         /// <param name="enableImages">Optional. Include image information in output.</param> | ||||
|         /// <response code="200">Year query returned.</response> | ||||
|         /// <returns> A <see cref="QueryResult{BaseItemDto}"/> containing the year result.</returns> | ||||
|         [HttpGet] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         public ActionResult<QueryResult<BaseItemDto>> GetYears( | ||||
|             [FromQuery] int? startIndex, | ||||
|             [FromQuery] int? limit, | ||||
|             [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))] string[] mediaTypes, | ||||
|             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, | ||||
|             [FromQuery] bool? enableUserData, | ||||
|             [FromQuery] int? imageTypeLimit, | ||||
|             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, | ||||
|             [FromQuery] Guid? userId, | ||||
|             [FromQuery] bool recursive = true, | ||||
|             [FromQuery] bool? enableImages = true) | ||||
|         bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes); | ||||
| 
 | ||||
|         IList<BaseItem> items; | ||||
|         if (parentItem.IsFolder) | ||||
|         { | ||||
|             var dtoOptions = new DtoOptions { Fields = fields } | ||||
|                 .AddClientFields(User) | ||||
|                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); | ||||
|             var folder = (Folder)parentItem; | ||||
| 
 | ||||
|             User? user = userId is null || userId.Value.Equals(default) | ||||
|                 ? null | ||||
|                 : _userManager.GetUserById(userId.Value); | ||||
|             BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); | ||||
| 
 | ||||
|             var query = new InternalItemsQuery(user) | ||||
|             if (userId.Equals(default)) | ||||
|             { | ||||
|                 ExcludeItemTypes = excludeItemTypes, | ||||
|                 IncludeItemTypes = includeItemTypes, | ||||
|                 MediaTypes = mediaTypes, | ||||
|                 DtoOptions = dtoOptions | ||||
|             }; | ||||
| 
 | ||||
|             bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes); | ||||
| 
 | ||||
|             IList<BaseItem> items; | ||||
|             if (parentItem.IsFolder) | ||||
|             { | ||||
|                 var folder = (Folder)parentItem; | ||||
| 
 | ||||
|                 if (userId.Equals(default)) | ||||
|                 { | ||||
|                     items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList(); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList(); | ||||
|                 } | ||||
|                 items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList(); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 items = new[] { parentItem }.Where(Filter).ToList(); | ||||
|                 items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList(); | ||||
|             } | ||||
| 
 | ||||
|             var extractedItems = GetAllItems(items); | ||||
| 
 | ||||
|             var filteredItems = _libraryManager.Sort(extractedItems, user, RequestHelpers.GetOrderBy(sortBy, sortOrder)); | ||||
| 
 | ||||
|             var ibnItemsArray = filteredItems.ToList(); | ||||
| 
 | ||||
|             IEnumerable<BaseItem> ibnItems = ibnItemsArray; | ||||
| 
 | ||||
|             if (startIndex.HasValue || limit.HasValue) | ||||
|             { | ||||
|                 if (startIndex.HasValue) | ||||
|                 { | ||||
|                     ibnItems = ibnItems.Skip(startIndex.Value); | ||||
|                 } | ||||
| 
 | ||||
|                 if (limit.HasValue) | ||||
|                 { | ||||
|                     ibnItems = ibnItems.Take(limit.Value); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>())); | ||||
| 
 | ||||
|             var dtos = tuples.Select(i => _dtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user)); | ||||
| 
 | ||||
|             var result = new QueryResult<BaseItemDto>( | ||||
|                 startIndex, | ||||
|                 ibnItemsArray.Count, | ||||
|                 dtos.Where(i => i is not null).ToArray()); | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a year. | ||||
|         /// </summary> | ||||
|         /// <param name="year">The year.</param> | ||||
|         /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|         /// <response code="200">Year returned.</response> | ||||
|         /// <response code="404">Year not found.</response> | ||||
|         /// <returns> | ||||
|         /// An <see cref="OkResult"/> containing the year, | ||||
|         /// or a <see cref="NotFoundResult"/> if year not found. | ||||
|         /// </returns> | ||||
|         [HttpGet("{year}")] | ||||
|         [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|         [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|         public ActionResult<BaseItemDto> GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId) | ||||
|         else | ||||
|         { | ||||
|             var item = _libraryManager.GetYear(year); | ||||
|             if (item is null) | ||||
|             { | ||||
|                 return NotFound(); | ||||
|             } | ||||
| 
 | ||||
|             var dtoOptions = new DtoOptions() | ||||
|                 .AddClientFields(User); | ||||
| 
 | ||||
|             if (userId.HasValue && !userId.Value.Equals(default)) | ||||
|             { | ||||
|                 var user = _userManager.GetUserById(userId.Value); | ||||
|                 return _dtoService.GetBaseItemDto(item, dtoOptions, user); | ||||
|             } | ||||
| 
 | ||||
|             return _dtoService.GetBaseItemDto(item, dtoOptions); | ||||
|             items = new[] { parentItem }.Where(Filter).ToList(); | ||||
|         } | ||||
| 
 | ||||
|         private bool FilterItem(BaseItem f, IReadOnlyCollection<BaseItemKind> excludeItemTypes, IReadOnlyCollection<BaseItemKind> includeItemTypes, IReadOnlyCollection<string> mediaTypes) | ||||
|         var extractedItems = GetAllItems(items); | ||||
| 
 | ||||
|         var filteredItems = _libraryManager.Sort(extractedItems, user, RequestHelpers.GetOrderBy(sortBy, sortOrder)); | ||||
| 
 | ||||
|         var ibnItemsArray = filteredItems.ToList(); | ||||
| 
 | ||||
|         IEnumerable<BaseItem> ibnItems = ibnItemsArray; | ||||
| 
 | ||||
|         if (startIndex.HasValue || limit.HasValue) | ||||
|         { | ||||
|             var baseItemKind = f.GetBaseItemKind(); | ||||
|             // Exclude item types | ||||
|             if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(baseItemKind)) | ||||
|             if (startIndex.HasValue) | ||||
|             { | ||||
|                 return false; | ||||
|                 ibnItems = ibnItems.Skip(startIndex.Value); | ||||
|             } | ||||
| 
 | ||||
|             // Include item types | ||||
|             if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(baseItemKind)) | ||||
|             if (limit.HasValue) | ||||
|             { | ||||
|                 return false; | ||||
|                 ibnItems = ibnItems.Take(limit.Value); | ||||
|             } | ||||
| 
 | ||||
|             // Include MediaTypes | ||||
|             if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         private IEnumerable<BaseItem> GetAllItems(IEnumerable<BaseItem> items) | ||||
|         var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>())); | ||||
| 
 | ||||
|         var dtos = tuples.Select(i => _dtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user)); | ||||
| 
 | ||||
|         var result = new QueryResult<BaseItemDto>( | ||||
|             startIndex, | ||||
|             ibnItemsArray.Count, | ||||
|             dtos.Where(i => i is not null).ToArray()); | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a year. | ||||
|     /// </summary> | ||||
|     /// <param name="year">The year.</param> | ||||
|     /// <param name="userId">Optional. Filter by user id, and attach user data.</param> | ||||
|     /// <response code="200">Year returned.</response> | ||||
|     /// <response code="404">Year not found.</response> | ||||
|     /// <returns> | ||||
|     /// An <see cref="OkResult"/> containing the year, | ||||
|     /// or a <see cref="NotFoundResult"/> if year not found. | ||||
|     /// </returns> | ||||
|     [HttpGet("{year}")] | ||||
|     [ProducesResponseType(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType(StatusCodes.Status404NotFound)] | ||||
|     public ActionResult<BaseItemDto> GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId) | ||||
|     { | ||||
|         var item = _libraryManager.GetYear(year); | ||||
|         if (item is null) | ||||
|         { | ||||
|             return items | ||||
|                 .Select(i => i.ProductionYear ?? 0) | ||||
|                 .Where(i => i > 0) | ||||
|                 .Distinct() | ||||
|                 .Select(year => _libraryManager.GetYear(year)); | ||||
|             return NotFound(); | ||||
|         } | ||||
| 
 | ||||
|         var dtoOptions = new DtoOptions() | ||||
|             .AddClientFields(User); | ||||
| 
 | ||||
|         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 bool FilterItem(BaseItem f, IReadOnlyCollection<BaseItemKind> excludeItemTypes, IReadOnlyCollection<BaseItemKind> includeItemTypes, IReadOnlyCollection<string> mediaTypes) | ||||
|     { | ||||
|         var baseItemKind = f.GetBaseItemKind(); | ||||
|         // Exclude item types | ||||
|         if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(baseItemKind)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         // Include item types | ||||
|         if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(baseItemKind)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         // Include MediaTypes | ||||
|         if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     private IEnumerable<BaseItem> GetAllItems(IEnumerable<BaseItem> items) | ||||
|     { | ||||
|         return items | ||||
|             .Select(i => i.ProductionYear ?? 0) | ||||
|             .Where(i => i > 0) | ||||
|             .Distinct() | ||||
|             .Select(year => _libraryManager.GetYear(year)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -7,110 +7,109 @@ using MediaBrowser.Model.Entities; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Extensions | ||||
| namespace Jellyfin.Api.Extensions; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Dto Extensions. | ||||
| /// </summary> | ||||
| public static class DtoExtensions | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Dto Extensions. | ||||
|     /// Add additional fields depending on client. | ||||
|     /// </summary> | ||||
|     public static class DtoExtensions | ||||
|     /// <remarks> | ||||
|     /// Use in place of GetDtoOptions. | ||||
|     /// Legacy order: 2. | ||||
|     /// </remarks> | ||||
|     /// <param name="dtoOptions">DtoOptions object.</param> | ||||
|     /// <param name="user">Current claims principal.</param> | ||||
|     /// <returns>Modified DtoOptions object.</returns> | ||||
|     internal static DtoOptions AddClientFields( | ||||
|         this DtoOptions dtoOptions, ClaimsPrincipal user) | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Add additional fields depending on client. | ||||
|         /// </summary> | ||||
|         /// <remarks> | ||||
|         /// Use in place of GetDtoOptions. | ||||
|         /// Legacy order: 2. | ||||
|         /// </remarks> | ||||
|         /// <param name="dtoOptions">DtoOptions object.</param> | ||||
|         /// <param name="user">Current claims principal.</param> | ||||
|         /// <returns>Modified DtoOptions object.</returns> | ||||
|         internal static DtoOptions AddClientFields( | ||||
|             this DtoOptions dtoOptions, ClaimsPrincipal user) | ||||
|         dtoOptions.Fields ??= Array.Empty<ItemFields>(); | ||||
| 
 | ||||
|         string? client = user.GetClient(); | ||||
| 
 | ||||
|         // No client in claim | ||||
|         if (string.IsNullOrEmpty(client)) | ||||
|         { | ||||
|             dtoOptions.Fields ??= Array.Empty<ItemFields>(); | ||||
| 
 | ||||
|             string? client = user.GetClient(); | ||||
| 
 | ||||
|             // No client in claim | ||||
|             if (string.IsNullOrEmpty(client)) | ||||
|             { | ||||
|                 return dtoOptions; | ||||
|             } | ||||
| 
 | ||||
|             if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount)) | ||||
|             { | ||||
|                 if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1) | ||||
|                 { | ||||
|                     int oldLen = dtoOptions.Fields.Count; | ||||
|                     var arr = new ItemFields[oldLen + 1]; | ||||
|                     dtoOptions.Fields.CopyTo(arr, 0); | ||||
|                     arr[oldLen] = ItemFields.RecursiveItemCount; | ||||
|                     dtoOptions.Fields = arr; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (!dtoOptions.ContainsField(ItemFields.ChildCount)) | ||||
|             { | ||||
|                 if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                     client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1) | ||||
|                 { | ||||
|                     int oldLen = dtoOptions.Fields.Count; | ||||
|                     var arr = new ItemFields[oldLen + 1]; | ||||
|                     dtoOptions.Fields.CopyTo(arr, 0); | ||||
|                     arr[oldLen] = ItemFields.ChildCount; | ||||
|                     dtoOptions.Fields = arr; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return dtoOptions; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Add additional DtoOptions. | ||||
|         /// </summary> | ||||
|         /// <remarks> | ||||
|         /// Converted from IHasDtoOptions. | ||||
|         /// Legacy order: 3. | ||||
|         /// </remarks> | ||||
|         /// <param name="dtoOptions">DtoOptions object.</param> | ||||
|         /// <param name="enableImages">Enable images.</param> | ||||
|         /// <param name="enableUserData">Enable user data.</param> | ||||
|         /// <param name="imageTypeLimit">Image type limit.</param> | ||||
|         /// <param name="enableImageTypes">Enable image types.</param> | ||||
|         /// <returns>Modified DtoOptions object.</returns> | ||||
|         internal static DtoOptions AddAdditionalDtoOptions( | ||||
|             this DtoOptions dtoOptions, | ||||
|             bool? enableImages, | ||||
|             bool? enableUserData, | ||||
|             int? imageTypeLimit, | ||||
|             IReadOnlyList<ImageType> enableImageTypes) | ||||
|         if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount)) | ||||
|         { | ||||
|             dtoOptions.EnableImages = enableImages ?? true; | ||||
| 
 | ||||
|             if (imageTypeLimit.HasValue) | ||||
|             if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                 client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                 client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                 client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1) | ||||
|             { | ||||
|                 dtoOptions.ImageTypeLimit = imageTypeLimit.Value; | ||||
|                 int oldLen = dtoOptions.Fields.Count; | ||||
|                 var arr = new ItemFields[oldLen + 1]; | ||||
|                 dtoOptions.Fields.CopyTo(arr, 0); | ||||
|                 arr[oldLen] = ItemFields.RecursiveItemCount; | ||||
|                 dtoOptions.Fields = arr; | ||||
|             } | ||||
| 
 | ||||
|             if (enableUserData.HasValue) | ||||
|             { | ||||
|                 dtoOptions.EnableUserData = enableUserData.Value; | ||||
|             } | ||||
| 
 | ||||
|             if (enableImageTypes.Count != 0) | ||||
|             { | ||||
|                 dtoOptions.ImageTypes = enableImageTypes; | ||||
|             } | ||||
| 
 | ||||
|             return dtoOptions; | ||||
|         } | ||||
| 
 | ||||
|         if (!dtoOptions.ContainsField(ItemFields.ChildCount)) | ||||
|         { | ||||
|             if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                 client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                 client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                 client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                 client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                 client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 || | ||||
|                 client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1) | ||||
|             { | ||||
|                 int oldLen = dtoOptions.Fields.Count; | ||||
|                 var arr = new ItemFields[oldLen + 1]; | ||||
|                 dtoOptions.Fields.CopyTo(arr, 0); | ||||
|                 arr[oldLen] = ItemFields.ChildCount; | ||||
|                 dtoOptions.Fields = arr; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return dtoOptions; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Add additional DtoOptions. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// Converted from IHasDtoOptions. | ||||
|     /// Legacy order: 3. | ||||
|     /// </remarks> | ||||
|     /// <param name="dtoOptions">DtoOptions object.</param> | ||||
|     /// <param name="enableImages">Enable images.</param> | ||||
|     /// <param name="enableUserData">Enable user data.</param> | ||||
|     /// <param name="imageTypeLimit">Image type limit.</param> | ||||
|     /// <param name="enableImageTypes">Enable image types.</param> | ||||
|     /// <returns>Modified DtoOptions object.</returns> | ||||
|     internal static DtoOptions AddAdditionalDtoOptions( | ||||
|         this DtoOptions dtoOptions, | ||||
|         bool? enableImages, | ||||
|         bool? enableUserData, | ||||
|         int? imageTypeLimit, | ||||
|         IReadOnlyList<ImageType> enableImageTypes) | ||||
|     { | ||||
|         dtoOptions.EnableImages = enableImages ?? true; | ||||
| 
 | ||||
|         if (imageTypeLimit.HasValue) | ||||
|         { | ||||
|             dtoOptions.ImageTypeLimit = imageTypeLimit.Value; | ||||
|         } | ||||
| 
 | ||||
|         if (enableUserData.HasValue) | ||||
|         { | ||||
|             dtoOptions.EnableUserData = enableUserData.Value; | ||||
|         } | ||||
| 
 | ||||
|         if (enableImageTypes.Count != 0) | ||||
|         { | ||||
|             dtoOptions.ImageTypes = enableImageTypes; | ||||
|         } | ||||
| 
 | ||||
|         return dtoOptions; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -2,20 +2,19 @@ using Jellyfin.Extensions.Json; | ||||
| using Microsoft.AspNetCore.Mvc.Formatters; | ||||
| using Microsoft.Net.Http.Headers; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Formatters | ||||
| namespace Jellyfin.Api.Formatters; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Camel Case Json Profile Formatter. | ||||
| /// </summary> | ||||
| public class CamelCaseJsonProfileFormatter : SystemTextJsonOutputFormatter | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Camel Case Json Profile Formatter. | ||||
|     /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class. | ||||
|     /// </summary> | ||||
|     public class CamelCaseJsonProfileFormatter : SystemTextJsonOutputFormatter | ||||
|     public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCaseOptions) | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class. | ||||
|         /// </summary> | ||||
|         public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCaseOptions) | ||||
|         { | ||||
|             SupportedMediaTypes.Clear(); | ||||
|             SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType)); | ||||
|         } | ||||
|         SupportedMediaTypes.Clear(); | ||||
|         SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -3,34 +3,33 @@ using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc.Formatters; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Formatters | ||||
| namespace Jellyfin.Api.Formatters; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Css output formatter. | ||||
| /// </summary> | ||||
| public class CssOutputFormatter : TextOutputFormatter | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Css output formatter. | ||||
|     /// Initializes a new instance of the <see cref="CssOutputFormatter"/> class. | ||||
|     /// </summary> | ||||
|     public class CssOutputFormatter : TextOutputFormatter | ||||
|     public CssOutputFormatter() | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="CssOutputFormatter"/> class. | ||||
|         /// </summary> | ||||
|         public CssOutputFormatter() | ||||
|         { | ||||
|             SupportedMediaTypes.Add("text/css"); | ||||
|         SupportedMediaTypes.Add("text/css"); | ||||
| 
 | ||||
|             SupportedEncodings.Add(Encoding.UTF8); | ||||
|             SupportedEncodings.Add(Encoding.Unicode); | ||||
|         } | ||||
|         SupportedEncodings.Add(Encoding.UTF8); | ||||
|         SupportedEncodings.Add(Encoding.Unicode); | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Write context object to stream. | ||||
|         /// </summary> | ||||
|         /// <param name="context">Writer context.</param> | ||||
|         /// <param name="selectedEncoding">Unused. Writer encoding.</param> | ||||
|         /// <returns>Write stream task.</returns> | ||||
|         public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) | ||||
|         { | ||||
|             var stringResponse = context.Object?.ToString(); | ||||
|             return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse); | ||||
|         } | ||||
|     /// <summary> | ||||
|     /// Write context object to stream. | ||||
|     /// </summary> | ||||
|     /// <param name="context">Writer context.</param> | ||||
|     /// <param name="selectedEncoding">Unused. Writer encoding.</param> | ||||
|     /// <returns>Write stream task.</returns> | ||||
|     public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) | ||||
|     { | ||||
|         var stringResponse = context.Object?.ToString(); | ||||
|         return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -3,22 +3,21 @@ using Jellyfin.Extensions.Json; | ||||
| using Microsoft.AspNetCore.Mvc.Formatters; | ||||
| using Microsoft.Net.Http.Headers; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Formatters | ||||
| namespace Jellyfin.Api.Formatters; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Pascal Case Json Profile Formatter. | ||||
| /// </summary> | ||||
| public class PascalCaseJsonProfileFormatter : SystemTextJsonOutputFormatter | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Pascal Case Json Profile Formatter. | ||||
|     /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class. | ||||
|     /// </summary> | ||||
|     public class PascalCaseJsonProfileFormatter : SystemTextJsonOutputFormatter | ||||
|     public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCaseOptions) | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class. | ||||
|         /// </summary> | ||||
|         public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCaseOptions) | ||||
|         { | ||||
|             SupportedMediaTypes.Clear(); | ||||
|             // Add application/json for default formatter | ||||
|             SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json)); | ||||
|             SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.PascalCaseMediaType)); | ||||
|         } | ||||
|         SupportedMediaTypes.Clear(); | ||||
|         // Add application/json for default formatter | ||||
|         SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json)); | ||||
|         SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.PascalCaseMediaType)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -4,30 +4,29 @@ using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc.Formatters; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Formatters | ||||
| namespace Jellyfin.Api.Formatters; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Xml output formatter. | ||||
| /// </summary> | ||||
| public class XmlOutputFormatter : TextOutputFormatter | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Xml output formatter. | ||||
|     /// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class. | ||||
|     /// </summary> | ||||
|     public class XmlOutputFormatter : TextOutputFormatter | ||||
|     public XmlOutputFormatter() | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class. | ||||
|         /// </summary> | ||||
|         public XmlOutputFormatter() | ||||
|         { | ||||
|             SupportedMediaTypes.Clear(); | ||||
|             SupportedMediaTypes.Add(MediaTypeNames.Text.Xml); | ||||
|         SupportedMediaTypes.Clear(); | ||||
|         SupportedMediaTypes.Add(MediaTypeNames.Text.Xml); | ||||
| 
 | ||||
|             SupportedEncodings.Add(Encoding.UTF8); | ||||
|             SupportedEncodings.Add(Encoding.Unicode); | ||||
|         } | ||||
|         SupportedEncodings.Add(Encoding.UTF8); | ||||
|         SupportedEncodings.Add(Encoding.Unicode); | ||||
|     } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) | ||||
|         { | ||||
|             var stringResponse = context.Object?.ToString(); | ||||
|             return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse); | ||||
|         } | ||||
|     /// <inheritdoc /> | ||||
|     public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) | ||||
|     { | ||||
|         var stringResponse = context.Object?.ToString(); | ||||
|         return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -16,165 +16,164 @@ using MediaBrowser.Model.Net; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Helpers | ||||
| namespace Jellyfin.Api.Helpers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Audio helper. | ||||
| /// </summary> | ||||
| public class AudioHelper | ||||
| { | ||||
|     private readonly IDlnaManager _dlnaManager; | ||||
|     private readonly IUserManager _userManager; | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     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 IHttpContextAccessor _httpContextAccessor; | ||||
|     private readonly EncodingHelper _encodingHelper; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Audio helper. | ||||
|     /// Initializes a new instance of the <see cref="AudioHelper"/> class. | ||||
|     /// </summary> | ||||
|     public class AudioHelper | ||||
|     /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> | ||||
|     /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> | ||||
|     /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> | ||||
|     /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param> | ||||
|     /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> | ||||
|     /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> | ||||
|     /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> | ||||
|     public AudioHelper( | ||||
|         IDlnaManager dlnaManager, | ||||
|         IUserManager userManager, | ||||
|         ILibraryManager libraryManager, | ||||
|         IMediaSourceManager mediaSourceManager, | ||||
|         IServerConfigurationManager serverConfigurationManager, | ||||
|         IMediaEncoder mediaEncoder, | ||||
|         IDeviceManager deviceManager, | ||||
|         TranscodingJobHelper transcodingJobHelper, | ||||
|         IHttpClientFactory httpClientFactory, | ||||
|         IHttpContextAccessor httpContextAccessor, | ||||
|         EncodingHelper encodingHelper) | ||||
|     { | ||||
|         private readonly IDlnaManager _dlnaManager; | ||||
|         private readonly IUserManager _userManager; | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         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 IHttpContextAccessor _httpContextAccessor; | ||||
|         private readonly EncodingHelper _encodingHelper; | ||||
|         _dlnaManager = dlnaManager; | ||||
|         _userManager = userManager; | ||||
|         _libraryManager = libraryManager; | ||||
|         _mediaSourceManager = mediaSourceManager; | ||||
|         _serverConfigurationManager = serverConfigurationManager; | ||||
|         _mediaEncoder = mediaEncoder; | ||||
|         _deviceManager = deviceManager; | ||||
|         _transcodingJobHelper = transcodingJobHelper; | ||||
|         _httpClientFactory = httpClientFactory; | ||||
|         _httpContextAccessor = httpContextAccessor; | ||||
|         _encodingHelper = encodingHelper; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="AudioHelper"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> | ||||
|         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> | ||||
|         /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param> | ||||
|         /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> | ||||
|         /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> | ||||
|         /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> | ||||
|         public AudioHelper( | ||||
|             IDlnaManager dlnaManager, | ||||
|             IUserManager userManager, | ||||
|             ILibraryManager libraryManager, | ||||
|             IMediaSourceManager mediaSourceManager, | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             IMediaEncoder mediaEncoder, | ||||
|             IDeviceManager deviceManager, | ||||
|             TranscodingJobHelper transcodingJobHelper, | ||||
|             IHttpClientFactory httpClientFactory, | ||||
|             IHttpContextAccessor httpContextAccessor, | ||||
|             EncodingHelper encodingHelper) | ||||
|     /// <summary> | ||||
|     /// Get audio stream. | ||||
|     /// </summary> | ||||
|     /// <param name="transcodingJobType">Transcoding job type.</param> | ||||
|     /// <param name="streamingRequest">Streaming controller.Request dto.</param> | ||||
|     /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns> | ||||
|     public async Task<ActionResult> GetAudioStream( | ||||
|         TranscodingJobType transcodingJobType, | ||||
|         StreamingRequestDto streamingRequest) | ||||
|     { | ||||
|         if (_httpContextAccessor.HttpContext is null) | ||||
|         { | ||||
|             _dlnaManager = dlnaManager; | ||||
|             _userManager = userManager; | ||||
|             _libraryManager = libraryManager; | ||||
|             _mediaSourceManager = mediaSourceManager; | ||||
|             _serverConfigurationManager = serverConfigurationManager; | ||||
|             _mediaEncoder = mediaEncoder; | ||||
|             _deviceManager = deviceManager; | ||||
|             _transcodingJobHelper = transcodingJobHelper; | ||||
|             _httpClientFactory = httpClientFactory; | ||||
|             _httpContextAccessor = httpContextAccessor; | ||||
|             _encodingHelper = encodingHelper; | ||||
|             throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get audio stream. | ||||
|         /// </summary> | ||||
|         /// <param name="transcodingJobType">Transcoding job type.</param> | ||||
|         /// <param name="streamingRequest">Streaming controller.Request dto.</param> | ||||
|         /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns> | ||||
|         public async Task<ActionResult> GetAudioStream( | ||||
|             TranscodingJobType transcodingJobType, | ||||
|             StreamingRequestDto streamingRequest) | ||||
|         { | ||||
|             if (_httpContextAccessor.HttpContext is null) | ||||
|             { | ||||
|                 throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext)); | ||||
|             } | ||||
|         bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head; | ||||
| 
 | ||||
|             bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head; | ||||
|         // CTS lifecycle is managed internally. | ||||
|         var cancellationTokenSource = new CancellationTokenSource(); | ||||
| 
 | ||||
|             // CTS lifecycle is managed internally. | ||||
|             var cancellationTokenSource = new CancellationTokenSource(); | ||||
| 
 | ||||
|             using var state = await StreamingHelpers.GetStreamingState( | ||||
|                     streamingRequest, | ||||
|                     _httpContextAccessor.HttpContext, | ||||
|                     _mediaSourceManager, | ||||
|                     _userManager, | ||||
|                     _libraryManager, | ||||
|                     _serverConfigurationManager, | ||||
|                     _mediaEncoder, | ||||
|                     _encodingHelper, | ||||
|                     _dlnaManager, | ||||
|                     _deviceManager, | ||||
|                     _transcodingJobHelper, | ||||
|                     transcodingJobType, | ||||
|                     cancellationTokenSource.Token) | ||||
|                 .ConfigureAwait(false); | ||||
| 
 | ||||
|             if (streamingRequest.Static && state.DirectStreamProvider is not null) | ||||
|             { | ||||
|                 StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); | ||||
| 
 | ||||
|                 var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId); | ||||
|                 if (liveStreamInfo is null) | ||||
|                 { | ||||
|                     throw new FileNotFoundException(); | ||||
|                 } | ||||
| 
 | ||||
|                 var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream()); | ||||
|                 // TODO (moved from MediaBrowser.Api): Don't hardcode contentType | ||||
|                 return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file.ts")); | ||||
|             } | ||||
| 
 | ||||
|             // Static remote stream | ||||
|             if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http) | ||||
|             { | ||||
|                 StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); | ||||
| 
 | ||||
|                 var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); | ||||
|                 return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false); | ||||
|             } | ||||
| 
 | ||||
|             if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File) | ||||
|             { | ||||
|                 return new BadRequestObjectResult($"Input protocol {state.InputProtocol} cannot be streamed statically"); | ||||
|             } | ||||
| 
 | ||||
|             var outputPath = state.OutputFilePath; | ||||
|             var outputPathExists = File.Exists(outputPath); | ||||
| 
 | ||||
|             var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); | ||||
|             var isTranscodeCached = outputPathExists && transcodingJob is not null; | ||||
| 
 | ||||
|             StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); | ||||
| 
 | ||||
|             // Static stream | ||||
|             if (streamingRequest.Static) | ||||
|             { | ||||
|                 var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath); | ||||
| 
 | ||||
|                 if (state.MediaSource.IsInfiniteStream) | ||||
|                 { | ||||
|                     var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper); | ||||
|                     return new FileStreamResult(stream, contentType); | ||||
|                 } | ||||
| 
 | ||||
|                 return FileStreamResponseHelpers.GetStaticFileResult( | ||||
|                     state.MediaPath, | ||||
|                     contentType); | ||||
|             } | ||||
| 
 | ||||
|             // Need to start ffmpeg (because media can't be returned directly) | ||||
|             var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); | ||||
|             var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath); | ||||
|             return await FileStreamResponseHelpers.GetTranscodedFile( | ||||
|                 state, | ||||
|                 isHeadRequest, | ||||
|         using var state = await StreamingHelpers.GetStreamingState( | ||||
|                 streamingRequest, | ||||
|                 _httpContextAccessor.HttpContext, | ||||
|                 _mediaSourceManager, | ||||
|                 _userManager, | ||||
|                 _libraryManager, | ||||
|                 _serverConfigurationManager, | ||||
|                 _mediaEncoder, | ||||
|                 _encodingHelper, | ||||
|                 _dlnaManager, | ||||
|                 _deviceManager, | ||||
|                 _transcodingJobHelper, | ||||
|                 ffmpegCommandLineArguments, | ||||
|                 transcodingJobType, | ||||
|                 cancellationTokenSource).ConfigureAwait(false); | ||||
|                 cancellationTokenSource.Token) | ||||
|             .ConfigureAwait(false); | ||||
| 
 | ||||
|         if (streamingRequest.Static && state.DirectStreamProvider is not null) | ||||
|         { | ||||
|             StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); | ||||
| 
 | ||||
|             var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId); | ||||
|             if (liveStreamInfo is null) | ||||
|             { | ||||
|                 throw new FileNotFoundException(); | ||||
|             } | ||||
| 
 | ||||
|             var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream()); | ||||
|             // TODO (moved from MediaBrowser.Api): Don't hardcode contentType | ||||
|             return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file.ts")); | ||||
|         } | ||||
| 
 | ||||
|         // Static remote stream | ||||
|         if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http) | ||||
|         { | ||||
|             StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); | ||||
| 
 | ||||
|             var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); | ||||
|             return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File) | ||||
|         { | ||||
|             return new BadRequestObjectResult($"Input protocol {state.InputProtocol} cannot be streamed statically"); | ||||
|         } | ||||
| 
 | ||||
|         var outputPath = state.OutputFilePath; | ||||
|         var outputPathExists = File.Exists(outputPath); | ||||
| 
 | ||||
|         var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); | ||||
|         var isTranscodeCached = outputPathExists && transcodingJob is not null; | ||||
| 
 | ||||
|         StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); | ||||
| 
 | ||||
|         // Static stream | ||||
|         if (streamingRequest.Static) | ||||
|         { | ||||
|             var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath); | ||||
| 
 | ||||
|             if (state.MediaSource.IsInfiniteStream) | ||||
|             { | ||||
|                 var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper); | ||||
|                 return new FileStreamResult(stream, contentType); | ||||
|             } | ||||
| 
 | ||||
|             return FileStreamResponseHelpers.GetStaticFileResult( | ||||
|                 state.MediaPath, | ||||
|                 contentType); | ||||
|         } | ||||
| 
 | ||||
|         // Need to start ffmpeg (because media can't be returned directly) | ||||
|         var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); | ||||
|         var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath); | ||||
|         return await FileStreamResponseHelpers.GetTranscodedFile( | ||||
|             state, | ||||
|             isHeadRequest, | ||||
|             _httpContextAccessor.HttpContext, | ||||
|             _transcodingJobHelper, | ||||
|             ffmpegCommandLineArguments, | ||||
|             transcodingJobType, | ||||
|             cancellationTokenSource).ConfigureAwait(false); | ||||
|     } | ||||
| } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -11,110 +11,109 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Net.Http.Headers; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Helpers | ||||
| namespace Jellyfin.Api.Helpers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The stream response helpers. | ||||
| /// </summary> | ||||
| public static class FileStreamResponseHelpers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The stream response helpers. | ||||
|     /// Returns a static file from a remote source. | ||||
|     /// </summary> | ||||
|     public static class FileStreamResponseHelpers | ||||
|     /// <param name="state">The current <see cref="StreamState"/>.</param> | ||||
|     /// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param> | ||||
|     /// <param name="httpContext">The current http context.</param> | ||||
|     /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||||
|     /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns> | ||||
|     public static async Task<ActionResult> GetStaticRemoteStreamResult( | ||||
|         StreamState state, | ||||
|         HttpClient httpClient, | ||||
|         HttpContext httpContext, | ||||
|         CancellationToken cancellationToken = default) | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Returns a static file from a remote source. | ||||
|         /// </summary> | ||||
|         /// <param name="state">The current <see cref="StreamState"/>.</param> | ||||
|         /// <param name="httpClient">The <see cref="HttpClient"/> making the remote request.</param> | ||||
|         /// <param name="httpContext">The current http context.</param> | ||||
|         /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||||
|         /// <returns>A <see cref="Task{ActionResult}"/> containing the API response.</returns> | ||||
|         public static async Task<ActionResult> GetStaticRemoteStreamResult( | ||||
|             StreamState state, | ||||
|             HttpClient httpClient, | ||||
|             HttpContext httpContext, | ||||
|             CancellationToken cancellationToken = default) | ||||
|         if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent)) | ||||
|         { | ||||
|             if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent)) | ||||
|             { | ||||
|                 httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent); | ||||
|             } | ||||
| 
 | ||||
|             // Can't dispose the response as it's required up the call chain. | ||||
|             var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false); | ||||
|             var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain; | ||||
| 
 | ||||
|             httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; | ||||
| 
 | ||||
|             return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType); | ||||
|             httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, useragent); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Returns a static file from the server. | ||||
|         /// </summary> | ||||
|         /// <param name="path">The path to the file.</param> | ||||
|         /// <param name="contentType">The content type of the file.</param> | ||||
|         /// <returns>An <see cref="ActionResult"/> the file.</returns> | ||||
|         public static ActionResult GetStaticFileResult( | ||||
|             string path, | ||||
|             string contentType) | ||||
|         // Can't dispose the response as it's required up the call chain. | ||||
|         var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false); | ||||
|         var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain; | ||||
| 
 | ||||
|         httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; | ||||
| 
 | ||||
|         return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Returns a static file from the server. | ||||
|     /// </summary> | ||||
|     /// <param name="path">The path to the file.</param> | ||||
|     /// <param name="contentType">The content type of the file.</param> | ||||
|     /// <returns>An <see cref="ActionResult"/> the file.</returns> | ||||
|     public static ActionResult GetStaticFileResult( | ||||
|         string path, | ||||
|         string contentType) | ||||
|     { | ||||
|         return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true }; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Returns a transcoded file from the server. | ||||
|     /// </summary> | ||||
|     /// <param name="state">The current <see cref="StreamState"/>.</param> | ||||
|     /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param> | ||||
|     /// <param name="httpContext">The current http context.</param> | ||||
|     /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param> | ||||
|     /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param> | ||||
|     /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param> | ||||
|     /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param> | ||||
|     /// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns> | ||||
|     public static async Task<ActionResult> GetTranscodedFile( | ||||
|         StreamState state, | ||||
|         bool isHeadRequest, | ||||
|         HttpContext httpContext, | ||||
|         TranscodingJobHelper transcodingJobHelper, | ||||
|         string ffmpegCommandLineArguments, | ||||
|         TranscodingJobType transcodingJobType, | ||||
|         CancellationTokenSource cancellationTokenSource) | ||||
|     { | ||||
|         // Use the command line args with a dummy playlist path | ||||
|         var outputPath = state.OutputFilePath; | ||||
| 
 | ||||
|         httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; | ||||
| 
 | ||||
|         var contentType = state.GetMimeType(outputPath); | ||||
| 
 | ||||
|         // Headers only | ||||
|         if (isHeadRequest) | ||||
|         { | ||||
|             return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true }; | ||||
|             httpContext.Response.Headers[HeaderNames.ContentType] = contentType; | ||||
|             return new OkResult(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Returns a transcoded file from the server. | ||||
|         /// </summary> | ||||
|         /// <param name="state">The current <see cref="StreamState"/>.</param> | ||||
|         /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param> | ||||
|         /// <param name="httpContext">The current http context.</param> | ||||
|         /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param> | ||||
|         /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param> | ||||
|         /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param> | ||||
|         /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param> | ||||
|         /// <returns>A <see cref="Task{ActionResult}"/> containing the transcoded file.</returns> | ||||
|         public static async Task<ActionResult> GetTranscodedFile( | ||||
|             StreamState state, | ||||
|             bool isHeadRequest, | ||||
|             HttpContext httpContext, | ||||
|             TranscodingJobHelper transcodingJobHelper, | ||||
|             string ffmpegCommandLineArguments, | ||||
|             TranscodingJobType transcodingJobType, | ||||
|             CancellationTokenSource cancellationTokenSource) | ||||
|         var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath); | ||||
|         await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); | ||||
|         try | ||||
|         { | ||||
|             // Use the command line args with a dummy playlist path | ||||
|             var outputPath = state.OutputFilePath; | ||||
| 
 | ||||
|             httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; | ||||
| 
 | ||||
|             var contentType = state.GetMimeType(outputPath); | ||||
| 
 | ||||
|             // Headers only | ||||
|             if (isHeadRequest) | ||||
|             TranscodingJobDto? job; | ||||
|             if (!File.Exists(outputPath)) | ||||
|             { | ||||
|                 httpContext.Response.Headers[HeaderNames.ContentType] = contentType; | ||||
|                 return new OkResult(); | ||||
|                 job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); | ||||
|                 state.Dispose(); | ||||
|             } | ||||
| 
 | ||||
|             var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath); | ||||
|             await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); | ||||
|             try | ||||
|             { | ||||
|                 TranscodingJobDto? job; | ||||
|                 if (!File.Exists(outputPath)) | ||||
|                 { | ||||
|                     job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); | ||||
|                     state.Dispose(); | ||||
|                 } | ||||
| 
 | ||||
|                 var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper); | ||||
|                 return new FileStreamResult(stream, contentType); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 transcodingLock.Release(); | ||||
|             } | ||||
|             var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper); | ||||
|             return new FileStreamResult(stream, contentType); | ||||
|         } | ||||
|         finally | ||||
|         { | ||||
|             transcodingLock.Release(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -2,182 +2,181 @@ | ||||
| using System.Globalization; | ||||
| using System.Text; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Helpers | ||||
| namespace Jellyfin.Api.Helpers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Hls Codec string helpers. | ||||
| /// </summary> | ||||
| public static class HlsCodecStringHelpers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Hls Codec string helpers. | ||||
|     /// Codec name for MP3. | ||||
|     /// </summary> | ||||
|     public static class HlsCodecStringHelpers | ||||
|     public const string MP3 = "mp4a.40.34"; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Codec name for AC-3. | ||||
|     /// </summary> | ||||
|     public const string AC3 = "mp4a.a5"; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Codec name for E-AC-3. | ||||
|     /// </summary> | ||||
|     public const string EAC3 = "mp4a.a6"; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Codec name for FLAC. | ||||
|     /// </summary> | ||||
|     public const string FLAC = "flac"; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Codec name for ALAC. | ||||
|     /// </summary> | ||||
|     public const string ALAC = "alac"; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Codec name for OPUS. | ||||
|     /// </summary> | ||||
|     public const string OPUS = "opus"; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a MP3 codec string. | ||||
|     /// </summary> | ||||
|     /// <returns>MP3 codec string.</returns> | ||||
|     public static string GetMP3String() | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Codec name for MP3. | ||||
|         /// </summary> | ||||
|         public const string MP3 = "mp4a.40.34"; | ||||
|         return MP3; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Codec name for AC-3. | ||||
|         /// </summary> | ||||
|         public const string AC3 = "mp4a.a5"; | ||||
|     /// <summary> | ||||
|     /// Gets an AAC codec string. | ||||
|     /// </summary> | ||||
|     /// <param name="profile">AAC profile.</param> | ||||
|     /// <returns>AAC codec string.</returns> | ||||
|     public static string GetAACString(string? profile) | ||||
|     { | ||||
|         StringBuilder result = new StringBuilder("mp4a", 9); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Codec name for E-AC-3. | ||||
|         /// </summary> | ||||
|         public const string EAC3 = "mp4a.a6"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Codec name for FLAC. | ||||
|         /// </summary> | ||||
|         public const string FLAC = "flac"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Codec name for ALAC. | ||||
|         /// </summary> | ||||
|         public const string ALAC = "alac"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Codec name for OPUS. | ||||
|         /// </summary> | ||||
|         public const string OPUS = "opus"; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a MP3 codec string. | ||||
|         /// </summary> | ||||
|         /// <returns>MP3 codec string.</returns> | ||||
|         public static string GetMP3String() | ||||
|         if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             return MP3; | ||||
|             result.Append(".40.5"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             // Default to LC if profile is invalid | ||||
|             result.Append(".40.2"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an AAC codec string. | ||||
|         /// </summary> | ||||
|         /// <param name="profile">AAC profile.</param> | ||||
|         /// <returns>AAC codec string.</returns> | ||||
|         public static string GetAACString(string? profile) | ||||
|         return result.ToString(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets an AC-3 codec string. | ||||
|     /// </summary> | ||||
|     /// <returns>AC-3 codec string.</returns> | ||||
|     public static string GetAC3String() | ||||
|     { | ||||
|         return AC3; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets an E-AC-3 codec string. | ||||
|     /// </summary> | ||||
|     /// <returns>E-AC-3 codec string.</returns> | ||||
|     public static string GetEAC3String() | ||||
|     { | ||||
|         return EAC3; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets an FLAC codec string. | ||||
|     /// </summary> | ||||
|     /// <returns>FLAC codec string.</returns> | ||||
|     public static string GetFLACString() | ||||
|     { | ||||
|         return FLAC; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets an ALAC codec string. | ||||
|     /// </summary> | ||||
|     /// <returns>ALAC codec string.</returns> | ||||
|     public static string GetALACString() | ||||
|     { | ||||
|         return ALAC; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets an OPUS codec string. | ||||
|     /// </summary> | ||||
|     /// <returns>OPUS codec string.</returns> | ||||
|     public static string GetOPUSString() | ||||
|     { | ||||
|         return OPUS; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a H.264 codec string. | ||||
|     /// </summary> | ||||
|     /// <param name="profile">H.264 profile.</param> | ||||
|     /// <param name="level">H.264 level.</param> | ||||
|     /// <returns>H.264 string.</returns> | ||||
|     public static string GetH264String(string? profile, int level) | ||||
|     { | ||||
|         StringBuilder result = new StringBuilder("avc1", 11); | ||||
| 
 | ||||
|         if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             StringBuilder result = new StringBuilder("mp4a", 9); | ||||
| 
 | ||||
|             if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 result.Append(".40.5"); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // Default to LC if profile is invalid | ||||
|                 result.Append(".40.2"); | ||||
|             } | ||||
| 
 | ||||
|             return result.ToString(); | ||||
|             result.Append(".6400"); | ||||
|         } | ||||
|         else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             result.Append(".4D40"); | ||||
|         } | ||||
|         else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             result.Append(".42E0"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             // Default to constrained baseline if profile is invalid | ||||
|             result.Append(".4240"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an AC-3 codec string. | ||||
|         /// </summary> | ||||
|         /// <returns>AC-3 codec string.</returns> | ||||
|         public static string GetAC3String() | ||||
|         string levelHex = level.ToString("X2", CultureInfo.InvariantCulture); | ||||
|         result.Append(levelHex); | ||||
| 
 | ||||
|         return result.ToString(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets a H.265 codec string. | ||||
|     /// </summary> | ||||
|     /// <param name="profile">H.265 profile.</param> | ||||
|     /// <param name="level">H.265 level.</param> | ||||
|     /// <returns>H.265 string.</returns> | ||||
|     public static string GetH265String(string? profile, int level) | ||||
|     { | ||||
|         // The h265 syntax is a bit of a mystery at the time this comment was written. | ||||
|         // This is what I've found through various sources: | ||||
|         // FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN] | ||||
|         StringBuilder result = new StringBuilder("hvc1", 16); | ||||
| 
 | ||||
|         if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase) | ||||
|             || string.Equals(profile, "main 10", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             return AC3; | ||||
|             result.Append(".2.4"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             // Default to main if profile is invalid | ||||
|             result.Append(".1.4"); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an E-AC-3 codec string. | ||||
|         /// </summary> | ||||
|         /// <returns>E-AC-3 codec string.</returns> | ||||
|         public static string GetEAC3String() | ||||
|         { | ||||
|             return EAC3; | ||||
|         } | ||||
|         result.Append(".L") | ||||
|             .Append(level) | ||||
|             .Append(".B0"); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an FLAC codec string. | ||||
|         /// </summary> | ||||
|         /// <returns>FLAC codec string.</returns> | ||||
|         public static string GetFLACString() | ||||
|         { | ||||
|             return FLAC; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an ALAC codec string. | ||||
|         /// </summary> | ||||
|         /// <returns>ALAC codec string.</returns> | ||||
|         public static string GetALACString() | ||||
|         { | ||||
|             return ALAC; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets an OPUS codec string. | ||||
|         /// </summary> | ||||
|         /// <returns>OPUS codec string.</returns> | ||||
|         public static string GetOPUSString() | ||||
|         { | ||||
|             return OPUS; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a H.264 codec string. | ||||
|         /// </summary> | ||||
|         /// <param name="profile">H.264 profile.</param> | ||||
|         /// <param name="level">H.264 level.</param> | ||||
|         /// <returns>H.264 string.</returns> | ||||
|         public static string GetH264String(string? profile, int level) | ||||
|         { | ||||
|             StringBuilder result = new StringBuilder("avc1", 11); | ||||
| 
 | ||||
|             if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 result.Append(".6400"); | ||||
|             } | ||||
|             else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 result.Append(".4D40"); | ||||
|             } | ||||
|             else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 result.Append(".42E0"); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // Default to constrained baseline if profile is invalid | ||||
|                 result.Append(".4240"); | ||||
|             } | ||||
| 
 | ||||
|             string levelHex = level.ToString("X2", CultureInfo.InvariantCulture); | ||||
|             result.Append(levelHex); | ||||
| 
 | ||||
|             return result.ToString(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets a H.265 codec string. | ||||
|         /// </summary> | ||||
|         /// <param name="profile">H.265 profile.</param> | ||||
|         /// <param name="level">H.265 level.</param> | ||||
|         /// <returns>H.265 string.</returns> | ||||
|         public static string GetH265String(string? profile, int level) | ||||
|         { | ||||
|             // The h265 syntax is a bit of a mystery at the time this comment was written. | ||||
|             // This is what I've found through various sources: | ||||
|             // FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN] | ||||
|             StringBuilder result = new StringBuilder("hvc1", 16); | ||||
| 
 | ||||
|             if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(profile, "main 10", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 result.Append(".2.4"); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // Default to main if profile is invalid | ||||
|                 result.Append(".1.4"); | ||||
|             } | ||||
| 
 | ||||
|             result.Append(".L") | ||||
|                 .Append(level) | ||||
|                 .Append(".B0"); | ||||
| 
 | ||||
|             return result.ToString(); | ||||
|         } | ||||
|         return result.ToString(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -8,131 +8,130 @@ using MediaBrowser.Controller.MediaEncoding; | ||||
| using MediaBrowser.Model.IO; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Helpers | ||||
| namespace Jellyfin.Api.Helpers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// The hls helpers. | ||||
| /// </summary> | ||||
| public static class HlsHelpers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// The hls helpers. | ||||
|     /// Waits for a minimum number of segments to be available. | ||||
|     /// </summary> | ||||
|     public static class HlsHelpers | ||||
|     /// <param name="playlist">The playlist string.</param> | ||||
|     /// <param name="segmentCount">The segment count.</param> | ||||
|     /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> | ||||
|     /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> | ||||
|     /// <returns>A <see cref="Task"/> indicating the waiting process.</returns> | ||||
|     public static async Task WaitForMinimumSegmentCount(string playlist, int? segmentCount, ILogger logger, CancellationToken cancellationToken) | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Waits for a minimum number of segments to be available. | ||||
|         /// </summary> | ||||
|         /// <param name="playlist">The playlist string.</param> | ||||
|         /// <param name="segmentCount">The segment count.</param> | ||||
|         /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> | ||||
|         /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> | ||||
|         /// <returns>A <see cref="Task"/> indicating the waiting process.</returns> | ||||
|         public static async Task WaitForMinimumSegmentCount(string playlist, int? segmentCount, ILogger logger, CancellationToken cancellationToken) | ||||
|         logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist); | ||||
| 
 | ||||
|         while (!cancellationToken.IsCancellationRequested) | ||||
|         { | ||||
|             logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist); | ||||
| 
 | ||||
|             while (!cancellationToken.IsCancellationRequested) | ||||
|             try | ||||
|             { | ||||
|                 try | ||||
|                 // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written | ||||
|                 var fileStream = new FileStream( | ||||
|                     playlist, | ||||
|                     FileMode.Open, | ||||
|                     FileAccess.Read, | ||||
|                     FileShare.ReadWrite, | ||||
|                     IODefaults.FileStreamBufferSize, | ||||
|                     FileOptions.Asynchronous | FileOptions.SequentialScan); | ||||
|                 await using (fileStream.ConfigureAwait(false)) | ||||
|                 { | ||||
|                     // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written | ||||
|                     var fileStream = new FileStream( | ||||
|                         playlist, | ||||
|                         FileMode.Open, | ||||
|                         FileAccess.Read, | ||||
|                         FileShare.ReadWrite, | ||||
|                         IODefaults.FileStreamBufferSize, | ||||
|                         FileOptions.Asynchronous | FileOptions.SequentialScan); | ||||
|                     await using (fileStream.ConfigureAwait(false)) | ||||
|                     using var reader = new StreamReader(fileStream); | ||||
|                     var count = 0; | ||||
| 
 | ||||
|                     while (!reader.EndOfStream) | ||||
|                     { | ||||
|                         using var reader = new StreamReader(fileStream); | ||||
|                         var count = 0; | ||||
| 
 | ||||
|                         while (!reader.EndOfStream) | ||||
|                         var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); | ||||
|                         if (line is null) | ||||
|                         { | ||||
|                             var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); | ||||
|                             if (line is null) | ||||
|                             { | ||||
|                                 // Nothing currently in buffer. | ||||
|                                 break; | ||||
|                             } | ||||
|                             // Nothing currently in buffer. | ||||
|                             break; | ||||
|                         } | ||||
| 
 | ||||
|                             if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) | ||||
|                         if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) | ||||
|                         { | ||||
|                             count++; | ||||
|                             if (count >= segmentCount) | ||||
|                             { | ||||
|                                 count++; | ||||
|                                 if (count >= segmentCount) | ||||
|                                 { | ||||
|                                     logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist); | ||||
|                                     return; | ||||
|                                 } | ||||
|                                 logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist); | ||||
|                                 return; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     await Task.Delay(100, cancellationToken).ConfigureAwait(false); | ||||
|                 } | ||||
|                 catch (IOException) | ||||
|                 { | ||||
|                     // May get an error if the file is locked | ||||
|                 } | ||||
| 
 | ||||
|                 await Task.Delay(50, cancellationToken).ConfigureAwait(false); | ||||
|                 await Task.Delay(100, cancellationToken).ConfigureAwait(false); | ||||
|             } | ||||
|             catch (IOException) | ||||
|             { | ||||
|                 // May get an error if the file is locked | ||||
|             } | ||||
| 
 | ||||
|             await Task.Delay(50, cancellationToken).ConfigureAwait(false); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the #EXT-X-MAP string. | ||||
|         /// </summary> | ||||
|         /// <param name="outputPath">The output path of the file.</param> | ||||
|         /// <param name="state">The <see cref="StreamState"/>.</param> | ||||
|         /// <param name="isOsDepends">Get a normal string or depends on OS.</param> | ||||
|         /// <returns>The string text of #EXT-X-MAP.</returns> | ||||
|         public static string GetFmp4InitFileName(string outputPath, StreamState state, bool isOsDepends) | ||||
|     /// <summary> | ||||
|     /// Gets the #EXT-X-MAP string. | ||||
|     /// </summary> | ||||
|     /// <param name="outputPath">The output path of the file.</param> | ||||
|     /// <param name="state">The <see cref="StreamState"/>.</param> | ||||
|     /// <param name="isOsDepends">Get a normal string or depends on OS.</param> | ||||
|     /// <returns>The string text of #EXT-X-MAP.</returns> | ||||
|     public static string GetFmp4InitFileName(string outputPath, StreamState state, bool isOsDepends) | ||||
|     { | ||||
|         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); | ||||
| 
 | ||||
|         // on Linux/Unix | ||||
|         // #EXT-X-MAP:URI="prefix-1.mp4" | ||||
|         var fmp4InitFileName = outputFileNameWithoutExtension + "-1" + outputExtension; | ||||
|         if (!isOsDepends) | ||||
|         { | ||||
|             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); | ||||
| 
 | ||||
|             // on Linux/Unix | ||||
|             // #EXT-X-MAP:URI="prefix-1.mp4" | ||||
|             var fmp4InitFileName = outputFileNameWithoutExtension + "-1" + outputExtension; | ||||
|             if (!isOsDepends) | ||||
|             { | ||||
|                 return fmp4InitFileName; | ||||
|             } | ||||
| 
 | ||||
|             if (OperatingSystem.IsWindows()) | ||||
|             { | ||||
|                 // on Windows | ||||
|                 // #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4" | ||||
|                 fmp4InitFileName = outputPrefix + "-1" + outputExtension; | ||||
|             } | ||||
| 
 | ||||
|             return fmp4InitFileName; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets the hls playlist text. | ||||
|         /// </summary> | ||||
|         /// <param name="path">The path to the playlist file.</param> | ||||
|         /// <param name="state">The <see cref="StreamState"/>.</param> | ||||
|         /// <returns>The playlist text as a string.</returns> | ||||
|         public static string GetLivePlaylistText(string path, StreamState state) | ||||
|         if (OperatingSystem.IsWindows()) | ||||
|         { | ||||
|             var text = File.ReadAllText(path); | ||||
| 
 | ||||
|             var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); | ||||
|             if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 var fmp4InitFileName = GetFmp4InitFileName(path, state, true); | ||||
|                 var baseUrlParam = string.Format( | ||||
|                     CultureInfo.InvariantCulture, | ||||
|                     "hls/{0}/", | ||||
|                     Path.GetFileNameWithoutExtension(path)); | ||||
|                 var newFmp4InitFileName = baseUrlParam + GetFmp4InitFileName(path, state, false); | ||||
| 
 | ||||
|                 // Replace fMP4 init file URI. | ||||
|                 text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.InvariantCulture); | ||||
|             } | ||||
| 
 | ||||
|             return text; | ||||
|             // on Windows | ||||
|             // #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4" | ||||
|             fmp4InitFileName = outputPrefix + "-1" + outputExtension; | ||||
|         } | ||||
| 
 | ||||
|         return fmp4InitFileName; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets the hls playlist text. | ||||
|     /// </summary> | ||||
|     /// <param name="path">The path to the playlist file.</param> | ||||
|     /// <param name="state">The <see cref="StreamState"/>.</param> | ||||
|     /// <returns>The playlist text as a string.</returns> | ||||
|     public static string GetLivePlaylistText(string path, StreamState state) | ||||
|     { | ||||
|         var text = File.ReadAllText(path); | ||||
| 
 | ||||
|         var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.'); | ||||
|         if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             var fmp4InitFileName = GetFmp4InitFileName(path, state, true); | ||||
|             var baseUrlParam = string.Format( | ||||
|                 CultureInfo.InvariantCulture, | ||||
|                 "hls/{0}/", | ||||
|                 Path.GetFileNameWithoutExtension(path)); | ||||
|             var newFmp4InitFileName = baseUrlParam + GetFmp4InitFileName(path, state, false); | ||||
| 
 | ||||
|             // Replace fMP4 init file URI. | ||||
|             text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.InvariantCulture); | ||||
|         } | ||||
| 
 | ||||
|         return text; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -25,476 +25,475 @@ using MediaBrowser.Model.Session; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Helpers | ||||
| namespace Jellyfin.Api.Helpers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Media info helper. | ||||
| /// </summary> | ||||
| public class MediaInfoHelper | ||||
| { | ||||
|     private readonly IUserManager _userManager; | ||||
|     private readonly ILibraryManager _libraryManager; | ||||
|     private readonly IMediaSourceManager _mediaSourceManager; | ||||
|     private readonly IMediaEncoder _mediaEncoder; | ||||
|     private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
|     private readonly ILogger<MediaInfoHelper> _logger; | ||||
|     private readonly INetworkManager _networkManager; | ||||
|     private readonly IDeviceManager _deviceManager; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Media info helper. | ||||
|     /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class. | ||||
|     /// </summary> | ||||
|     public class MediaInfoHelper | ||||
|     /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|     /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> | ||||
|     /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> | ||||
|     /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param> | ||||
|     /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> | ||||
|     /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> | ||||
|     public MediaInfoHelper( | ||||
|         IUserManager userManager, | ||||
|         ILibraryManager libraryManager, | ||||
|         IMediaSourceManager mediaSourceManager, | ||||
|         IMediaEncoder mediaEncoder, | ||||
|         IServerConfigurationManager serverConfigurationManager, | ||||
|         ILogger<MediaInfoHelper> logger, | ||||
|         INetworkManager networkManager, | ||||
|         IDeviceManager deviceManager) | ||||
|     { | ||||
|         private readonly IUserManager _userManager; | ||||
|         private readonly ILibraryManager _libraryManager; | ||||
|         private readonly IMediaSourceManager _mediaSourceManager; | ||||
|         private readonly IMediaEncoder _mediaEncoder; | ||||
|         private readonly IServerConfigurationManager _serverConfigurationManager; | ||||
|         private readonly ILogger<MediaInfoHelper> _logger; | ||||
|         private readonly INetworkManager _networkManager; | ||||
|         private readonly IDeviceManager _deviceManager; | ||||
|         _userManager = userManager; | ||||
|         _libraryManager = libraryManager; | ||||
|         _mediaSourceManager = mediaSourceManager; | ||||
|         _mediaEncoder = mediaEncoder; | ||||
|         _serverConfigurationManager = serverConfigurationManager; | ||||
|         _logger = logger; | ||||
|         _networkManager = networkManager; | ||||
|         _deviceManager = deviceManager; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="MediaInfoHelper"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> | ||||
|         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> | ||||
|         /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param> | ||||
|         /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> | ||||
|         /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> | ||||
|         public MediaInfoHelper( | ||||
|             IUserManager userManager, | ||||
|             ILibraryManager libraryManager, | ||||
|             IMediaSourceManager mediaSourceManager, | ||||
|             IMediaEncoder mediaEncoder, | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             ILogger<MediaInfoHelper> logger, | ||||
|             INetworkManager networkManager, | ||||
|             IDeviceManager deviceManager) | ||||
|     /// <summary> | ||||
|     /// Get playback info. | ||||
|     /// </summary> | ||||
|     /// <param name="id">Item id.</param> | ||||
|     /// <param name="userId">User Id.</param> | ||||
|     /// <param name="mediaSourceId">Media source id.</param> | ||||
|     /// <param name="liveStreamId">Live stream id.</param> | ||||
|     /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns> | ||||
|     public async Task<PlaybackInfoResponse> GetPlaybackInfo( | ||||
|         Guid id, | ||||
|         Guid? userId, | ||||
|         string? mediaSourceId = null, | ||||
|         string? liveStreamId = null) | ||||
|     { | ||||
|         var user = userId is null || userId.Value.Equals(default) | ||||
|             ? null | ||||
|             : _userManager.GetUserById(userId.Value); | ||||
|         var item = _libraryManager.GetItemById(id); | ||||
|         var result = new PlaybackInfoResponse(); | ||||
| 
 | ||||
|         MediaSourceInfo[] mediaSources; | ||||
|         if (string.IsNullOrWhiteSpace(liveStreamId)) | ||||
|         { | ||||
|             _userManager = userManager; | ||||
|             _libraryManager = libraryManager; | ||||
|             _mediaSourceManager = mediaSourceManager; | ||||
|             _mediaEncoder = mediaEncoder; | ||||
|             _serverConfigurationManager = serverConfigurationManager; | ||||
|             _logger = logger; | ||||
|             _networkManager = networkManager; | ||||
|             _deviceManager = deviceManager; | ||||
|         } | ||||
|             // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes? | ||||
|             var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Get playback info. | ||||
|         /// </summary> | ||||
|         /// <param name="id">Item id.</param> | ||||
|         /// <param name="userId">User Id.</param> | ||||
|         /// <param name="mediaSourceId">Media source id.</param> | ||||
|         /// <param name="liveStreamId">Live stream id.</param> | ||||
|         /// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns> | ||||
|         public async Task<PlaybackInfoResponse> GetPlaybackInfo( | ||||
|             Guid id, | ||||
|             Guid? userId, | ||||
|             string? mediaSourceId = null, | ||||
|             string? liveStreamId = null) | ||||
|         { | ||||
|             var user = userId is null || userId.Value.Equals(default) | ||||
|                 ? null | ||||
|                 : _userManager.GetUserById(userId.Value); | ||||
|             var item = _libraryManager.GetItemById(id); | ||||
|             var result = new PlaybackInfoResponse(); | ||||
| 
 | ||||
|             MediaSourceInfo[] mediaSources; | ||||
|             if (string.IsNullOrWhiteSpace(liveStreamId)) | ||||
|             if (string.IsNullOrWhiteSpace(mediaSourceId)) | ||||
|             { | ||||
|                 // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes? | ||||
|                 var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false); | ||||
| 
 | ||||
|                 if (string.IsNullOrWhiteSpace(mediaSourceId)) | ||||
|                 { | ||||
|                     mediaSources = mediaSourcesList.ToArray(); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     mediaSources = mediaSourcesList | ||||
|                         .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)) | ||||
|                         .ToArray(); | ||||
|                 } | ||||
|                 mediaSources = mediaSourcesList.ToArray(); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false); | ||||
| 
 | ||||
|                 mediaSources = new[] { mediaSource }; | ||||
|                 mediaSources = mediaSourcesList | ||||
|                     .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)) | ||||
|                     .ToArray(); | ||||
|             } | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false); | ||||
| 
 | ||||
|             if (mediaSources.Length == 0) | ||||
|             { | ||||
|                 result.MediaSources = Array.Empty<MediaSourceInfo>(); | ||||
| 
 | ||||
|                 result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it | ||||
|                 // Should we move this directly into MediaSourceManager? | ||||
|                 var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources)); | ||||
|                 if (mediaSourcesClone is not null) | ||||
|                 { | ||||
|                     result.MediaSources = mediaSourcesClone; | ||||
|                 } | ||||
| 
 | ||||
|                 result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); | ||||
|             } | ||||
| 
 | ||||
|             return result; | ||||
|             mediaSources = new[] { mediaSource }; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// SetDeviceSpecificData. | ||||
|         /// </summary> | ||||
|         /// <param name="item">Item to set data for.</param> | ||||
|         /// <param name="mediaSource">Media source info.</param> | ||||
|         /// <param name="profile">Device profile.</param> | ||||
|         /// <param name="claimsPrincipal">Current claims principal.</param> | ||||
|         /// <param name="maxBitrate">Max bitrate.</param> | ||||
|         /// <param name="startTimeTicks">Start time ticks.</param> | ||||
|         /// <param name="mediaSourceId">Media source id.</param> | ||||
|         /// <param name="audioStreamIndex">Audio stream index.</param> | ||||
|         /// <param name="subtitleStreamIndex">Subtitle stream index.</param> | ||||
|         /// <param name="maxAudioChannels">Max audio channels.</param> | ||||
|         /// <param name="playSessionId">Play session id.</param> | ||||
|         /// <param name="userId">User id.</param> | ||||
|         /// <param name="enableDirectPlay">Enable direct play.</param> | ||||
|         /// <param name="enableDirectStream">Enable direct stream.</param> | ||||
|         /// <param name="enableTranscoding">Enable transcoding.</param> | ||||
|         /// <param name="allowVideoStreamCopy">Allow video stream copy.</param> | ||||
|         /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param> | ||||
|         /// <param name="ipAddress">Requesting IP address.</param> | ||||
|         public void SetDeviceSpecificData( | ||||
|             BaseItem item, | ||||
|             MediaSourceInfo mediaSource, | ||||
|             DeviceProfile profile, | ||||
|             ClaimsPrincipal claimsPrincipal, | ||||
|             int? maxBitrate, | ||||
|             long startTimeTicks, | ||||
|             string mediaSourceId, | ||||
|             int? audioStreamIndex, | ||||
|             int? subtitleStreamIndex, | ||||
|             int? maxAudioChannels, | ||||
|             string playSessionId, | ||||
|             Guid userId, | ||||
|             bool enableDirectPlay, | ||||
|             bool enableDirectStream, | ||||
|             bool enableTranscoding, | ||||
|             bool allowVideoStreamCopy, | ||||
|             bool allowAudioStreamCopy, | ||||
|             IPAddress ipAddress) | ||||
|         if (mediaSources.Length == 0) | ||||
|         { | ||||
|             var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); | ||||
|             result.MediaSources = Array.Empty<MediaSourceInfo>(); | ||||
| 
 | ||||
|             var options = new MediaOptions | ||||
|             result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it | ||||
|             // Should we move this directly into MediaSourceManager? | ||||
|             var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources)); | ||||
|             if (mediaSourcesClone is not null) | ||||
|             { | ||||
|                 MediaSources = new[] { mediaSource }, | ||||
|                 Context = EncodingContext.Streaming, | ||||
|                 DeviceId = claimsPrincipal.GetDeviceId(), | ||||
|                 ItemId = item.Id, | ||||
|                 Profile = profile, | ||||
|                 MaxAudioChannels = maxAudioChannels, | ||||
|                 AllowAudioStreamCopy = allowAudioStreamCopy, | ||||
|                 AllowVideoStreamCopy = allowVideoStreamCopy | ||||
|             }; | ||||
| 
 | ||||
|             if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 options.MediaSourceId = mediaSourceId; | ||||
|                 options.AudioStreamIndex = audioStreamIndex; | ||||
|                 options.SubtitleStreamIndex = subtitleStreamIndex; | ||||
|                 result.MediaSources = mediaSourcesClone; | ||||
|             } | ||||
| 
 | ||||
|             var user = _userManager.GetUserById(userId); | ||||
|             result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); | ||||
|         } | ||||
| 
 | ||||
|             if (!enableDirectPlay) | ||||
|             { | ||||
|                 mediaSource.SupportsDirectPlay = false; | ||||
|             } | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|             if (!enableDirectStream || !allowVideoStreamCopy) | ||||
|             { | ||||
|                 mediaSource.SupportsDirectStream = false; | ||||
|             } | ||||
|     /// <summary> | ||||
|     /// SetDeviceSpecificData. | ||||
|     /// </summary> | ||||
|     /// <param name="item">Item to set data for.</param> | ||||
|     /// <param name="mediaSource">Media source info.</param> | ||||
|     /// <param name="profile">Device profile.</param> | ||||
|     /// <param name="claimsPrincipal">Current claims principal.</param> | ||||
|     /// <param name="maxBitrate">Max bitrate.</param> | ||||
|     /// <param name="startTimeTicks">Start time ticks.</param> | ||||
|     /// <param name="mediaSourceId">Media source id.</param> | ||||
|     /// <param name="audioStreamIndex">Audio stream index.</param> | ||||
|     /// <param name="subtitleStreamIndex">Subtitle stream index.</param> | ||||
|     /// <param name="maxAudioChannels">Max audio channels.</param> | ||||
|     /// <param name="playSessionId">Play session id.</param> | ||||
|     /// <param name="userId">User id.</param> | ||||
|     /// <param name="enableDirectPlay">Enable direct play.</param> | ||||
|     /// <param name="enableDirectStream">Enable direct stream.</param> | ||||
|     /// <param name="enableTranscoding">Enable transcoding.</param> | ||||
|     /// <param name="allowVideoStreamCopy">Allow video stream copy.</param> | ||||
|     /// <param name="allowAudioStreamCopy">Allow audio stream copy.</param> | ||||
|     /// <param name="ipAddress">Requesting IP address.</param> | ||||
|     public void SetDeviceSpecificData( | ||||
|         BaseItem item, | ||||
|         MediaSourceInfo mediaSource, | ||||
|         DeviceProfile profile, | ||||
|         ClaimsPrincipal claimsPrincipal, | ||||
|         int? maxBitrate, | ||||
|         long startTimeTicks, | ||||
|         string mediaSourceId, | ||||
|         int? audioStreamIndex, | ||||
|         int? subtitleStreamIndex, | ||||
|         int? maxAudioChannels, | ||||
|         string playSessionId, | ||||
|         Guid userId, | ||||
|         bool enableDirectPlay, | ||||
|         bool enableDirectStream, | ||||
|         bool enableTranscoding, | ||||
|         bool allowVideoStreamCopy, | ||||
|         bool allowAudioStreamCopy, | ||||
|         IPAddress ipAddress) | ||||
|     { | ||||
|         var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); | ||||
| 
 | ||||
|             if (!enableTranscoding) | ||||
|             { | ||||
|                 mediaSource.SupportsTranscoding = false; | ||||
|             } | ||||
|         var options = new MediaOptions | ||||
|         { | ||||
|             MediaSources = new[] { mediaSource }, | ||||
|             Context = EncodingContext.Streaming, | ||||
|             DeviceId = claimsPrincipal.GetDeviceId(), | ||||
|             ItemId = item.Id, | ||||
|             Profile = profile, | ||||
|             MaxAudioChannels = maxAudioChannels, | ||||
|             AllowAudioStreamCopy = allowAudioStreamCopy, | ||||
|             AllowVideoStreamCopy = allowVideoStreamCopy | ||||
|         }; | ||||
| 
 | ||||
|         if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             options.MediaSourceId = mediaSourceId; | ||||
|             options.AudioStreamIndex = audioStreamIndex; | ||||
|             options.SubtitleStreamIndex = subtitleStreamIndex; | ||||
|         } | ||||
| 
 | ||||
|         var user = _userManager.GetUserById(userId); | ||||
| 
 | ||||
|         if (!enableDirectPlay) | ||||
|         { | ||||
|             mediaSource.SupportsDirectPlay = false; | ||||
|         } | ||||
| 
 | ||||
|         if (!enableDirectStream || !allowVideoStreamCopy) | ||||
|         { | ||||
|             mediaSource.SupportsDirectStream = false; | ||||
|         } | ||||
| 
 | ||||
|         if (!enableTranscoding) | ||||
|         { | ||||
|             mediaSource.SupportsTranscoding = false; | ||||
|         } | ||||
| 
 | ||||
|         if (item is Audio) | ||||
|         { | ||||
|             _logger.LogInformation( | ||||
|                 "User policy for {0}. EnableAudioPlaybackTranscoding: {1}", | ||||
|                 user.Username, | ||||
|                 user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             _logger.LogInformation( | ||||
|                 "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}", | ||||
|                 user.Username, | ||||
|                 user.HasPermission(PermissionKind.EnablePlaybackRemuxing), | ||||
|                 user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), | ||||
|                 user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); | ||||
|         } | ||||
| 
 | ||||
|         options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); | ||||
| 
 | ||||
|         if (!options.ForceDirectStream) | ||||
|         { | ||||
|             // direct-stream http streaming is currently broken | ||||
|             options.EnableDirectStream = false; | ||||
|         } | ||||
| 
 | ||||
|         // Beginning of Playback Determination | ||||
|         var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) | ||||
|             ? streamBuilder.GetOptimalAudioStream(options) | ||||
|             : streamBuilder.GetOptimalVideoStream(options); | ||||
| 
 | ||||
|         if (streamInfo is not null) | ||||
|         { | ||||
|             streamInfo.PlaySessionId = playSessionId; | ||||
|             streamInfo.StartPositionTicks = startTimeTicks; | ||||
| 
 | ||||
|             mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay; | ||||
| 
 | ||||
|             // Players do not handle this being set according to PlayMethod | ||||
|             mediaSource.SupportsDirectStream = | ||||
|                 options.EnableDirectStream | ||||
|                     ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream | ||||
|                     : streamInfo.PlayMethod == PlayMethod.DirectPlay; | ||||
| 
 | ||||
|             mediaSource.SupportsTranscoding = | ||||
|                 streamInfo.PlayMethod == PlayMethod.DirectStream | ||||
|                 || mediaSource.TranscodingContainer is not null | ||||
|                 || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context); | ||||
| 
 | ||||
|             if (item is Audio) | ||||
|             { | ||||
|                 _logger.LogInformation( | ||||
|                     "User policy for {0}. EnableAudioPlaybackTranscoding: {1}", | ||||
|                     user.Username, | ||||
|                     user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); | ||||
|                 if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) | ||||
|                 { | ||||
|                     mediaSource.SupportsTranscoding = false; | ||||
|                 } | ||||
|             } | ||||
|             else if (item is Video) | ||||
|             { | ||||
|                 if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) | ||||
|                     && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) | ||||
|                     && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) | ||||
|                 { | ||||
|                     mediaSource.SupportsTranscoding = false; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) | ||||
|             { | ||||
|                 mediaSource.SupportsDirectPlay = false; | ||||
|                 mediaSource.SupportsDirectStream = false; | ||||
| 
 | ||||
|                 mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-'); | ||||
|                 mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; | ||||
|                 mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; | ||||
|                 mediaSource.TranscodingContainer = streamInfo.Container; | ||||
|                 mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _logger.LogInformation( | ||||
|                     "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}", | ||||
|                     user.Username, | ||||
|                     user.HasPermission(PermissionKind.EnablePlaybackRemuxing), | ||||
|                     user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), | ||||
|                     user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); | ||||
|             } | ||||
| 
 | ||||
|             options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); | ||||
| 
 | ||||
|             if (!options.ForceDirectStream) | ||||
|             { | ||||
|                 // direct-stream http streaming is currently broken | ||||
|                 options.EnableDirectStream = false; | ||||
|             } | ||||
| 
 | ||||
|             // Beginning of Playback Determination | ||||
|             var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) | ||||
|                 ? streamBuilder.GetOptimalAudioStream(options) | ||||
|                 : streamBuilder.GetOptimalVideoStream(options); | ||||
| 
 | ||||
|             if (streamInfo is not null) | ||||
|             { | ||||
|                 streamInfo.PlaySessionId = playSessionId; | ||||
|                 streamInfo.StartPositionTicks = startTimeTicks; | ||||
| 
 | ||||
|                 mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay; | ||||
| 
 | ||||
|                 // Players do not handle this being set according to PlayMethod | ||||
|                 mediaSource.SupportsDirectStream = | ||||
|                     options.EnableDirectStream | ||||
|                         ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream | ||||
|                         : streamInfo.PlayMethod == PlayMethod.DirectPlay; | ||||
| 
 | ||||
|                 mediaSource.SupportsTranscoding = | ||||
|                     streamInfo.PlayMethod == PlayMethod.DirectStream | ||||
|                     || mediaSource.TranscodingContainer is not null | ||||
|                     || profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context); | ||||
| 
 | ||||
|                 if (item is Audio) | ||||
|                 if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream)) | ||||
|                 { | ||||
|                     if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) | ||||
|                     { | ||||
|                         mediaSource.SupportsTranscoding = false; | ||||
|                     } | ||||
|                 } | ||||
|                 else if (item is Video) | ||||
|                 { | ||||
|                     if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) | ||||
|                         && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) | ||||
|                         && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) | ||||
|                     { | ||||
|                         mediaSource.SupportsTranscoding = false; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) | ||||
|                 { | ||||
|                     mediaSource.SupportsDirectPlay = false; | ||||
|                     mediaSource.SupportsDirectStream = false; | ||||
| 
 | ||||
|                     streamInfo.PlayMethod = PlayMethod.Transcode; | ||||
|                     mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-'); | ||||
|                     mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; | ||||
|                     mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; | ||||
|                     mediaSource.TranscodingContainer = streamInfo.Container; | ||||
|                     mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream)) | ||||
| 
 | ||||
|                     if (!allowVideoStreamCopy) | ||||
|                     { | ||||
|                         streamInfo.PlayMethod = PlayMethod.Transcode; | ||||
|                         mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-'); | ||||
| 
 | ||||
|                         if (!allowVideoStreamCopy) | ||||
|                         { | ||||
|                             mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; | ||||
|                         } | ||||
| 
 | ||||
|                         if (!allowAudioStreamCopy) | ||||
|                         { | ||||
|                             mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 // Do this after the above so that StartPositionTicks is set | ||||
|                 // The token must not be null | ||||
|                 SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!); | ||||
|                 mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; | ||||
|             } | ||||
| 
 | ||||
|             foreach (var attachment in mediaSource.MediaAttachments) | ||||
|             { | ||||
|                 attachment.DeliveryUrl = string.Format( | ||||
|                     CultureInfo.InvariantCulture, | ||||
|                     "/Videos/{0}/{1}/Attachments/{2}", | ||||
|                     item.Id, | ||||
|                     mediaSource.Id, | ||||
|                     attachment.Index); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Sort media source. | ||||
|         /// </summary> | ||||
|         /// <param name="result">Playback info response.</param> | ||||
|         /// <param name="maxBitrate">Max bitrate.</param> | ||||
|         public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate) | ||||
|         { | ||||
|             var originalList = result.MediaSources.ToList(); | ||||
| 
 | ||||
|             result.MediaSources = result.MediaSources.OrderBy(i => | ||||
|                 { | ||||
|                     // Nothing beats direct playing a file | ||||
|                     if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File) | ||||
|                     { | ||||
|                         return 0; | ||||
|                         mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; | ||||
|                     } | ||||
| 
 | ||||
|                     return 1; | ||||
|                 }) | ||||
|                 .ThenBy(i => | ||||
|                 { | ||||
|                     // Let's assume direct streaming a file is just as desirable as direct playing a remote url | ||||
|                     if (i.SupportsDirectPlay || i.SupportsDirectStream) | ||||
|                     if (!allowAudioStreamCopy) | ||||
|                     { | ||||
|                         return 0; | ||||
|                     } | ||||
| 
 | ||||
|                     return 1; | ||||
|                 }) | ||||
|                 .ThenBy(i => | ||||
|                 { | ||||
|                     return i.Protocol switch | ||||
|                     { | ||||
|                         MediaProtocol.File => 0, | ||||
|                         _ => 1, | ||||
|                     }; | ||||
|                 }) | ||||
|                 .ThenBy(i => | ||||
|                 { | ||||
|                     if (maxBitrate.HasValue && i.Bitrate.HasValue) | ||||
|                     { | ||||
|                         return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2; | ||||
|                     } | ||||
| 
 | ||||
|                     return 1; | ||||
|                 }) | ||||
|                 .ThenBy(originalList.IndexOf) | ||||
|                 .ToArray(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Open media source. | ||||
|         /// </summary> | ||||
|         /// <param name="httpContext">Http Context.</param> | ||||
|         /// <param name="request">Live stream request.</param> | ||||
|         /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns> | ||||
|         public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request) | ||||
|         { | ||||
|             var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false); | ||||
| 
 | ||||
|             var profile = request.DeviceProfile; | ||||
|             if (profile is null) | ||||
|             { | ||||
|                 var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId()); | ||||
|                 if (clientCapabilities is not null) | ||||
|                 { | ||||
|                     profile = clientCapabilities.DeviceProfile; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (profile is not null) | ||||
|             { | ||||
|                 var item = _libraryManager.GetItemById(request.ItemId); | ||||
| 
 | ||||
|                 SetDeviceSpecificData( | ||||
|                     item, | ||||
|                     result.MediaSource, | ||||
|                     profile, | ||||
|                     httpContext.User, | ||||
|                     request.MaxStreamingBitrate, | ||||
|                     request.StartTimeTicks ?? 0, | ||||
|                     result.MediaSource.Id, | ||||
|                     request.AudioStreamIndex, | ||||
|                     request.SubtitleStreamIndex, | ||||
|                     request.MaxAudioChannels, | ||||
|                     request.PlaySessionId, | ||||
|                     request.UserId, | ||||
|                     request.EnableDirectPlay, | ||||
|                     request.EnableDirectStream, | ||||
|                     true, | ||||
|                     true, | ||||
|                     true, | ||||
|                     httpContext.GetNormalizedRemoteIp()); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl)) | ||||
|                 { | ||||
|                     result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // here was a check if (result.MediaSource is not null) but Rider said it will never be null | ||||
|             NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video); | ||||
| 
 | ||||
|             return result; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Normalize media source container. | ||||
|         /// </summary> | ||||
|         /// <param name="mediaSource">Media source.</param> | ||||
|         /// <param name="profile">Device profile.</param> | ||||
|         /// <param name="type">Dlna profile type.</param> | ||||
|         public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type) | ||||
|         { | ||||
|             mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type); | ||||
|         } | ||||
| 
 | ||||
|         private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken) | ||||
|         { | ||||
|             var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken); | ||||
|             mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex; | ||||
| 
 | ||||
|             mediaSource.TranscodeReasons = info.TranscodeReasons; | ||||
| 
 | ||||
|             foreach (var profile in profiles) | ||||
|             { | ||||
|                 foreach (var stream in mediaSource.MediaStreams) | ||||
|                 { | ||||
|                     if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index) | ||||
|                     { | ||||
|                         stream.DeliveryMethod = profile.DeliveryMethod; | ||||
| 
 | ||||
|                         if (profile.DeliveryMethod == SubtitleDeliveryMethod.External) | ||||
|                         { | ||||
|                             stream.DeliveryUrl = profile.Url.TrimStart('-'); | ||||
|                             stream.IsExternalUrl = profile.IsExternalUrl; | ||||
|                         } | ||||
|                         mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Do this after the above so that StartPositionTicks is set | ||||
|             // The token must not be null | ||||
|             SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, claimsPrincipal.GetToken()!); | ||||
|             mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex; | ||||
|         } | ||||
| 
 | ||||
|         private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress) | ||||
|         foreach (var attachment in mediaSource.MediaAttachments) | ||||
|         { | ||||
|             var maxBitrate = clientMaxBitrate; | ||||
|             var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0; | ||||
| 
 | ||||
|             if (remoteClientMaxBitrate <= 0) | ||||
|             { | ||||
|                 remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit; | ||||
|             } | ||||
| 
 | ||||
|             if (remoteClientMaxBitrate > 0) | ||||
|             { | ||||
|                 var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress); | ||||
| 
 | ||||
|                 _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork); | ||||
|                 if (!isInLocalNetwork) | ||||
|                 { | ||||
|                     maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return maxBitrate; | ||||
|             attachment.DeliveryUrl = string.Format( | ||||
|                 CultureInfo.InvariantCulture, | ||||
|                 "/Videos/{0}/{1}/Attachments/{2}", | ||||
|                 item.Id, | ||||
|                 mediaSource.Id, | ||||
|                 attachment.Index); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Sort media source. | ||||
|     /// </summary> | ||||
|     /// <param name="result">Playback info response.</param> | ||||
|     /// <param name="maxBitrate">Max bitrate.</param> | ||||
|     public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate) | ||||
|     { | ||||
|         var originalList = result.MediaSources.ToList(); | ||||
| 
 | ||||
|         result.MediaSources = result.MediaSources.OrderBy(i => | ||||
|             { | ||||
|                 // Nothing beats direct playing a file | ||||
|                 if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File) | ||||
|                 { | ||||
|                     return 0; | ||||
|                 } | ||||
| 
 | ||||
|                 return 1; | ||||
|             }) | ||||
|             .ThenBy(i => | ||||
|             { | ||||
|                 // Let's assume direct streaming a file is just as desirable as direct playing a remote url | ||||
|                 if (i.SupportsDirectPlay || i.SupportsDirectStream) | ||||
|                 { | ||||
|                     return 0; | ||||
|                 } | ||||
| 
 | ||||
|                 return 1; | ||||
|             }) | ||||
|             .ThenBy(i => | ||||
|             { | ||||
|                 return i.Protocol switch | ||||
|                 { | ||||
|                     MediaProtocol.File => 0, | ||||
|                     _ => 1, | ||||
|                 }; | ||||
|             }) | ||||
|             .ThenBy(i => | ||||
|             { | ||||
|                 if (maxBitrate.HasValue && i.Bitrate.HasValue) | ||||
|                 { | ||||
|                     return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2; | ||||
|                 } | ||||
| 
 | ||||
|                 return 1; | ||||
|             }) | ||||
|             .ThenBy(originalList.IndexOf) | ||||
|             .ToArray(); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Open media source. | ||||
|     /// </summary> | ||||
|     /// <param name="httpContext">Http Context.</param> | ||||
|     /// <param name="request">Live stream request.</param> | ||||
|     /// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns> | ||||
|     public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request) | ||||
|     { | ||||
|         var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false); | ||||
| 
 | ||||
|         var profile = request.DeviceProfile; | ||||
|         if (profile is null) | ||||
|         { | ||||
|             var clientCapabilities = _deviceManager.GetCapabilities(httpContext.User.GetDeviceId()); | ||||
|             if (clientCapabilities is not null) | ||||
|             { | ||||
|                 profile = clientCapabilities.DeviceProfile; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (profile is not null) | ||||
|         { | ||||
|             var item = _libraryManager.GetItemById(request.ItemId); | ||||
| 
 | ||||
|             SetDeviceSpecificData( | ||||
|                 item, | ||||
|                 result.MediaSource, | ||||
|                 profile, | ||||
|                 httpContext.User, | ||||
|                 request.MaxStreamingBitrate, | ||||
|                 request.StartTimeTicks ?? 0, | ||||
|                 result.MediaSource.Id, | ||||
|                 request.AudioStreamIndex, | ||||
|                 request.SubtitleStreamIndex, | ||||
|                 request.MaxAudioChannels, | ||||
|                 request.PlaySessionId, | ||||
|                 request.UserId, | ||||
|                 request.EnableDirectPlay, | ||||
|                 request.EnableDirectStream, | ||||
|                 true, | ||||
|                 true, | ||||
|                 true, | ||||
|                 httpContext.GetNormalizedRemoteIp()); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl)) | ||||
|             { | ||||
|                 result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // here was a check if (result.MediaSource is not null) but Rider said it will never be null | ||||
|         NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video); | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Normalize media source container. | ||||
|     /// </summary> | ||||
|     /// <param name="mediaSource">Media source.</param> | ||||
|     /// <param name="profile">Device profile.</param> | ||||
|     /// <param name="type">Dlna profile type.</param> | ||||
|     public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type) | ||||
|     { | ||||
|         mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, profile, type); | ||||
|     } | ||||
| 
 | ||||
|     private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken) | ||||
|     { | ||||
|         var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken); | ||||
|         mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex; | ||||
| 
 | ||||
|         mediaSource.TranscodeReasons = info.TranscodeReasons; | ||||
| 
 | ||||
|         foreach (var profile in profiles) | ||||
|         { | ||||
|             foreach (var stream in mediaSource.MediaStreams) | ||||
|             { | ||||
|                 if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index) | ||||
|                 { | ||||
|                     stream.DeliveryMethod = profile.DeliveryMethod; | ||||
| 
 | ||||
|                     if (profile.DeliveryMethod == SubtitleDeliveryMethod.External) | ||||
|                     { | ||||
|                         stream.DeliveryUrl = profile.Url.TrimStart('-'); | ||||
|                         stream.IsExternalUrl = profile.IsExternalUrl; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private int? GetMaxBitrate(int? clientMaxBitrate, User user, IPAddress ipAddress) | ||||
|     { | ||||
|         var maxBitrate = clientMaxBitrate; | ||||
|         var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0; | ||||
| 
 | ||||
|         if (remoteClientMaxBitrate <= 0) | ||||
|         { | ||||
|             remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit; | ||||
|         } | ||||
| 
 | ||||
|         if (remoteClientMaxBitrate > 0) | ||||
|         { | ||||
|             var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress); | ||||
| 
 | ||||
|             _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork); | ||||
|             if (!isInLocalNetwork) | ||||
|             { | ||||
|                 maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return maxBitrate; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -6,178 +6,177 @@ using System.Threading.Tasks; | ||||
| using Jellyfin.Api.Models.PlaybackDtos; | ||||
| using MediaBrowser.Model.IO; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Helpers | ||||
| namespace Jellyfin.Api.Helpers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// A progressive file stream for transferring transcoded files as they are written to. | ||||
| /// </summary> | ||||
| public class ProgressiveFileStream : Stream | ||||
| { | ||||
|     private readonly Stream _stream; | ||||
|     private readonly TranscodingJobDto? _job; | ||||
|     private readonly TranscodingJobHelper? _transcodingJobHelper; | ||||
|     private readonly int _timeoutMs; | ||||
|     private bool _disposed; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// A progressive file stream for transferring transcoded files as they are written to. | ||||
|     /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class. | ||||
|     /// </summary> | ||||
|     public class ProgressiveFileStream : Stream | ||||
|     /// <param name="filePath">The path to the transcoded file.</param> | ||||
|     /// <param name="job">The transcoding job information.</param> | ||||
|     /// <param name="transcodingJobHelper">The transcoding job helper.</param> | ||||
|     /// <param name="timeoutMs">The timeout duration in milliseconds.</param> | ||||
|     public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000) | ||||
|     { | ||||
|         private readonly Stream _stream; | ||||
|         private readonly TranscodingJobDto? _job; | ||||
|         private readonly TranscodingJobHelper? _transcodingJobHelper; | ||||
|         private readonly int _timeoutMs; | ||||
|         private bool _disposed; | ||||
|         _job = job; | ||||
|         _transcodingJobHelper = transcodingJobHelper; | ||||
|         _timeoutMs = timeoutMs; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="filePath">The path to the transcoded file.</param> | ||||
|         /// <param name="job">The transcoding job information.</param> | ||||
|         /// <param name="transcodingJobHelper">The transcoding job helper.</param> | ||||
|         /// <param name="timeoutMs">The timeout duration in milliseconds.</param> | ||||
|         public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000) | ||||
|         _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class. | ||||
|     /// </summary> | ||||
|     /// <param name="stream">The stream to progressively copy.</param> | ||||
|     /// <param name="timeoutMs">The timeout duration in milliseconds.</param> | ||||
|     public ProgressiveFileStream(Stream stream, int timeoutMs = 30000) | ||||
|     { | ||||
|         _job = null; | ||||
|         _transcodingJobHelper = null; | ||||
|         _timeoutMs = timeoutMs; | ||||
|         _stream = stream; | ||||
|     } | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     public override bool CanRead => _stream.CanRead; | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     public override bool CanSeek => false; | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     public override bool CanWrite => false; | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     public override long Length => throw new NotSupportedException(); | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     public override long Position | ||||
|     { | ||||
|         get => throw new NotSupportedException(); | ||||
|         set => throw new NotSupportedException(); | ||||
|     } | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     public override void Flush() | ||||
|     { | ||||
|         // Not supported | ||||
|     } | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     public override int Read(byte[] buffer, int offset, int count) | ||||
|         => Read(buffer.AsSpan(offset, count)); | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     public override int Read(Span<byte> buffer) | ||||
|     { | ||||
|         int totalBytesRead = 0; | ||||
|         var stopwatch = Stopwatch.StartNew(); | ||||
| 
 | ||||
|         while (true) | ||||
|         { | ||||
|             _job = job; | ||||
|             _transcodingJobHelper = transcodingJobHelper; | ||||
|             _timeoutMs = timeoutMs; | ||||
| 
 | ||||
|             _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="stream">The stream to progressively copy.</param> | ||||
|         /// <param name="timeoutMs">The timeout duration in milliseconds.</param> | ||||
|         public ProgressiveFileStream(Stream stream, int timeoutMs = 30000) | ||||
|         { | ||||
|             _job = null; | ||||
|             _transcodingJobHelper = null; | ||||
|             _timeoutMs = timeoutMs; | ||||
|             _stream = stream; | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override bool CanRead => _stream.CanRead; | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override bool CanSeek => false; | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override bool CanWrite => false; | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override long Length => throw new NotSupportedException(); | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override long Position | ||||
|         { | ||||
|             get => throw new NotSupportedException(); | ||||
|             set => throw new NotSupportedException(); | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override void Flush() | ||||
|         { | ||||
|             // Not supported | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override int Read(byte[] buffer, int offset, int count) | ||||
|             => Read(buffer.AsSpan(offset, count)); | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override int Read(Span<byte> buffer) | ||||
|         { | ||||
|             int totalBytesRead = 0; | ||||
|             var stopwatch = Stopwatch.StartNew(); | ||||
| 
 | ||||
|             while (true) | ||||
|             totalBytesRead += _stream.Read(buffer); | ||||
|             if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds)) | ||||
|             { | ||||
|                 totalBytesRead += _stream.Read(buffer); | ||||
|                 if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds)) | ||||
|                 { | ||||
|                     break; | ||||
|                 } | ||||
| 
 | ||||
|                 Thread.Sleep(50); | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             UpdateBytesWritten(totalBytesRead); | ||||
| 
 | ||||
|             return totalBytesRead; | ||||
|             Thread.Sleep(50); | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | ||||
|             => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); | ||||
|         UpdateBytesWritten(totalBytesRead); | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) | ||||
|         return totalBytesRead; | ||||
|     } | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | ||||
|         => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) | ||||
|     { | ||||
|         int totalBytesRead = 0; | ||||
|         var stopwatch = Stopwatch.StartNew(); | ||||
| 
 | ||||
|         while (true) | ||||
|         { | ||||
|             int totalBytesRead = 0; | ||||
|             var stopwatch = Stopwatch.StartNew(); | ||||
| 
 | ||||
|             while (true) | ||||
|             totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); | ||||
|             if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds)) | ||||
|             { | ||||
|                 totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); | ||||
|                 if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds)) | ||||
|                 { | ||||
|                     break; | ||||
|                 } | ||||
| 
 | ||||
|                 await Task.Delay(50, cancellationToken).ConfigureAwait(false); | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             UpdateBytesWritten(totalBytesRead); | ||||
| 
 | ||||
|             return totalBytesRead; | ||||
|             await Task.Delay(50, cancellationToken).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override long Seek(long offset, SeekOrigin origin) | ||||
|             => throw new NotSupportedException(); | ||||
|         UpdateBytesWritten(totalBytesRead); | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override void SetLength(long value) | ||||
|             => throw new NotSupportedException(); | ||||
|         return totalBytesRead; | ||||
|     } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         public override void Write(byte[] buffer, int offset, int count) | ||||
|             => throw new NotSupportedException(); | ||||
|     /// <inheritdoc /> | ||||
|     public override long Seek(long offset, SeekOrigin origin) | ||||
|         => throw new NotSupportedException(); | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Dispose(bool disposing) | ||||
|     /// <inheritdoc /> | ||||
|     public override void SetLength(long value) | ||||
|         => throw new NotSupportedException(); | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     public override void Write(byte[] buffer, int offset, int count) | ||||
|         => throw new NotSupportedException(); | ||||
| 
 | ||||
|     /// <inheritdoc /> | ||||
|     protected override void Dispose(bool disposing) | ||||
|     { | ||||
|         if (_disposed) | ||||
|         { | ||||
|             if (_disposed) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|             try | ||||
|         try | ||||
|         { | ||||
|             if (disposing) | ||||
|             { | ||||
|                 if (disposing) | ||||
|                 _stream.Dispose(); | ||||
| 
 | ||||
|                 if (_job is not null) | ||||
|                 { | ||||
|                     _stream.Dispose(); | ||||
| 
 | ||||
|                     if (_job is not null) | ||||
|                     { | ||||
|                         _transcodingJobHelper?.OnTranscodeEndRequest(_job); | ||||
|                     } | ||||
|                     _transcodingJobHelper?.OnTranscodeEndRequest(_job); | ||||
|                 } | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 _disposed = true; | ||||
|                 base.Dispose(disposing); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private void UpdateBytesWritten(int totalBytesRead) | ||||
|         finally | ||||
|         { | ||||
|             if (_job is not null) | ||||
|             { | ||||
|                 _job.BytesDownloaded += totalBytesRead; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private bool StopReading(int bytesRead, long elapsed) | ||||
|         { | ||||
|             // It should stop reading when anything has been successfully read or if the job has exited | ||||
|             // If the job is null, however, it's a live stream and will require user action to close, | ||||
|             // but don't keep it open indefinitely if it isn't reading anything | ||||
|             return bytesRead > 0 || (_job?.HasExited ?? elapsed >= _timeoutMs); | ||||
|             _disposed = true; | ||||
|             base.Dispose(disposing); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void UpdateBytesWritten(int totalBytesRead) | ||||
|     { | ||||
|         if (_job is not null) | ||||
|         { | ||||
|             _job.BytesDownloaded += totalBytesRead; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private bool StopReading(int bytesRead, long elapsed) | ||||
|     { | ||||
|         // It should stop reading when anything has been successfully read or if the job has exited | ||||
|         // If the job is null, however, it's a live stream and will require user action to close, | ||||
|         // but don't keep it open indefinitely if it isn't reading anything | ||||
|         return bytesRead > 0 || (_job?.HasExited ?? elapsed >= _timeoutMs); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -16,133 +16,132 @@ using MediaBrowser.Model.Dto; | ||||
| using MediaBrowser.Model.Querying; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Helpers | ||||
| namespace Jellyfin.Api.Helpers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Request Extensions. | ||||
| /// </summary> | ||||
| public static class RequestHelpers | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Request Extensions. | ||||
|     /// Get Order By. | ||||
|     /// </summary> | ||||
|     public static class RequestHelpers | ||||
|     /// <param name="sortBy">Sort By. Comma delimited string.</param> | ||||
|     /// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param> | ||||
|     /// <returns>Order By.</returns> | ||||
|     public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder) | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Get Order By. | ||||
|         /// </summary> | ||||
|         /// <param name="sortBy">Sort By. Comma delimited string.</param> | ||||
|         /// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param> | ||||
|         /// <returns>Order By.</returns> | ||||
|         public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder) | ||||
|         if (sortBy.Count == 0) | ||||
|         { | ||||
|             if (sortBy.Count == 0) | ||||
|             { | ||||
|                 return Array.Empty<(string, SortOrder)>(); | ||||
|             } | ||||
| 
 | ||||
|             var result = new (string, SortOrder)[sortBy.Count]; | ||||
|             var i = 0; | ||||
|             // Add elements which have a SortOrder specified | ||||
|             for (; i < requestedSortOrder.Count; i++) | ||||
|             { | ||||
|                 result[i] = (sortBy[i], requestedSortOrder[i]); | ||||
|             } | ||||
| 
 | ||||
|             // Add remaining elements with the first specified SortOrder | ||||
|             // or the default one if no SortOrders are specified | ||||
|             var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending; | ||||
|             for (; i < sortBy.Count; i++) | ||||
|             { | ||||
|                 result[i] = (sortBy[i], order); | ||||
|             } | ||||
| 
 | ||||
|             return result; | ||||
|             return Array.Empty<(string, SortOrder)>(); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Checks if the user can update an entry. | ||||
|         /// </summary> | ||||
|         /// <param name="userManager">An instance of the <see cref="IUserManager"/> interface.</param> | ||||
|         /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param> | ||||
|         /// <param name="userId">The user id.</param> | ||||
|         /// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param> | ||||
|         /// <returns>A <see cref="bool"/> whether the user can update the entry.</returns> | ||||
|         internal static bool AssertCanUpdateUser(IUserManager userManager, ClaimsPrincipal claimsPrincipal, Guid userId, bool restrictUserPreferences) | ||||
|         var result = new (string, SortOrder)[sortBy.Count]; | ||||
|         var i = 0; | ||||
|         // Add elements which have a SortOrder specified | ||||
|         for (; i < requestedSortOrder.Count; i++) | ||||
|         { | ||||
|             var authenticatedUserId = claimsPrincipal.GetUserId(); | ||||
|             var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator); | ||||
|             result[i] = (sortBy[i], requestedSortOrder[i]); | ||||
|         } | ||||
| 
 | ||||
|             // If they're going to update the record of another user, they must be an administrator | ||||
|             if (!userId.Equals(authenticatedUserId) && !isAdministrator) | ||||
|         // Add remaining elements with the first specified SortOrder | ||||
|         // or the default one if no SortOrders are specified | ||||
|         var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending; | ||||
|         for (; i < sortBy.Count; i++) | ||||
|         { | ||||
|             result[i] = (sortBy[i], order); | ||||
|         } | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Checks if the user can update an entry. | ||||
|     /// </summary> | ||||
|     /// <param name="userManager">An instance of the <see cref="IUserManager"/> interface.</param> | ||||
|     /// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> for the current request.</param> | ||||
|     /// <param name="userId">The user id.</param> | ||||
|     /// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param> | ||||
|     /// <returns>A <see cref="bool"/> whether the user can update the entry.</returns> | ||||
|     internal static bool AssertCanUpdateUser(IUserManager userManager, ClaimsPrincipal claimsPrincipal, Guid userId, bool restrictUserPreferences) | ||||
|     { | ||||
|         var authenticatedUserId = claimsPrincipal.GetUserId(); | ||||
|         var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator); | ||||
| 
 | ||||
|         // If they're going to update the record of another user, they must be an administrator | ||||
|         if (!userId.Equals(authenticatedUserId) && !isAdministrator) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         // TODO the EnableUserPreferenceAccess policy does not seem to be used elsewhere | ||||
|         if (!restrictUserPreferences || isAdministrator) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         var user = userManager.GetUserById(userId); | ||||
|         return user.EnableUserPreferenceAccess; | ||||
|     } | ||||
| 
 | ||||
|     internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext) | ||||
|     { | ||||
|         var userId = httpContext.User.GetUserId(); | ||||
|         var user = userManager.GetUserById(userId); | ||||
|         var session = await sessionManager.LogSessionActivity( | ||||
|             httpContext.User.GetClient(), | ||||
|             httpContext.User.GetVersion(), | ||||
|             httpContext.User.GetDeviceId(), | ||||
|             httpContext.User.GetDevice(), | ||||
|             httpContext.GetNormalizedRemoteIp().ToString(), | ||||
|             user).ConfigureAwait(false); | ||||
| 
 | ||||
|         if (session is null) | ||||
|         { | ||||
|             throw new ArgumentException("Session not found."); | ||||
|         } | ||||
| 
 | ||||
|         return session; | ||||
|     } | ||||
| 
 | ||||
|     internal static async Task<string> GetSessionId(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext) | ||||
|     { | ||||
|         var session = await GetSession(sessionManager, userManager, httpContext).ConfigureAwait(false); | ||||
| 
 | ||||
|         return session.Id; | ||||
|     } | ||||
| 
 | ||||
|     internal static QueryResult<BaseItemDto> CreateQueryResult( | ||||
|         QueryResult<(BaseItem Item, ItemCounts ItemCounts)> result, | ||||
|         DtoOptions dtoOptions, | ||||
|         IDtoService dtoService, | ||||
|         bool includeItemTypes, | ||||
|         User? user) | ||||
|     { | ||||
|         var dtos = result.Items.Select(i => | ||||
|         { | ||||
|             var (baseItem, counts) = i; | ||||
|             var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); | ||||
| 
 | ||||
|             if (includeItemTypes) | ||||
|             { | ||||
|                 return false; | ||||
|                 dto.ChildCount = counts.ItemCount; | ||||
|                 dto.ProgramCount = counts.ProgramCount; | ||||
|                 dto.SeriesCount = counts.SeriesCount; | ||||
|                 dto.EpisodeCount = counts.EpisodeCount; | ||||
|                 dto.MovieCount = counts.MovieCount; | ||||
|                 dto.TrailerCount = counts.TrailerCount; | ||||
|                 dto.AlbumCount = counts.AlbumCount; | ||||
|                 dto.SongCount = counts.SongCount; | ||||
|                 dto.ArtistCount = counts.ArtistCount; | ||||
|             } | ||||
| 
 | ||||
|             // TODO the EnableUserPreferenceAccess policy does not seem to be used elsewhere | ||||
|             if (!restrictUserPreferences || isAdministrator) | ||||
|             { | ||||
|                 return true; | ||||
|             } | ||||
|             return dto; | ||||
|         }); | ||||
| 
 | ||||
|             var user = userManager.GetUserById(userId); | ||||
|             return user.EnableUserPreferenceAccess; | ||||
|         } | ||||
| 
 | ||||
|         internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext) | ||||
|         { | ||||
|             var userId = httpContext.User.GetUserId(); | ||||
|             var user = userManager.GetUserById(userId); | ||||
|             var session = await sessionManager.LogSessionActivity( | ||||
|                 httpContext.User.GetClient(), | ||||
|                 httpContext.User.GetVersion(), | ||||
|                 httpContext.User.GetDeviceId(), | ||||
|                 httpContext.User.GetDevice(), | ||||
|                 httpContext.GetNormalizedRemoteIp().ToString(), | ||||
|                 user).ConfigureAwait(false); | ||||
| 
 | ||||
|             if (session is null) | ||||
|             { | ||||
|                 throw new ArgumentException("Session not found."); | ||||
|             } | ||||
| 
 | ||||
|             return session; | ||||
|         } | ||||
| 
 | ||||
|         internal static async Task<string> GetSessionId(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext) | ||||
|         { | ||||
|             var session = await GetSession(sessionManager, userManager, httpContext).ConfigureAwait(false); | ||||
| 
 | ||||
|             return session.Id; | ||||
|         } | ||||
| 
 | ||||
|         internal static QueryResult<BaseItemDto> CreateQueryResult( | ||||
|             QueryResult<(BaseItem Item, ItemCounts ItemCounts)> result, | ||||
|             DtoOptions dtoOptions, | ||||
|             IDtoService dtoService, | ||||
|             bool includeItemTypes, | ||||
|             User? user) | ||||
|         { | ||||
|             var dtos = result.Items.Select(i => | ||||
|             { | ||||
|                 var (baseItem, counts) = i; | ||||
|                 var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); | ||||
| 
 | ||||
|                 if (includeItemTypes) | ||||
|                 { | ||||
|                     dto.ChildCount = counts.ItemCount; | ||||
|                     dto.ProgramCount = counts.ProgramCount; | ||||
|                     dto.SeriesCount = counts.SeriesCount; | ||||
|                     dto.EpisodeCount = counts.EpisodeCount; | ||||
|                     dto.MovieCount = counts.MovieCount; | ||||
|                     dto.TrailerCount = counts.TrailerCount; | ||||
|                     dto.AlbumCount = counts.AlbumCount; | ||||
|                     dto.SongCount = counts.SongCount; | ||||
|                     dto.ArtistCount = counts.ArtistCount; | ||||
|                 } | ||||
| 
 | ||||
|                 return dto; | ||||
|             }); | ||||
| 
 | ||||
|             return new QueryResult<BaseItemDto>( | ||||
|                 result.StartIndex, | ||||
|                 result.TotalRecordCount, | ||||
|                 dtos.ToArray()); | ||||
|         } | ||||
|         return new QueryResult<BaseItemDto>( | ||||
|             result.StartIndex, | ||||
|             result.TotalRecordCount, | ||||
|             dtos.ToArray()); | ||||
|     } | ||||
| } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -7,75 +7,74 @@ using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Middleware | ||||
| namespace Jellyfin.Api.Middleware; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Redirect requests without baseurl prefix to the baseurl prefixed URL. | ||||
| /// </summary> | ||||
| public class BaseUrlRedirectionMiddleware | ||||
| { | ||||
|     private readonly RequestDelegate _next; | ||||
|     private readonly ILogger<BaseUrlRedirectionMiddleware> _logger; | ||||
|     private readonly IConfiguration _configuration; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Redirect requests without baseurl prefix to the baseurl prefixed URL. | ||||
|     /// Initializes a new instance of the <see cref="BaseUrlRedirectionMiddleware"/> class. | ||||
|     /// </summary> | ||||
|     public class BaseUrlRedirectionMiddleware | ||||
|     /// <param name="next">The next delegate in the pipeline.</param> | ||||
|     /// <param name="logger">The logger.</param> | ||||
|     /// <param name="configuration">The application configuration.</param> | ||||
|     public BaseUrlRedirectionMiddleware( | ||||
|         RequestDelegate next, | ||||
|         ILogger<BaseUrlRedirectionMiddleware> logger, | ||||
|         IConfiguration configuration) | ||||
|     { | ||||
|         private readonly RequestDelegate _next; | ||||
|         private readonly ILogger<BaseUrlRedirectionMiddleware> _logger; | ||||
|         private readonly IConfiguration _configuration; | ||||
|         _next = next; | ||||
|         _logger = logger; | ||||
|         _configuration = configuration; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="BaseUrlRedirectionMiddleware"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="next">The next delegate in the pipeline.</param> | ||||
|         /// <param name="logger">The logger.</param> | ||||
|         /// <param name="configuration">The application configuration.</param> | ||||
|         public BaseUrlRedirectionMiddleware( | ||||
|             RequestDelegate next, | ||||
|             ILogger<BaseUrlRedirectionMiddleware> logger, | ||||
|             IConfiguration configuration) | ||||
|     /// <summary> | ||||
|     /// Executes the middleware action. | ||||
|     /// </summary> | ||||
|     /// <param name="httpContext">The current HTTP context.</param> | ||||
|     /// <param name="serverConfigurationManager">The server configuration manager.</param> | ||||
|     /// <returns>The async task.</returns> | ||||
|     public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager) | ||||
|     { | ||||
|         var localPath = httpContext.Request.Path.ToString(); | ||||
|         var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl; | ||||
| 
 | ||||
|         if (string.IsNullOrEmpty(localPath) | ||||
|             || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) | ||||
|             || string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) | ||||
|             || string.Equals(localPath, baseUrlPrefix + "/web", StringComparison.OrdinalIgnoreCase) | ||||
|             || string.Equals(localPath, baseUrlPrefix + "/web/", StringComparison.OrdinalIgnoreCase) | ||||
|             || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase) | ||||
|            ) | ||||
|         { | ||||
|             _next = next; | ||||
|             _logger = logger; | ||||
|             _configuration = configuration; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Executes the middleware action. | ||||
|         /// </summary> | ||||
|         /// <param name="httpContext">The current HTTP context.</param> | ||||
|         /// <param name="serverConfigurationManager">The server configuration manager.</param> | ||||
|         /// <returns>The async task.</returns> | ||||
|         public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager) | ||||
|         { | ||||
|             var localPath = httpContext.Request.Path.ToString(); | ||||
|             var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl; | ||||
| 
 | ||||
|             if (string.IsNullOrEmpty(localPath) | ||||
|                 || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(localPath, baseUrlPrefix + "/web", StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(localPath, baseUrlPrefix + "/web/", StringComparison.OrdinalIgnoreCase) | ||||
|                 || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase) | ||||
|                ) | ||||
|             // Redirect health endpoint | ||||
|             if (string.Equals(localPath, "/health", StringComparison.OrdinalIgnoreCase) | ||||
|                 || string.Equals(localPath, "/health/", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 // Redirect health endpoint | ||||
|                 if (string.Equals(localPath, "/health", StringComparison.OrdinalIgnoreCase) | ||||
|                     || string.Equals(localPath, "/health/", StringComparison.OrdinalIgnoreCase)) | ||||
|                 { | ||||
|                     _logger.LogDebug("Redirecting /health check"); | ||||
|                     httpContext.Response.Redirect(baseUrlPrefix + "/health"); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 // Always redirect back to the default path if the base prefix is invalid or missing | ||||
|                 _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); | ||||
| 
 | ||||
|                 var port = httpContext.Request.Host.Port ?? -1; | ||||
|                 var uri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, localPath).Uri; | ||||
|                 var redirectUri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]).Uri; | ||||
|                 var target = uri.MakeRelativeUri(redirectUri).ToString(); | ||||
|                 _logger.LogDebug("Redirecting to {Target}", target); | ||||
| 
 | ||||
|                 httpContext.Response.Redirect(target); | ||||
|                 _logger.LogDebug("Redirecting /health check"); | ||||
|                 httpContext.Response.Redirect(baseUrlPrefix + "/health"); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             await _next(httpContext).ConfigureAwait(false); | ||||
|             // Always redirect back to the default path if the base prefix is invalid or missing | ||||
|             _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); | ||||
| 
 | ||||
|             var port = httpContext.Request.Host.Port ?? -1; | ||||
|             var uri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, localPath).Uri; | ||||
|             var redirectUri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]).Uri; | ||||
|             var target = uri.MakeRelativeUri(redirectUri).ToString(); | ||||
|             _logger.LogDebug("Redirecting to {Target}", target); | ||||
| 
 | ||||
|             httpContext.Response.Redirect(target); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await _next(httpContext).ConfigureAwait(false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -12,140 +12,139 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.Extensions.Hosting; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Middleware | ||||
| namespace Jellyfin.Api.Middleware; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Exception Middleware. | ||||
| /// </summary> | ||||
| public class ExceptionMiddleware | ||||
| { | ||||
|     private readonly RequestDelegate _next; | ||||
|     private readonly ILogger<ExceptionMiddleware> _logger; | ||||
|     private readonly IServerConfigurationManager _configuration; | ||||
|     private readonly IWebHostEnvironment _hostEnvironment; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Exception Middleware. | ||||
|     /// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class. | ||||
|     /// </summary> | ||||
|     public class ExceptionMiddleware | ||||
|     /// <param name="next">Next request delegate.</param> | ||||
|     /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param> | ||||
|     /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     /// <param name="hostEnvironment">Instance of the <see cref="IWebHostEnvironment"/> interface.</param> | ||||
|     public ExceptionMiddleware( | ||||
|         RequestDelegate next, | ||||
|         ILogger<ExceptionMiddleware> logger, | ||||
|         IServerConfigurationManager serverConfigurationManager, | ||||
|         IWebHostEnvironment hostEnvironment) | ||||
|     { | ||||
|         private readonly RequestDelegate _next; | ||||
|         private readonly ILogger<ExceptionMiddleware> _logger; | ||||
|         private readonly IServerConfigurationManager _configuration; | ||||
|         private readonly IWebHostEnvironment _hostEnvironment; | ||||
|         _next = next; | ||||
|         _logger = logger; | ||||
|         _configuration = serverConfigurationManager; | ||||
|         _hostEnvironment = hostEnvironment; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="next">Next request delegate.</param> | ||||
|         /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <param name="hostEnvironment">Instance of the <see cref="IWebHostEnvironment"/> interface.</param> | ||||
|         public ExceptionMiddleware( | ||||
|             RequestDelegate next, | ||||
|             ILogger<ExceptionMiddleware> logger, | ||||
|             IServerConfigurationManager serverConfigurationManager, | ||||
|             IWebHostEnvironment hostEnvironment) | ||||
|     /// <summary> | ||||
|     /// Invoke request. | ||||
|     /// </summary> | ||||
|     /// <param name="context">Request context.</param> | ||||
|     /// <returns>Task.</returns> | ||||
|     public async Task Invoke(HttpContext context) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             _next = next; | ||||
|             _logger = logger; | ||||
|             _configuration = serverConfigurationManager; | ||||
|             _hostEnvironment = hostEnvironment; | ||||
|             await _next(context).ConfigureAwait(false); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Invoke request. | ||||
|         /// </summary> | ||||
|         /// <param name="context">Request context.</param> | ||||
|         /// <returns>Task.</returns> | ||||
|         public async Task Invoke(HttpContext context) | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             try | ||||
|             if (context.Response.HasStarted) | ||||
|             { | ||||
|                 await _next(context).ConfigureAwait(false); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 if (context.Response.HasStarted) | ||||
|                 { | ||||
|                     _logger.LogWarning("The response has already started, the exception middleware will not be executed."); | ||||
|                     throw; | ||||
|                 } | ||||
| 
 | ||||
|                 ex = GetActualException(ex); | ||||
| 
 | ||||
|                 bool ignoreStackTrace = | ||||
|                     ex is SocketException | ||||
|                     || ex is IOException | ||||
|                     || ex is OperationCanceledException | ||||
|                     || ex is SecurityException | ||||
|                     || ex is AuthenticationException | ||||
|                     || ex is FileNotFoundException; | ||||
| 
 | ||||
|                 if (ignoreStackTrace) | ||||
|                 { | ||||
|                     _logger.LogError( | ||||
|                         "Error processing request: {ExceptionMessage}. URL {Method} {Url}.", | ||||
|                         ex.Message.TrimEnd('.'), | ||||
|                         context.Request.Method, | ||||
|                         context.Request.Path); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     _logger.LogError( | ||||
|                         ex, | ||||
|                         "Error processing request. URL {Method} {Url}.", | ||||
|                         context.Request.Method, | ||||
|                         context.Request.Path); | ||||
|                 } | ||||
| 
 | ||||
|                 context.Response.StatusCode = GetStatusCode(ex); | ||||
|                 context.Response.ContentType = MediaTypeNames.Text.Plain; | ||||
| 
 | ||||
|                 // Don't send exception unless the server is in a Development environment | ||||
|                 var errorContent = _hostEnvironment.IsDevelopment() | ||||
|                         ? NormalizeExceptionMessage(ex.Message) | ||||
|                         : "Error processing request."; | ||||
|                 await context.Response.WriteAsync(errorContent).ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private static Exception GetActualException(Exception ex) | ||||
|         { | ||||
|             if (ex is AggregateException agg) | ||||
|             { | ||||
|                 var inner = agg.InnerException; | ||||
|                 if (inner is not null) | ||||
|                 { | ||||
|                     return GetActualException(inner); | ||||
|                 } | ||||
| 
 | ||||
|                 var inners = agg.InnerExceptions; | ||||
|                 if (inners.Count > 0) | ||||
|                 { | ||||
|                     return GetActualException(inners[0]); | ||||
|                 } | ||||
|                 _logger.LogWarning("The response has already started, the exception middleware will not be executed."); | ||||
|                 throw; | ||||
|             } | ||||
| 
 | ||||
|             return ex; | ||||
|         } | ||||
|             ex = GetActualException(ex); | ||||
| 
 | ||||
|         private static int GetStatusCode(Exception ex) | ||||
|         { | ||||
|             switch (ex) | ||||
|             bool ignoreStackTrace = | ||||
|                 ex is SocketException | ||||
|                 || ex is IOException | ||||
|                 || ex is OperationCanceledException | ||||
|                 || ex is SecurityException | ||||
|                 || ex is AuthenticationException | ||||
|                 || ex is FileNotFoundException; | ||||
| 
 | ||||
|             if (ignoreStackTrace) | ||||
|             { | ||||
|                 case ArgumentException _: return StatusCodes.Status400BadRequest; | ||||
|                 case AuthenticationException _: return StatusCodes.Status401Unauthorized; | ||||
|                 case SecurityException _: return StatusCodes.Status403Forbidden; | ||||
|                 case DirectoryNotFoundException _: | ||||
|                 case FileNotFoundException _: | ||||
|                 case ResourceNotFoundException _: return StatusCodes.Status404NotFound; | ||||
|                 case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed; | ||||
|                 default: return StatusCodes.Status500InternalServerError; | ||||
|                 _logger.LogError( | ||||
|                     "Error processing request: {ExceptionMessage}. URL {Method} {Url}.", | ||||
|                     ex.Message.TrimEnd('.'), | ||||
|                     context.Request.Method, | ||||
|                     context.Request.Path); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _logger.LogError( | ||||
|                     ex, | ||||
|                     "Error processing request. URL {Method} {Url}.", | ||||
|                     context.Request.Method, | ||||
|                     context.Request.Path); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private string NormalizeExceptionMessage(string msg) | ||||
|         { | ||||
|             // Strip any information we don't want to reveal | ||||
|             return msg.Replace( | ||||
|                     _configuration.ApplicationPaths.ProgramSystemPath, | ||||
|                     string.Empty, | ||||
|                     StringComparison.OrdinalIgnoreCase) | ||||
|                 .Replace( | ||||
|                     _configuration.ApplicationPaths.ProgramDataPath, | ||||
|                     string.Empty, | ||||
|                     StringComparison.OrdinalIgnoreCase); | ||||
|             context.Response.StatusCode = GetStatusCode(ex); | ||||
|             context.Response.ContentType = MediaTypeNames.Text.Plain; | ||||
| 
 | ||||
|             // Don't send exception unless the server is in a Development environment | ||||
|             var errorContent = _hostEnvironment.IsDevelopment() | ||||
|                     ? NormalizeExceptionMessage(ex.Message) | ||||
|                     : "Error processing request."; | ||||
|             await context.Response.WriteAsync(errorContent).ConfigureAwait(false); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static Exception GetActualException(Exception ex) | ||||
|     { | ||||
|         if (ex is AggregateException agg) | ||||
|         { | ||||
|             var inner = agg.InnerException; | ||||
|             if (inner is not null) | ||||
|             { | ||||
|                 return GetActualException(inner); | ||||
|             } | ||||
| 
 | ||||
|             var inners = agg.InnerExceptions; | ||||
|             if (inners.Count > 0) | ||||
|             { | ||||
|                 return GetActualException(inners[0]); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return ex; | ||||
|     } | ||||
| 
 | ||||
|     private static int GetStatusCode(Exception ex) | ||||
|     { | ||||
|         switch (ex) | ||||
|         { | ||||
|             case ArgumentException _: return StatusCodes.Status400BadRequest; | ||||
|             case AuthenticationException _: return StatusCodes.Status401Unauthorized; | ||||
|             case SecurityException _: return StatusCodes.Status403Forbidden; | ||||
|             case DirectoryNotFoundException _: | ||||
|             case FileNotFoundException _: | ||||
|             case ResourceNotFoundException _: return StatusCodes.Status404NotFound; | ||||
|             case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed; | ||||
|             default: return StatusCodes.Status500InternalServerError; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private string NormalizeExceptionMessage(string msg) | ||||
|     { | ||||
|         // Strip any information we don't want to reveal | ||||
|         return msg.Replace( | ||||
|                 _configuration.ApplicationPaths.ProgramSystemPath, | ||||
|                 string.Empty, | ||||
|                 StringComparison.OrdinalIgnoreCase) | ||||
|             .Replace( | ||||
|                 _configuration.ApplicationPaths.ProgramDataPath, | ||||
|                 string.Empty, | ||||
|                 StringComparison.OrdinalIgnoreCase); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -4,47 +4,46 @@ using MediaBrowser.Common.Extensions; | ||||
| using MediaBrowser.Common.Net; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Middleware | ||||
| namespace Jellyfin.Api.Middleware; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Validates the IP of requests coming from local networks wrt. remote access. | ||||
| /// </summary> | ||||
| public class IpBasedAccessValidationMiddleware | ||||
| { | ||||
|     private readonly RequestDelegate _next; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Validates the IP of requests coming from local networks wrt. remote access. | ||||
|     /// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class. | ||||
|     /// </summary> | ||||
|     public class IpBasedAccessValidationMiddleware | ||||
|     /// <param name="next">The next delegate in the pipeline.</param> | ||||
|     public IpBasedAccessValidationMiddleware(RequestDelegate next) | ||||
|     { | ||||
|         private readonly RequestDelegate _next; | ||||
|         _next = next; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="next">The next delegate in the pipeline.</param> | ||||
|         public IpBasedAccessValidationMiddleware(RequestDelegate next) | ||||
|     /// <summary> | ||||
|     /// Executes the middleware action. | ||||
|     /// </summary> | ||||
|     /// <param name="httpContext">The current HTTP context.</param> | ||||
|     /// <param name="networkManager">The network manager.</param> | ||||
|     /// <returns>The async task.</returns> | ||||
|     public async Task Invoke(HttpContext httpContext, INetworkManager networkManager) | ||||
|     { | ||||
|         if (httpContext.IsLocal()) | ||||
|         { | ||||
|             _next = next; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Executes the middleware action. | ||||
|         /// </summary> | ||||
|         /// <param name="httpContext">The current HTTP context.</param> | ||||
|         /// <param name="networkManager">The network manager.</param> | ||||
|         /// <returns>The async task.</returns> | ||||
|         public async Task Invoke(HttpContext httpContext, INetworkManager networkManager) | ||||
|         { | ||||
|             if (httpContext.IsLocal()) | ||||
|             { | ||||
|                 // Running locally. | ||||
|                 await _next(httpContext).ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; | ||||
| 
 | ||||
|             if (!networkManager.HasRemoteAccess(remoteIp)) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Running locally. | ||||
|             await _next(httpContext).ConfigureAwait(false); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; | ||||
| 
 | ||||
|         if (!networkManager.HasRemoteAccess(remoteIp)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await _next(httpContext).ConfigureAwait(false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -5,41 +5,40 @@ using MediaBrowser.Common.Net; | ||||
| using MediaBrowser.Controller.Configuration; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Middleware | ||||
| namespace Jellyfin.Api.Middleware; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Validates the LAN host IP based on application configuration. | ||||
| /// </summary> | ||||
| public class LanFilteringMiddleware | ||||
| { | ||||
|     private readonly RequestDelegate _next; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Validates the LAN host IP based on application configuration. | ||||
|     /// Initializes a new instance of the <see cref="LanFilteringMiddleware"/> class. | ||||
|     /// </summary> | ||||
|     public class LanFilteringMiddleware | ||||
|     /// <param name="next">The next delegate in the pipeline.</param> | ||||
|     public LanFilteringMiddleware(RequestDelegate next) | ||||
|     { | ||||
|         private readonly RequestDelegate _next; | ||||
|         _next = next; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="LanFilteringMiddleware"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="next">The next delegate in the pipeline.</param> | ||||
|         public LanFilteringMiddleware(RequestDelegate next) | ||||
|     /// <summary> | ||||
|     /// Executes the middleware action. | ||||
|     /// </summary> | ||||
|     /// <param name="httpContext">The current HTTP context.</param> | ||||
|     /// <param name="networkManager">The network manager.</param> | ||||
|     /// <param name="serverConfigurationManager">The server configuration manager.</param> | ||||
|     /// <returns>The async task.</returns> | ||||
|     public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) | ||||
|     { | ||||
|         var host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; | ||||
| 
 | ||||
|         if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess) | ||||
|         { | ||||
|             _next = next; | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Executes the middleware action. | ||||
|         /// </summary> | ||||
|         /// <param name="httpContext">The current HTTP context.</param> | ||||
|         /// <param name="networkManager">The network manager.</param> | ||||
|         /// <param name="serverConfigurationManager">The server configuration manager.</param> | ||||
|         /// <returns>The async task.</returns> | ||||
|         public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) | ||||
|         { | ||||
|             var host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; | ||||
| 
 | ||||
|             if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             await _next(httpContext).ConfigureAwait(false); | ||||
|         } | ||||
|         await _next(httpContext).ConfigureAwait(false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -3,52 +3,51 @@ using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Middleware | ||||
| namespace Jellyfin.Api.Middleware; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Removes /emby and /mediabrowser from requested route. | ||||
| /// </summary> | ||||
| public class LegacyEmbyRouteRewriteMiddleware | ||||
| { | ||||
|     private const string EmbyPath = "/emby"; | ||||
|     private const string MediabrowserPath = "/mediabrowser"; | ||||
| 
 | ||||
|     private readonly RequestDelegate _next; | ||||
|     private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Removes /emby and /mediabrowser from requested route. | ||||
|     /// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class. | ||||
|     /// </summary> | ||||
|     public class LegacyEmbyRouteRewriteMiddleware | ||||
|     /// <param name="next">The next delegate in the pipeline.</param> | ||||
|     /// <param name="logger">The logger.</param> | ||||
|     public LegacyEmbyRouteRewriteMiddleware( | ||||
|         RequestDelegate next, | ||||
|         ILogger<LegacyEmbyRouteRewriteMiddleware> logger) | ||||
|     { | ||||
|         private const string EmbyPath = "/emby"; | ||||
|         private const string MediabrowserPath = "/mediabrowser"; | ||||
|         _next = next; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         private readonly RequestDelegate _next; | ||||
|         private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="next">The next delegate in the pipeline.</param> | ||||
|         /// <param name="logger">The logger.</param> | ||||
|         public LegacyEmbyRouteRewriteMiddleware( | ||||
|             RequestDelegate next, | ||||
|             ILogger<LegacyEmbyRouteRewriteMiddleware> logger) | ||||
|     /// <summary> | ||||
|     /// Executes the middleware action. | ||||
|     /// </summary> | ||||
|     /// <param name="httpContext">The current HTTP context.</param> | ||||
|     /// <returns>The async task.</returns> | ||||
|     public async Task Invoke(HttpContext httpContext) | ||||
|     { | ||||
|         var localPath = httpContext.Request.Path.ToString(); | ||||
|         if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             _next = next; | ||||
|             _logger = logger; | ||||
|             httpContext.Request.Path = localPath[EmbyPath.Length..]; | ||||
|             _logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath); | ||||
|         } | ||||
|         else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             httpContext.Request.Path = localPath[MediabrowserPath.Length..]; | ||||
|             _logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Executes the middleware action. | ||||
|         /// </summary> | ||||
|         /// <param name="httpContext">The current HTTP context.</param> | ||||
|         /// <returns>The async task.</returns> | ||||
|         public async Task Invoke(HttpContext httpContext) | ||||
|         { | ||||
|             var localPath = httpContext.Request.Path.ToString(); | ||||
|             if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 httpContext.Request.Path = localPath[EmbyPath.Length..]; | ||||
|                 _logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath); | ||||
|             } | ||||
|             else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 httpContext.Request.Path = localPath[MediabrowserPath.Length..]; | ||||
|                 _logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath); | ||||
|             } | ||||
| 
 | ||||
|             await _next(httpContext).ConfigureAwait(false); | ||||
|         } | ||||
|         await _next(httpContext).ConfigureAwait(false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -2,38 +2,37 @@ using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Http.Features; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Middleware | ||||
| namespace Jellyfin.Api.Middleware; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// URL decodes the querystring before binding. | ||||
| /// </summary> | ||||
| public class QueryStringDecodingMiddleware | ||||
| { | ||||
|     private readonly RequestDelegate _next; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// URL decodes the querystring before binding. | ||||
|     /// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class. | ||||
|     /// </summary> | ||||
|     public class QueryStringDecodingMiddleware | ||||
|     /// <param name="next">The next delegate in the pipeline.</param> | ||||
|     public QueryStringDecodingMiddleware(RequestDelegate next) | ||||
|     { | ||||
|         private readonly RequestDelegate _next; | ||||
|         _next = next; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="next">The next delegate in the pipeline.</param> | ||||
|         public QueryStringDecodingMiddleware(RequestDelegate next) | ||||
|     /// <summary> | ||||
|     /// Executes the middleware action. | ||||
|     /// </summary> | ||||
|     /// <param name="httpContext">The current HTTP context.</param> | ||||
|     /// <returns>The async task.</returns> | ||||
|     public async Task Invoke(HttpContext httpContext) | ||||
|     { | ||||
|         var feature = httpContext.Features.Get<IQueryFeature>(); | ||||
|         if (feature is not null) | ||||
|         { | ||||
|             _next = next; | ||||
|             httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(feature)); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Executes the middleware action. | ||||
|         /// </summary> | ||||
|         /// <param name="httpContext">The current HTTP context.</param> | ||||
|         /// <returns>The async task.</returns> | ||||
|         public async Task Invoke(HttpContext httpContext) | ||||
|         { | ||||
|             var feature = httpContext.Features.Get<IQueryFeature>(); | ||||
|             if (feature is not null) | ||||
|             { | ||||
|                 httpContext.Features.Set<IQueryFeature>(new UrlDecodeQueryFeature(feature)); | ||||
|             } | ||||
| 
 | ||||
|             await _next(httpContext).ConfigureAwait(false); | ||||
|         } | ||||
|         await _next(httpContext).ConfigureAwait(false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -7,63 +7,62 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Http.Extensions; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Middleware | ||||
| namespace Jellyfin.Api.Middleware; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Response time middleware. | ||||
| /// </summary> | ||||
| public class ResponseTimeMiddleware | ||||
| { | ||||
|     private const string ResponseHeaderResponseTime = "X-Response-Time-ms"; | ||||
| 
 | ||||
|     private readonly RequestDelegate _next; | ||||
|     private readonly ILogger<ResponseTimeMiddleware> _logger; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Response time middleware. | ||||
|     /// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class. | ||||
|     /// </summary> | ||||
|     public class ResponseTimeMiddleware | ||||
|     /// <param name="next">Next request delegate.</param> | ||||
|     /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param> | ||||
|     public ResponseTimeMiddleware( | ||||
|         RequestDelegate next, | ||||
|         ILogger<ResponseTimeMiddleware> logger) | ||||
|     { | ||||
|         private const string ResponseHeaderResponseTime = "X-Response-Time-ms"; | ||||
|         _next = next; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         private readonly RequestDelegate _next; | ||||
|         private readonly ILogger<ResponseTimeMiddleware> _logger; | ||||
|     /// <summary> | ||||
|     /// Invoke request. | ||||
|     /// </summary> | ||||
|     /// <param name="context">Request context.</param> | ||||
|     /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|     /// <returns>Task.</returns> | ||||
|     public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager) | ||||
|     { | ||||
|         var startTimestamp = Stopwatch.GetTimestamp(); | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="next">Next request delegate.</param> | ||||
|         /// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param> | ||||
|         public ResponseTimeMiddleware( | ||||
|             RequestDelegate next, | ||||
|             ILogger<ResponseTimeMiddleware> logger) | ||||
|         var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning; | ||||
|         var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs; | ||||
|         context.Response.OnStarting(() => | ||||
|         { | ||||
|             _next = next; | ||||
|             _logger = logger; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Invoke request. | ||||
|         /// </summary> | ||||
|         /// <param name="context">Request context.</param> | ||||
|         /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> | ||||
|         /// <returns>Task.</returns> | ||||
|         public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager) | ||||
|         { | ||||
|             var startTimestamp = Stopwatch.GetTimestamp(); | ||||
| 
 | ||||
|             var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning; | ||||
|             var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs; | ||||
|             context.Response.OnStarting(() => | ||||
|             var responseTime = Stopwatch.GetElapsedTime(startTimestamp); | ||||
|             var responseTimeMs = responseTime.TotalMilliseconds; | ||||
|             if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug)) | ||||
|             { | ||||
|                 var responseTime = Stopwatch.GetElapsedTime(startTimestamp); | ||||
|                 var responseTimeMs = responseTime.TotalMilliseconds; | ||||
|                 if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug)) | ||||
|                 { | ||||
|                     _logger.LogDebug( | ||||
|                         "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}", | ||||
|                         context.Request.GetDisplayUrl(), | ||||
|                         context.GetNormalizedRemoteIp(), | ||||
|                         responseTime, | ||||
|                         context.Response.StatusCode); | ||||
|                 } | ||||
|                 _logger.LogDebug( | ||||
|                     "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}", | ||||
|                     context.Request.GetDisplayUrl(), | ||||
|                     context.GetNormalizedRemoteIp(), | ||||
|                     responseTime, | ||||
|                     context.Response.StatusCode); | ||||
|             } | ||||
| 
 | ||||
|                 context.Response.Headers[ResponseHeaderResponseTime] = responseTimeMs.ToString(CultureInfo.InvariantCulture); | ||||
|                 return Task.CompletedTask; | ||||
|             }); | ||||
|             context.Response.Headers[ResponseHeaderResponseTime] = responseTimeMs.ToString(CultureInfo.InvariantCulture); | ||||
|             return Task.CompletedTask; | ||||
|         }); | ||||
| 
 | ||||
|             // Call the next delegate/middleware in the pipeline | ||||
|             await this._next(context).ConfigureAwait(false); | ||||
|         } | ||||
|         // Call the next delegate/middleware in the pipeline | ||||
|         await this._next(context).ConfigureAwait(false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -3,45 +3,44 @@ using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Middleware | ||||
| namespace Jellyfin.Api.Middleware; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Redirect requests to robots.txt to web/robots.txt. | ||||
| /// </summary> | ||||
| public class RobotsRedirectionMiddleware | ||||
| { | ||||
|     private readonly RequestDelegate _next; | ||||
|     private readonly ILogger<RobotsRedirectionMiddleware> _logger; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Redirect requests to robots.txt to web/robots.txt. | ||||
|     /// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class. | ||||
|     /// </summary> | ||||
|     public class RobotsRedirectionMiddleware | ||||
|     /// <param name="next">The next delegate in the pipeline.</param> | ||||
|     /// <param name="logger">The logger.</param> | ||||
|     public RobotsRedirectionMiddleware( | ||||
|         RequestDelegate next, | ||||
|         ILogger<RobotsRedirectionMiddleware> logger) | ||||
|     { | ||||
|         private readonly RequestDelegate _next; | ||||
|         private readonly ILogger<RobotsRedirectionMiddleware> _logger; | ||||
|         _next = next; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="next">The next delegate in the pipeline.</param> | ||||
|         /// <param name="logger">The logger.</param> | ||||
|         public RobotsRedirectionMiddleware( | ||||
|             RequestDelegate next, | ||||
|             ILogger<RobotsRedirectionMiddleware> logger) | ||||
|     /// <summary> | ||||
|     /// Executes the middleware action. | ||||
|     /// </summary> | ||||
|     /// <param name="httpContext">The current HTTP context.</param> | ||||
|     /// <returns>The async task.</returns> | ||||
|     public async Task Invoke(HttpContext httpContext) | ||||
|     { | ||||
|         var localPath = httpContext.Request.Path.ToString(); | ||||
|         if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             _next = next; | ||||
|             _logger = logger; | ||||
|             _logger.LogDebug("Redirecting robots.txt request to web/robots.txt"); | ||||
|             httpContext.Response.Redirect("web/robots.txt"); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Executes the middleware action. | ||||
|         /// </summary> | ||||
|         /// <param name="httpContext">The current HTTP context.</param> | ||||
|         /// <returns>The async task.</returns> | ||||
|         public async Task Invoke(HttpContext httpContext) | ||||
|         { | ||||
|             var localPath = httpContext.Request.Path.ToString(); | ||||
|             if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 _logger.LogDebug("Redirecting robots.txt request to web/robots.txt"); | ||||
|                 httpContext.Response.Redirect("web/robots.txt"); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             await _next(httpContext).ConfigureAwait(false); | ||||
|         } | ||||
|         await _next(httpContext).ConfigureAwait(false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -5,47 +5,46 @@ using MediaBrowser.Controller; | ||||
| using MediaBrowser.Model.Globalization; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Middleware | ||||
| namespace Jellyfin.Api.Middleware; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Shows a custom message during server startup. | ||||
| /// </summary> | ||||
| public class ServerStartupMessageMiddleware | ||||
| { | ||||
|     private readonly RequestDelegate _next; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Shows a custom message during server startup. | ||||
|     /// Initializes a new instance of the <see cref="ServerStartupMessageMiddleware"/> class. | ||||
|     /// </summary> | ||||
|     public class ServerStartupMessageMiddleware | ||||
|     /// <param name="next">The next delegate in the pipeline.</param> | ||||
|     public ServerStartupMessageMiddleware(RequestDelegate next) | ||||
|     { | ||||
|         private readonly RequestDelegate _next; | ||||
|         _next = next; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="ServerStartupMessageMiddleware"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="next">The next delegate in the pipeline.</param> | ||||
|         public ServerStartupMessageMiddleware(RequestDelegate next) | ||||
|     /// <summary> | ||||
|     /// Executes the middleware action. | ||||
|     /// </summary> | ||||
|     /// <param name="httpContext">The current HTTP context.</param> | ||||
|     /// <param name="serverApplicationHost">The server application host.</param> | ||||
|     /// <param name="localizationManager">The localization manager.</param> | ||||
|     /// <returns>The async task.</returns> | ||||
|     public async Task Invoke( | ||||
|         HttpContext httpContext, | ||||
|         IServerApplicationHost serverApplicationHost, | ||||
|         ILocalizationManager localizationManager) | ||||
|     { | ||||
|         if (serverApplicationHost.CoreStartupHasCompleted | ||||
|             || httpContext.Request.Path.Equals("/system/ping", StringComparison.OrdinalIgnoreCase)) | ||||
|         { | ||||
|             _next = next; | ||||
|             await _next(httpContext).ConfigureAwait(false); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Executes the middleware action. | ||||
|         /// </summary> | ||||
|         /// <param name="httpContext">The current HTTP context.</param> | ||||
|         /// <param name="serverApplicationHost">The server application host.</param> | ||||
|         /// <param name="localizationManager">The localization manager.</param> | ||||
|         /// <returns>The async task.</returns> | ||||
|         public async Task Invoke( | ||||
|             HttpContext httpContext, | ||||
|             IServerApplicationHost serverApplicationHost, | ||||
|             ILocalizationManager localizationManager) | ||||
|         { | ||||
|             if (serverApplicationHost.CoreStartupHasCompleted | ||||
|                 || httpContext.Request.Path.Equals("/system/ping", StringComparison.OrdinalIgnoreCase)) | ||||
|             { | ||||
|                 await _next(httpContext).ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); | ||||
|             httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; | ||||
|             httpContext.Response.ContentType = MediaTypeNames.Text.Html; | ||||
|             await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false); | ||||
|         } | ||||
|         var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); | ||||
|         httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; | ||||
|         httpContext.Response.ContentType = MediaTypeNames.Text.Html; | ||||
|         await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -6,79 +6,78 @@ using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Http.Features; | ||||
| using Microsoft.Extensions.Primitives; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Middleware | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Defines the <see cref="UrlDecodeQueryFeature"/>. | ||||
|     /// </summary> | ||||
|     public class UrlDecodeQueryFeature : IQueryFeature | ||||
|     { | ||||
|         private IQueryCollection? _store; | ||||
| namespace Jellyfin.Api.Middleware; | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="feature">The <see cref="IQueryFeature"/> instance.</param> | ||||
|         public UrlDecodeQueryFeature(IQueryFeature feature) | ||||
| /// <summary> | ||||
| /// Defines the <see cref="UrlDecodeQueryFeature"/>. | ||||
| /// </summary> | ||||
| public class UrlDecodeQueryFeature : IQueryFeature | ||||
| { | ||||
|     private IQueryCollection? _store; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class. | ||||
|     /// </summary> | ||||
|     /// <param name="feature">The <see cref="IQueryFeature"/> instance.</param> | ||||
|     public UrlDecodeQueryFeature(IQueryFeature feature) | ||||
|     { | ||||
|         Query = feature.Query; | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>. | ||||
|     /// </summary> | ||||
|     public IQueryCollection Query | ||||
|     { | ||||
|         get | ||||
|         { | ||||
|             Query = feature.Query; | ||||
|             return _store ?? QueryCollection.Empty; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>. | ||||
|         /// </summary> | ||||
|         public IQueryCollection Query | ||||
|         set | ||||
|         { | ||||
|             get | ||||
|             // Only interested in where the querystring is encoded which shows up as one key with nothing in the value. | ||||
|             if (value.Count != 1) | ||||
|             { | ||||
|                 return _store ?? QueryCollection.Empty; | ||||
|                 _store = value; | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             set | ||||
|             // Encoded querystrings have no value, so don't process anything if a value is present. | ||||
|             var (key, stringValues) = value.First(); | ||||
|             if (!string.IsNullOrEmpty(stringValues)) | ||||
|             { | ||||
|                 // Only interested in where the querystring is encoded which shows up as one key with nothing in the value. | ||||
|                 if (value.Count != 1) | ||||
|                 { | ||||
|                     _store = value; | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 // Encoded querystrings have no value, so don't process anything if a value is present. | ||||
|                 var (key, stringValues) = value.First(); | ||||
|                 if (!string.IsNullOrEmpty(stringValues)) | ||||
|                 { | ||||
|                     _store = value; | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 if (!key.Contains('=', StringComparison.Ordinal)) | ||||
|                 { | ||||
|                     _store = value; | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 var pairs = new Dictionary<string, StringValues>(); | ||||
|                 foreach (var pair in key.SpanSplit('&')) | ||||
|                 { | ||||
|                     var i = pair.IndexOf('='); | ||||
|                     if (i == -1) | ||||
|                     { | ||||
|                         // encoded is an equals. | ||||
|                         // We use TryAdd so duplicate keys get ignored | ||||
|                         pairs.TryAdd(pair.ToString(), StringValues.Empty); | ||||
|                         continue; | ||||
|                     } | ||||
| 
 | ||||
|                     var k = pair[..i].ToString(); | ||||
|                     var v = pair[(i + 1)..].ToString(); | ||||
|                     if (!pairs.TryAdd(k, new StringValues(v))) | ||||
|                     { | ||||
|                         pairs[k] = StringValues.Concat(pairs[k], v); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 _store = new QueryCollection(pairs); | ||||
|                 _store = value; | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             if (!key.Contains('=', StringComparison.Ordinal)) | ||||
|             { | ||||
|                 _store = value; | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             var pairs = new Dictionary<string, StringValues>(); | ||||
|             foreach (var pair in key.SpanSplit('&')) | ||||
|             { | ||||
|                 var i = pair.IndexOf('='); | ||||
|                 if (i == -1) | ||||
|                 { | ||||
|                     // encoded is an equals. | ||||
|                     // We use TryAdd so duplicate keys get ignored | ||||
|                     pairs.TryAdd(pair.ToString(), StringValues.Empty); | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 var k = pair[..i].ToString(); | ||||
|                 var v = pair[(i + 1)..].ToString(); | ||||
|                 if (!pairs.TryAdd(k, new StringValues(v))) | ||||
|                 { | ||||
|                     pairs[k] = StringValues.Concat(pairs[k], v); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             _store = new QueryCollection(pairs); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -2,39 +2,38 @@ using System.Threading.Tasks; | ||||
| using MediaBrowser.Controller.Net; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| 
 | ||||
| namespace Jellyfin.Api.Middleware | ||||
| namespace Jellyfin.Api.Middleware; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Handles WebSocket requests. | ||||
| /// </summary> | ||||
| public class WebSocketHandlerMiddleware | ||||
| { | ||||
|     private readonly RequestDelegate _next; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Handles WebSocket requests. | ||||
|     /// Initializes a new instance of the <see cref="WebSocketHandlerMiddleware"/> class. | ||||
|     /// </summary> | ||||
|     public class WebSocketHandlerMiddleware | ||||
|     /// <param name="next">The next delegate in the pipeline.</param> | ||||
|     public WebSocketHandlerMiddleware(RequestDelegate next) | ||||
|     { | ||||
|         private readonly RequestDelegate _next; | ||||
|         _next = next; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="WebSocketHandlerMiddleware"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="next">The next delegate in the pipeline.</param> | ||||
|         public WebSocketHandlerMiddleware(RequestDelegate next) | ||||
|     /// <summary> | ||||
|     /// Executes the middleware action. | ||||
|     /// </summary> | ||||
|     /// <param name="httpContext">The current HTTP context.</param> | ||||
|     /// <param name="webSocketManager">The WebSocket connection manager.</param> | ||||
|     /// <returns>The async task.</returns> | ||||
|     public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager) | ||||
|     { | ||||
|         if (!httpContext.WebSockets.IsWebSocketRequest) | ||||
|         { | ||||
|             _next = next; | ||||
|             await _next(httpContext).ConfigureAwait(false); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Executes the middleware action. | ||||
|         /// </summary> | ||||
|         /// <param name="httpContext">The current HTTP context.</param> | ||||
|         /// <param name="webSocketManager">The WebSocket connection manager.</param> | ||||
|         /// <returns>The async task.</returns> | ||||
|         public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager) | ||||
|         { | ||||
|             if (!httpContext.WebSockets.IsWebSocketRequest) | ||||
|             { | ||||
|                 await _next(httpContext).ConfigureAwait(false); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false); | ||||
|         } | ||||
|         await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -5,86 +5,85 @@ using System.Threading.Tasks; | ||||
| using Microsoft.AspNetCore.Mvc.ModelBinding; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace Jellyfin.Api.ModelBinders | ||||
| namespace Jellyfin.Api.ModelBinders; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Comma delimited array model binder. | ||||
| /// Returns an empty array of specified type if there is no query parameter. | ||||
| /// </summary> | ||||
| public class CommaDelimitedArrayModelBinder : IModelBinder | ||||
| { | ||||
|     private readonly ILogger<CommaDelimitedArrayModelBinder> _logger; | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Comma delimited array model binder. | ||||
|     /// Returns an empty array of specified type if there is no query parameter. | ||||
|     /// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinder"/> class. | ||||
|     /// </summary> | ||||
|     public class CommaDelimitedArrayModelBinder : IModelBinder | ||||
|     /// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedArrayModelBinder}"/> interface.</param> | ||||
|     public CommaDelimitedArrayModelBinder(ILogger<CommaDelimitedArrayModelBinder> logger) | ||||
|     { | ||||
|         private readonly ILogger<CommaDelimitedArrayModelBinder> _logger; | ||||
|         _logger = logger; | ||||
|     } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinder"/> class. | ||||
|         /// </summary> | ||||
|         /// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedArrayModelBinder}"/> interface.</param> | ||||
|         public CommaDelimitedArrayModelBinder(ILogger<CommaDelimitedArrayModelBinder> logger) | ||||
|     /// <inheritdoc/> | ||||
|     public Task BindModelAsync(ModelBindingContext bindingContext) | ||||
|     { | ||||
|         var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); | ||||
|         var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0]; | ||||
|         var converter = TypeDescriptor.GetConverter(elementType); | ||||
| 
 | ||||
|         if (valueProviderResult.Length > 1) | ||||
|         { | ||||
|             _logger = logger; | ||||
|             var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter); | ||||
|             bindingContext.Result = ModelBindingResult.Success(typedValues); | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc/> | ||||
|         public Task BindModelAsync(ModelBindingContext bindingContext) | ||||
|         else | ||||
|         { | ||||
|             var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); | ||||
|             var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0]; | ||||
|             var converter = TypeDescriptor.GetConverter(elementType); | ||||
|             var value = valueProviderResult.FirstValue; | ||||
| 
 | ||||
|             if (valueProviderResult.Length > 1) | ||||
|             if (value is not null) | ||||
|             { | ||||
|                 var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter); | ||||
|                 var splitValues = value.Split(',', StringSplitOptions.RemoveEmptyEntries); | ||||
|                 var typedValues = GetParsedResult(splitValues, elementType, converter); | ||||
|                 bindingContext.Result = ModelBindingResult.Success(typedValues); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var value = valueProviderResult.FirstValue; | ||||
| 
 | ||||
|                 if (value is not null) | ||||
|                 { | ||||
|                     var splitValues = value.Split(',', StringSplitOptions.RemoveEmptyEntries); | ||||
|                     var typedValues = GetParsedResult(splitValues, elementType, converter); | ||||
|                     bindingContext.Result = ModelBindingResult.Success(typedValues); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     var emptyResult = Array.CreateInstance(elementType, 0); | ||||
|                     bindingContext.Result = ModelBindingResult.Success(emptyResult); | ||||
|                 } | ||||
|                 var emptyResult = Array.CreateInstance(elementType, 0); | ||||
|                 bindingContext.Result = ModelBindingResult.Success(emptyResult); | ||||
|             } | ||||
| 
 | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
| 
 | ||||
|         private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter) | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
| 
 | ||||
|     private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter) | ||||
|     { | ||||
|         var parsedValues = new object?[values.Count]; | ||||
|         var convertedCount = 0; | ||||
|         for (var i = 0; i < values.Count; i++) | ||||
|         { | ||||
|             var parsedValues = new object?[values.Count]; | ||||
|             var convertedCount = 0; | ||||
|             for (var i = 0; i < values.Count; i++) | ||||
|             try | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     parsedValues[i] = converter.ConvertFromString(values[i].Trim()); | ||||
|                     convertedCount++; | ||||
|                 } | ||||
|                 catch (FormatException e) | ||||
|                 { | ||||
|                     _logger.LogDebug(e, "Error converting value."); | ||||
|                 } | ||||
|                 parsedValues[i] = converter.ConvertFromString(values[i].Trim()); | ||||
|                 convertedCount++; | ||||
|             } | ||||
| 
 | ||||
|             var typedValues = Array.CreateInstance(elementType, convertedCount); | ||||
|             var typedValueIndex = 0; | ||||
|             for (var i = 0; i < parsedValues.Length; i++) | ||||
|             catch (FormatException e) | ||||
|             { | ||||
|                 if (parsedValues[i] != null) | ||||
|                 { | ||||
|                     typedValues.SetValue(parsedValues[i], typedValueIndex); | ||||
|                     typedValueIndex++; | ||||
|                 } | ||||
|                 _logger.LogDebug(e, "Error converting value."); | ||||
|             } | ||||
| 
 | ||||
|             return typedValues; | ||||
|         } | ||||
| 
 | ||||
|         var typedValues = Array.CreateInstance(elementType, convertedCount); | ||||
|         var typedValueIndex = 0; | ||||
|         for (var i = 0; i < parsedValues.Length; i++) | ||||
|         { | ||||
|             if (parsedValues[i] != null) | ||||
|             { | ||||
|                 typedValues.SetValue(parsedValues[i], typedValueIndex); | ||||
|                 typedValueIndex++; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return typedValues; | ||||
|     } | ||||
| } | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user