mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-10-27 16:52:45 -04:00
Migrate to file-scoped namespaces
This commit is contained in:
parent
58b3945805
commit
f5f890e685
@ -2,29 +2,28 @@
|
|||||||
|
|
||||||
using System;
|
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>
|
/// <summary>
|
||||||
/// Internal produces image attribute.
|
/// Initializes a new instance of the <see cref="AcceptsFileAttribute"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
/// <param name="contentTypes">Content types this endpoint produces.</param>
|
||||||
public class AcceptsFileAttribute : Attribute
|
public AcceptsFileAttribute(params string[] contentTypes)
|
||||||
{
|
{
|
||||||
private readonly string[] _contentTypes;
|
_contentTypes = 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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
|
namespace Jellyfin.Api.Attributes;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Produces file attribute of "image/*".
|
|
||||||
/// </summary>
|
|
||||||
public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute
|
|
||||||
{
|
|
||||||
private const string ContentType = "image/*";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class.
|
/// Produces file attribute of "image/*".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AcceptsImageFileAttribute()
|
public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute
|
||||||
: base(ContentType)
|
{
|
||||||
{
|
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 System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
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>
|
/// <summary>
|
||||||
/// Identifies an action that supports the HTTP GET method.
|
/// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
|
||||||
/// </summary>
|
/// </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 System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
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>
|
/// <summary>
|
||||||
/// Identifies an action that supports the HTTP GET method.
|
/// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
|
||||||
/// </summary>
|
/// </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;
|
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
|
namespace Jellyfin.Api.Attributes;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Produces file attribute of "image/*".
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ProducesAudioFileAttribute : ProducesFileAttribute
|
|
||||||
{
|
|
||||||
private const string ContentType = "audio/*";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ProducesAudioFileAttribute"/> class.
|
/// Produces file attribute of "image/*".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProducesAudioFileAttribute()
|
public sealed class ProducesAudioFileAttribute : ProducesFileAttribute
|
||||||
: base(ContentType)
|
{
|
||||||
{
|
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;
|
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>
|
/// <summary>
|
||||||
/// Internal produces image attribute.
|
/// Initializes a new instance of the <see cref="ProducesFileAttribute"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
/// <param name="contentTypes">Content types this endpoint produces.</param>
|
||||||
public class ProducesFileAttribute : Attribute
|
public ProducesFileAttribute(params string[] contentTypes)
|
||||||
{
|
{
|
||||||
private readonly string[] _contentTypes;
|
_contentTypes = 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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
|
namespace Jellyfin.Api.Attributes;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Produces file attribute of "image/*".
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ProducesImageFileAttribute : ProducesFileAttribute
|
|
||||||
{
|
|
||||||
private const string ContentType = "image/*";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ProducesImageFileAttribute"/> class.
|
/// Produces file attribute of "image/*".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProducesImageFileAttribute()
|
public sealed class ProducesImageFileAttribute : ProducesFileAttribute
|
||||||
: base(ContentType)
|
{
|
||||||
{
|
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
|
namespace Jellyfin.Api.Attributes;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Produces file attribute of "image/*".
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute
|
|
||||||
{
|
|
||||||
private const string ContentType = "application/x-mpegURL";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ProducesPlaylistFileAttribute"/> class.
|
/// Produces file attribute of "image/*".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProducesPlaylistFileAttribute()
|
public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute
|
||||||
: base(ContentType)
|
{
|
||||||
{
|
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
|
namespace Jellyfin.Api.Attributes;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Produces file attribute of "video/*".
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ProducesVideoFileAttribute : ProducesFileAttribute
|
|
||||||
{
|
|
||||||
private const string ContentType = "video/*";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ProducesVideoFileAttribute"/> class.
|
/// Produces file attribute of "video/*".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProducesVideoFileAttribute()
|
public sealed class ProducesVideoFileAttribute : ProducesFileAttribute
|
||||||
: base(ContentType)
|
{
|
||||||
{
|
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 Jellyfin.Extensions.Json;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Base api controller for the API setting a default route.
|
/// Create a new <see cref="OkResult{T}"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
/// <param name="value">The value to return.</param>
|
||||||
[Route("[controller]")]
|
/// <typeparam name="T">The type to return.</typeparam>
|
||||||
[Produces(
|
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
|
||||||
MediaTypeNames.Application.Json,
|
protected ActionResult<IEnumerable<T>> Ok<T>(IEnumerable<T>? value)
|
||||||
JsonDefaults.CamelCaseMediaType,
|
=> new OkResult<IEnumerable<T>?>(value);
|
||||||
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);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new <see cref="OkResult{T}"/>.
|
/// Create a new <see cref="OkResult{T}"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">The value to return.</param>
|
/// <param name="value">The value to return.</param>
|
||||||
/// <typeparam name="T">The type to return.</typeparam>
|
/// <typeparam name="T">The type to return.</typeparam>
|
||||||
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
|
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
|
||||||
protected ActionResult<T> Ok<T>(T value)
|
protected ActionResult<T> Ok<T>(T value)
|
||||||
=> new OkResult<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>
|
/// <summary>
|
||||||
/// Authentication schemes for user authentication in the API.
|
/// Scheme name for the custom legacy authentication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AuthenticationSchemes
|
public const string CustomAuthentication = "CustomAuthentication";
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Scheme name for the custom legacy authentication.
|
|
||||||
/// </summary>
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Internal claim types for authorization.
|
/// User Id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class InternalClaimTypes
|
public const string UserId = "Jellyfin-UserId";
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// User Id.
|
|
||||||
/// </summary>
|
|
||||||
public const string UserId = "Jellyfin-UserId";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Device Id.
|
/// Device Id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string DeviceId = "Jellyfin-DeviceId";
|
public const string DeviceId = "Jellyfin-DeviceId";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Device.
|
/// Device.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Device = "Jellyfin-Device";
|
public const string Device = "Jellyfin-Device";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client.
|
/// Client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Client = "Jellyfin-Client";
|
public const string Client = "Jellyfin-Client";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Version.
|
/// Version.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Version = "Jellyfin-Version";
|
public const string Version = "Jellyfin-Version";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Token.
|
/// Token.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Token = "Jellyfin-Token";
|
public const string Token = "Jellyfin-Token";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is Api Key.
|
/// Is Api Key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string IsApiKey = "Jellyfin-IsApiKey";
|
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>
|
/// <summary>
|
||||||
/// Policies for the API authorization.
|
/// Policy name for default authorization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Policies
|
public const string DefaultAuthorization = "DefaultAuthorization";
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Policy name for default authorization.
|
|
||||||
/// </summary>
|
|
||||||
public const string DefaultAuthorization = "DefaultAuthorization";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring first time setup or elevated privileges.
|
/// Policy name for requiring first time setup or elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
|
public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring elevated privileges.
|
/// Policy name for requiring elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string RequiresElevation = "RequiresElevation";
|
public const string RequiresElevation = "RequiresElevation";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for allowing local access only.
|
/// Policy name for allowing local access only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LocalAccessOnly = "LocalAccessOnly";
|
public const string LocalAccessOnly = "LocalAccessOnly";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for escaping schedule controls.
|
/// Policy name for escaping schedule controls.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string IgnoreParentalControl = "IgnoreParentalControl";
|
public const string IgnoreParentalControl = "IgnoreParentalControl";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring download permission.
|
/// Policy name for requiring download permission.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Download = "Download";
|
public const string Download = "Download";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring first time setup or default permissions.
|
/// Policy name for requiring first time setup or default permissions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
|
public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring local access or elevated privileges.
|
/// Policy name for requiring local access or elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
|
public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring (anonymous) LAN access.
|
/// Policy name for requiring (anonymous) LAN access.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy";
|
public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for escaping schedule controls or requiring first time setup.
|
/// Policy name for escaping schedule controls or requiring first time setup.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
|
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for accessing SyncPlay.
|
/// Policy name for accessing SyncPlay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayHasAccess = "SyncPlayHasAccess";
|
public const string SyncPlayHasAccess = "SyncPlayHasAccess";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for creating a SyncPlay group.
|
/// Policy name for creating a SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
|
public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for joining a SyncPlay group.
|
/// Policy name for joining a SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
|
public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for accessing a SyncPlay group.
|
/// Policy name for accessing a SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
|
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>
|
/// <summary>
|
||||||
/// Constants for user roles used in the authentication and authorization for the API.
|
/// Guest user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class UserRoles
|
public const string Guest = "Guest";
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Guest user.
|
|
||||||
/// </summary>
|
|
||||||
public const string Guest = "Guest";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Regular user with no special privileges.
|
/// Regular user with no special privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string User = "User";
|
public const string User = "User";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Administrator user with elevated privileges.
|
/// Administrator user with elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Administrator = "Administrator";
|
public const string Administrator = "Administrator";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,50 +8,49 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Activity log controller.
|
/// Initializes a new instance of the <see cref="ActivityLogController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("System/ActivityLog")]
|
/// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
public ActivityLogController(IActivityManager activityManager)
|
||||||
public class ActivityLogController : BaseJellyfinApiController
|
|
||||||
{
|
{
|
||||||
private readonly IActivityManager _activityManager;
|
_activityManager = activityManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ActivityLogController"/> class.
|
/// Gets activity log entries.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
|
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||||
public ActivityLogController(IActivityManager activityManager)
|
/// <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;
|
Skip = startIndex,
|
||||||
}
|
Limit = limit,
|
||||||
|
MinDate = minDate,
|
||||||
/// <summary>
|
HasUserId = hasUserId
|
||||||
/// Gets activity log entries.
|
}).ConfigureAwait(false);
|
||||||
/// </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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,70 +7,69 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Authentication controller.
|
/// Initializes a new instance of the <see cref="ApiKeyController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Auth")]
|
/// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param>
|
||||||
public class ApiKeyController : BaseJellyfinApiController
|
public ApiKeyController(IAuthenticationManager authenticationManager)
|
||||||
{
|
{
|
||||||
private readonly IAuthenticationManager _authenticationManager;
|
_authenticationManager = authenticationManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ApiKeyController"/> class.
|
/// Get all keys.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param>
|
/// <response code="200">Api keys retrieved.</response>
|
||||||
public ApiKeyController(IAuthenticationManager authenticationManager)
|
/// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns>
|
||||||
{
|
[HttpGet("Keys")]
|
||||||
_authenticationManager = authenticationManager;
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
}
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys()
|
||||||
|
{
|
||||||
|
var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false);
|
||||||
|
|
||||||
/// <summary>
|
return new QueryResult<AuthenticationInfo>(keys);
|
||||||
/// 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);
|
/// <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>
|
return NoContent();
|
||||||
/// 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();
|
/// <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>
|
return NoContent();
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,464 +17,463 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The artists controller.
|
/// Initializes a new instance of the <see cref="ArtistsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Artists")]
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
public class ArtistsController : BaseJellyfinApiController
|
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||||
|
public ArtistsController(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
IDtoService dtoService)
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ArtistsController"/> class.
|
/// Gets all artists from a given item, folder, or the entire library.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</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="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
public ArtistsController(
|
/// <param name="searchTerm">Optional. Search term.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||||
IUserManager userManager,
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
IDtoService dtoService)
|
/// <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;
|
user = _userManager.GetUserById(userId.Value);
|
||||||
_userManager = userManager;
|
|
||||||
_dtoService = dtoService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var query = new InternalItemsQuery(user)
|
||||||
/// 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 }
|
ExcludeItemTypes = excludeItemTypes,
|
||||||
.AddClientFields(User)
|
IncludeItemTypes = includeItemTypes,
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
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;
|
if (parentId.HasValue)
|
||||||
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
|
{
|
||||||
|
if (parentItem is Folder)
|
||||||
if (userId.HasValue && !userId.Equals(default))
|
|
||||||
{
|
{
|
||||||
user = _userManager.GetUserById(userId.Value);
|
query.AncestorIds = new[] { parentId.Value };
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var query = new InternalItemsQuery(user)
|
|
||||||
{
|
{
|
||||||
ExcludeItemTypes = excludeItemTypes,
|
query.ItemIds = new[] { parentId.Value };
|
||||||
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.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>
|
// Studios
|
||||||
/// Gets all album artists from a given item, folder, or the entire library.
|
if (studios.Length != 0)
|
||||||
/// </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 }
|
query.StudioIds = studios.Select(i =>
|
||||||
.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);
|
try
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
return _libraryManager.GetStudio(i);
|
||||||
}
|
}
|
||||||
else
|
catch
|
||||||
{
|
{
|
||||||
query.ItemIds = new[] { parentId.Value };
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
|
||||||
|
|
||||||
// Studios
|
|
||||||
if (studios.Length != 0)
|
|
||||||
{
|
|
||||||
query.StudioIds = studios.Select(i =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _libraryManager.GetStudio(i);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
foreach (var filter in filters)
|
||||||
/// 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);
|
switch (filter)
|
||||||
|
|
||||||
var item = _libraryManager.GetArtist(name, dtoOptions);
|
|
||||||
|
|
||||||
if (userId.HasValue && !userId.Value.Equals(default))
|
|
||||||
{
|
{
|
||||||
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The audio controller.
|
/// Initializes a new instance of the <see cref="AudioController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
// TODO: In order to authenticate this in the future, Dlna playback will require updating
|
/// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
|
||||||
public class AudioController : BaseJellyfinApiController
|
public AudioController(AudioHelper audioHelper)
|
||||||
{
|
{
|
||||||
private readonly AudioHelper _audioHelper;
|
_audioHelper = audioHelper;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
|
/// <summary>
|
||||||
|
/// Gets an audio stream.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Initializes a new instance of the <see cref="AudioController"/> class.
|
/// <param name="itemId">The item id.</param>
|
||||||
/// </summary>
|
/// <param name="container">The audio container.</param>
|
||||||
/// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</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>
|
||||||
public AudioController(AudioHelper audioHelper)
|
/// <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>
|
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
|
||||||
/// Gets an audio stream.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">The item id.</param>
|
/// <summary>
|
||||||
/// <param name="container">The audio container.</param>
|
/// Gets an audio stream.
|
||||||
/// <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>
|
/// </summary>
|
||||||
/// <param name="params">The streaming parameters.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="tag">The tag.</param>
|
/// <param name="container">The audio container.</param>
|
||||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</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="playSessionId">The play session id.</param>
|
/// <param name="params">The streaming parameters.</param>
|
||||||
/// <param name="segmentContainer">The segment container.</param>
|
/// <param name="tag">The tag.</param>
|
||||||
/// <param name="segmentLength">The segment length.</param>
|
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||||
/// <param name="minSegments">The minimum number of segments.</param>
|
/// <param name="playSessionId">The play session id.</param>
|
||||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
/// <param name="segmentContainer">The segment container.</param>
|
||||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
/// <param name="segmentLength">The segment length.</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="minSegments">The minimum number of segments.</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="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</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="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</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="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</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="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</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="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</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="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</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="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
|
||||||
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
|
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
||||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</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="width">Optional. The fixed horizontal resolution of the encoded video.</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="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</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="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
||||||
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
||||||
/// <param name="maxRefFrames">Optional.</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="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
||||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
||||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
/// <param name="maxRefFrames">Optional.</param>
|
||||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||||
/// <param name="liveStreamId">The live stream id.</param>
|
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</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="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
/// <param name="liveStreamId">The live stream id.</param>
|
||||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</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="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="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||||
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||||
/// <response code="200">Audio stream returned.</response>
|
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
||||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
||||||
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
|
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||||
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
|
/// <response code="200">Audio stream returned.</response>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||||
[ProducesAudioFile]
|
[HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
|
||||||
public async Task<ActionResult> GetAudioStream(
|
[HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
|
||||||
[FromRoute, Required] Guid itemId,
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[FromQuery] string? container,
|
[ProducesAudioFile]
|
||||||
[FromQuery] bool? @static,
|
public async Task<ActionResult> GetAudioStreamByContainer(
|
||||||
[FromQuery] string? @params,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] string? tag,
|
[FromRoute, Required] string container,
|
||||||
[FromQuery] string? deviceProfileId,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? segmentContainer,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] string? deviceProfileId,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? segmentContainer,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] string? audioCodec,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] string? audioCodec,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] bool? breakOnNonKeyFrames,
|
||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] int? audioBitRate,
|
||||||
[FromQuery] string? level,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] string? level,
|
||||||
[FromQuery] long? startTimeTicks,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] int? width,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] int? height,
|
[FromQuery] bool? copyTimestamps,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] long? startTimeTicks,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? width,
|
||||||
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] bool? deInterlace,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] bool? requireNonAnamorphic,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] int? transcodingMaxAudioChannels,
|
[FromQuery] bool? requireAvc,
|
||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] bool? deInterlace,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] bool? requireNonAnamorphic,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] int? transcodingMaxAudioChannels,
|
||||||
[FromQuery] string? videoCodec,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? subtitleCodec,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] string? videoCodec,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] string? subtitleCodec,
|
||||||
[FromQuery] EncodingContext? context,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] Dictionary<string, string>? streamOptions)
|
[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,
|
||||||
Id = itemId,
|
Static = @static ?? false,
|
||||||
Container = container,
|
Params = @params,
|
||||||
Static = @static ?? false,
|
Tag = tag,
|
||||||
Params = @params,
|
DeviceProfileId = deviceProfileId,
|
||||||
Tag = tag,
|
PlaySessionId = playSessionId,
|
||||||
DeviceProfileId = deviceProfileId,
|
SegmentContainer = segmentContainer,
|
||||||
PlaySessionId = playSessionId,
|
SegmentLength = segmentLength,
|
||||||
SegmentContainer = segmentContainer,
|
MinSegments = minSegments,
|
||||||
SegmentLength = segmentLength,
|
MediaSourceId = mediaSourceId,
|
||||||
MinSegments = minSegments,
|
DeviceId = deviceId,
|
||||||
MediaSourceId = mediaSourceId,
|
AudioCodec = audioCodec,
|
||||||
DeviceId = deviceId,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AudioCodec = audioCodec,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AudioSampleRate = audioSampleRate,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioBitRate = audioBitRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioBitDepth = maxAudioBitDepth,
|
||||||
AudioBitRate = audioBitRate,
|
AudioChannels = audioChannels,
|
||||||
MaxAudioBitDepth = maxAudioBitDepth,
|
Profile = profile,
|
||||||
AudioChannels = audioChannels,
|
Level = level,
|
||||||
Profile = profile,
|
Framerate = framerate,
|
||||||
Level = level,
|
MaxFramerate = maxFramerate,
|
||||||
Framerate = framerate,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
MaxFramerate = maxFramerate,
|
StartTimeTicks = startTimeTicks,
|
||||||
CopyTimestamps = copyTimestamps ?? false,
|
Width = width,
|
||||||
StartTimeTicks = startTimeTicks,
|
Height = height,
|
||||||
Width = width,
|
VideoBitRate = videoBitRate,
|
||||||
Height = height,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
VideoBitRate = videoBitRate,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
MaxRefFrames = maxRefFrames,
|
||||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
MaxRefFrames = maxRefFrames,
|
RequireAvc = requireAvc ?? false,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireAvc = requireAvc ?? false,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
DeInterlace = deInterlace ?? false,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
LiveStreamId = liveStreamId,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
LiveStreamId = liveStreamId,
|
VideoCodec = videoCodec,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
SubtitleCodec = subtitleCodec,
|
||||||
VideoCodec = videoCodec,
|
TranscodeReasons = transcodeReasons,
|
||||||
SubtitleCodec = subtitleCodec,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
TranscodeReasons = transcodeReasons,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
Context = context ?? EncodingContext.Static,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
StreamOptions = streamOptions
|
||||||
Context = context ?? EncodingContext.Static,
|
};
|
||||||
StreamOptions = streamOptions
|
|
||||||
};
|
|
||||||
|
|
||||||
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,54 +4,53 @@ using MediaBrowser.Model.Branding;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Branding controller.
|
/// Initializes a new instance of the <see cref="BrandingController"/> class.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BrandingController"/> class.
|
/// Gets branding configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <response code="200">Branding configuration returned.</response>
|
||||||
public BrandingController(IServerConfigurationManager serverConfigurationManager)
|
/// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns>
|
||||||
{
|
[HttpGet("Configuration")]
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
}
|
public ActionResult<BrandingOptions> GetBrandingOptions()
|
||||||
|
{
|
||||||
|
return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets branding configuration.
|
/// Gets branding css.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Branding configuration returned.</response>
|
/// <response code="200">Branding css returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns>
|
/// <response code="204">No branding css configured.</response>
|
||||||
[HttpGet("Configuration")]
|
/// <returns>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// An <see cref="OkResult"/> containing the branding css if exist,
|
||||||
public ActionResult<BrandingOptions> GetBrandingOptions()
|
/// or a <see cref="NoContentResult"/> if the css is not configured.
|
||||||
{
|
/// </returns>
|
||||||
return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
[HttpGet("Css")]
|
||||||
}
|
[HttpGet("Css.css", Name = "GetBrandingCss_2")]
|
||||||
|
[Produces("text/css")]
|
||||||
/// <summary>
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
/// Gets branding css.
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
/// </summary>
|
public ActionResult<string> GetBrandingCss()
|
||||||
/// <response code="200">Branding css returned.</response>
|
{
|
||||||
/// <response code="204">No branding css configured.</response>
|
var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||||
/// <returns>
|
return options.CustomCss ?? string.Empty;
|
||||||
/// 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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Channels Controller.
|
/// Initializes a new instance of the <see cref="ChannelsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param>
|
||||||
public class ChannelsController : BaseJellyfinApiController
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
public ChannelsController(IChannelManager channelManager, IUserManager userManager)
|
||||||
{
|
{
|
||||||
private readonly IChannelManager _channelManager;
|
_channelManager = channelManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ChannelsController"/> class.
|
/// Gets available channels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param>
|
/// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||||
public ChannelsController(IChannelManager channelManager, IUserManager userManager)
|
/// <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;
|
Limit = limit,
|
||||||
_userManager = userManager;
|
StartIndex = startIndex,
|
||||||
}
|
UserId = userId ?? Guid.Empty,
|
||||||
|
SupportsLatestItems = supportsLatestItems,
|
||||||
|
SupportsMediaDeletion = supportsMediaDeletion,
|
||||||
|
IsFavorite = isFavorite
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets available channels.
|
/// Get all channel features.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</param>
|
/// <response code="200">All channel features returned.</response>
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
[HttpGet("Features")]
|
||||||
/// <param name="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param>
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
/// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param>
|
public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures()
|
||||||
/// <param name="isFavorite">Optional. Filter by channels that are favorite.</param>
|
{
|
||||||
/// <response code="200">Channels returned.</response>
|
return _channelManager.GetAllChannelFeatures();
|
||||||
/// <returns>An <see cref="OkResult"/> containing the channels.</returns>
|
}
|
||||||
[HttpGet]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <summary>
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetChannels(
|
/// Get channel features.
|
||||||
[FromQuery] Guid? userId,
|
/// </summary>
|
||||||
[FromQuery] int? startIndex,
|
/// <param name="channelId">Channel id.</param>
|
||||||
[FromQuery] int? limit,
|
/// <response code="200">Channel features returned.</response>
|
||||||
[FromQuery] bool? supportsLatestItems,
|
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
|
||||||
[FromQuery] bool? supportsMediaDeletion,
|
[HttpGet("{channelId}/Features")]
|
||||||
[FromQuery] bool? isFavorite)
|
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,
|
case ItemFilter.IsFolder:
|
||||||
StartIndex = startIndex,
|
query.IsFolder = true;
|
||||||
UserId = userId ?? Guid.Empty,
|
break;
|
||||||
SupportsLatestItems = supportsLatestItems,
|
case ItemFilter.IsNotFolder:
|
||||||
SupportsMediaDeletion = supportsMediaDeletion,
|
query.IsFolder = false;
|
||||||
IsFavorite = isFavorite
|
break;
|
||||||
});
|
case ItemFilter.IsUnplayed:
|
||||||
}
|
query.IsPlayed = false;
|
||||||
|
break;
|
||||||
/// <summary>
|
case ItemFilter.IsPlayed:
|
||||||
/// Get all channel features.
|
query.IsPlayed = true;
|
||||||
/// </summary>
|
break;
|
||||||
/// <response code="200">All channel features returned.</response>
|
case ItemFilter.IsFavorite:
|
||||||
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
|
query.IsFavorite = true;
|
||||||
[HttpGet("Features")]
|
break;
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
case ItemFilter.IsResumable:
|
||||||
public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures()
|
query.IsResumable = true;
|
||||||
{
|
break;
|
||||||
return _channelManager.GetAllChannelFeatures();
|
case ItemFilter.Likes:
|
||||||
}
|
query.IsLiked = true;
|
||||||
|
break;
|
||||||
/// <summary>
|
case ItemFilter.Dislikes:
|
||||||
/// Get channel features.
|
query.IsLiked = false;
|
||||||
/// </summary>
|
break;
|
||||||
/// <param name="channelId">Channel id.</param>
|
case ItemFilter.IsFavoriteOrLikes:
|
||||||
/// <response code="200">Channel features returned.</response>
|
query.IsFavoriteOrLiked = true;
|
||||||
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
|
break;
|
||||||
[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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
|
||||||
/// Gets latest channel items.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">Optional. User Id.</param>
|
/// <summary>
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
/// Gets latest channel items.
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
/// </summary>
|
||||||
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
/// <param name="userId">Optional. User Id.</param>
|
||||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</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="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <response code="200">Latest channel items returned.</response>
|
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
||||||
/// <returns>
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
/// A <see cref="Task"/> representing the request to get the latest channel items.
|
/// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the latest channel items.
|
/// <response code="200">Latest channel items returned.</response>
|
||||||
/// </returns>
|
/// <returns>
|
||||||
[HttpGet("Items/Latest")]
|
/// A <see cref="Task"/> representing the request to get the latest channel items.
|
||||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
|
/// The task result contains an <see cref="OkResult"/> containing the latest channel items.
|
||||||
[FromQuery] Guid? userId,
|
/// </returns>
|
||||||
[FromQuery] int? startIndex,
|
[HttpGet("Items/Latest")]
|
||||||
[FromQuery] int? limit,
|
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
|
[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)
|
Limit = limit,
|
||||||
? null
|
StartIndex = startIndex,
|
||||||
: _userManager.GetUserById(userId.Value);
|
ChannelIds = channelIds,
|
||||||
|
DtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
};
|
||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
foreach (var filter in filters)
|
||||||
|
{
|
||||||
|
switch (filter)
|
||||||
{
|
{
|
||||||
Limit = limit,
|
case ItemFilter.IsFolder:
|
||||||
StartIndex = startIndex,
|
query.IsFolder = true;
|
||||||
ChannelIds = channelIds,
|
break;
|
||||||
DtoOptions = new DtoOptions { Fields = fields }
|
case ItemFilter.IsNotFolder:
|
||||||
};
|
query.IsFolder = false;
|
||||||
|
break;
|
||||||
foreach (var filter in filters)
|
case ItemFilter.IsUnplayed:
|
||||||
{
|
query.IsPlayed = false;
|
||||||
switch (filter)
|
break;
|
||||||
{
|
case ItemFilter.IsPlayed:
|
||||||
case ItemFilter.IsFolder:
|
query.IsPlayed = true;
|
||||||
query.IsFolder = true;
|
break;
|
||||||
break;
|
case ItemFilter.IsFavorite:
|
||||||
case ItemFilter.IsNotFolder:
|
query.IsFavorite = true;
|
||||||
query.IsFolder = false;
|
break;
|
||||||
break;
|
case ItemFilter.IsResumable:
|
||||||
case ItemFilter.IsUnplayed:
|
query.IsResumable = true;
|
||||||
query.IsPlayed = false;
|
break;
|
||||||
break;
|
case ItemFilter.Likes:
|
||||||
case ItemFilter.IsPlayed:
|
query.IsLiked = true;
|
||||||
query.IsPlayed = true;
|
break;
|
||||||
break;
|
case ItemFilter.Dislikes:
|
||||||
case ItemFilter.IsFavorite:
|
query.IsLiked = false;
|
||||||
query.IsFavorite = true;
|
break;
|
||||||
break;
|
case ItemFilter.IsFavoriteOrLikes:
|
||||||
case ItemFilter.IsResumable:
|
query.IsFavoriteOrLiked = true;
|
||||||
query.IsResumable = true;
|
break;
|
||||||
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Client log controller.
|
/// Initializes a new instance of the <see cref="ClientLogController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param>
|
||||||
public class ClientLogController : BaseJellyfinApiController
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
|
public ClientLogController(
|
||||||
|
IClientEventLogger clientEventLogger,
|
||||||
|
IServerConfigurationManager serverConfigurationManager)
|
||||||
{
|
{
|
||||||
private const int MaxDocumentSize = 1_000_000;
|
_clientEventLogger = clientEventLogger;
|
||||||
private readonly IClientEventLogger _clientEventLogger;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ClientLogController"/> class.
|
/// Upload a document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param>
|
/// <response code="200">Document saved.</response>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <response code="403">Event logging disabled.</response>
|
||||||
public ClientLogController(
|
/// <response code="413">Upload size too large.</response>
|
||||||
IClientEventLogger clientEventLogger,
|
/// <returns>Create response.</returns>
|
||||||
IServerConfigurationManager serverConfigurationManager)
|
[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;
|
return Forbid();
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (Request.ContentLength > MaxDocumentSize)
|
||||||
/// 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)
|
// Manually validate to return proper status code.
|
||||||
{
|
return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
|
||||||
return Forbid();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Request.ContentLength > MaxDocumentSize)
|
|
||||||
{
|
|
||||||
// Manually validate to return proper status code.
|
|
||||||
return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
|
|
||||||
}
|
|
||||||
|
|
||||||
var (clientName, clientVersion) = GetRequestInformation();
|
|
||||||
var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return Ok(new ClientLogDocumentResponseDto(fileName));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private (string ClientName, string ClientVersion) GetRequestInformation()
|
var (clientName, clientVersion) = GetRequestInformation();
|
||||||
{
|
var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
|
||||||
var clientName = HttpContext.User.GetClient() ?? "unknown-client";
|
.ConfigureAwait(false);
|
||||||
var clientVersion = HttpContext.User.GetIsApiKey()
|
return Ok(new ClientLogDocumentResponseDto(fileName));
|
||||||
? "apikey"
|
}
|
||||||
: HttpContext.User.GetVersion() ?? "unknown-version";
|
|
||||||
|
|
||||||
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The collection controller.
|
/// Initializes a new instance of the <see cref="CollectionController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Collections")]
|
/// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
|
||||||
public class CollectionController : BaseJellyfinApiController
|
public CollectionController(
|
||||||
|
ICollectionManager collectionManager,
|
||||||
|
IDtoService dtoService)
|
||||||
{
|
{
|
||||||
private readonly ICollectionManager _collectionManager;
|
_collectionManager = collectionManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CollectionController"/> class.
|
/// Creates a new collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param>
|
/// <param name="name">The name of the collection.</param>
|
||||||
/// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
|
/// <param name="ids">Item Ids to add to the collection.</param>
|
||||||
public CollectionController(
|
/// <param name="parentId">Optional. Create the collection within a specific folder.</param>
|
||||||
ICollectionManager collectionManager,
|
/// <param name="isLocked">Whether or not to lock the new collection.</param>
|
||||||
IDtoService dtoService)
|
/// <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;
|
IsLocked = isLocked,
|
||||||
_dtoService = dtoService;
|
Name = name,
|
||||||
}
|
ParentId = parentId,
|
||||||
|
ItemIdList = ids,
|
||||||
|
UserIds = new[] { userId }
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
/// <summary>
|
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||||
/// Creates a new collection.
|
|
||||||
/// </summary>
|
var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||||
/// <param name="name">The name of the collection.</param>
|
|
||||||
/// <param name="ids">Item Ids to add to the collection.</param>
|
return new CollectionCreationResult
|
||||||
/// <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();
|
Id = dto.Id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
/// <summary>
|
||||||
{
|
/// Adds items to a collection.
|
||||||
IsLocked = isLocked,
|
/// </summary>
|
||||||
Name = name,
|
/// <param name="collectionId">The collection id.</param>
|
||||||
ParentId = parentId,
|
/// <param name="ids">Item ids, comma delimited.</param>
|
||||||
ItemIdList = ids,
|
/// <response code="204">Items added to collection.</response>
|
||||||
UserIds = new[] { userId }
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
}).ConfigureAwait(false);
|
[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);
|
/// <summary>
|
||||||
|
/// Removes items from a collection.
|
||||||
var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
|
/// </summary>
|
||||||
|
/// <param name="collectionId">The collection id.</param>
|
||||||
return new CollectionCreationResult
|
/// <param name="ids">Item ids, comma delimited.</param>
|
||||||
{
|
/// <response code="204">Items removed from collection.</response>
|
||||||
Id = dto.Id
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
};
|
[HttpDelete("{collectionId}/Items")]
|
||||||
}
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public async Task<ActionResult> RemoveFromCollection(
|
||||||
/// <summary>
|
[FromRoute, Required] Guid collectionId,
|
||||||
/// Adds items to a collection.
|
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
||||||
/// </summary>
|
{
|
||||||
/// <param name="collectionId">The collection id.</param>
|
await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
|
||||||
/// <param name="ids">Item ids, comma delimited.</param>
|
return NoContent();
|
||||||
/// <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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,124 +13,123 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Configuration Controller.
|
/// Initializes a new instance of the <see cref="ConfigurationController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("System")]
|
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||||
public class ConfigurationController : BaseJellyfinApiController
|
public ConfigurationController(
|
||||||
|
IServerConfigurationManager configurationManager,
|
||||||
|
IMediaEncoder mediaEncoder)
|
||||||
{
|
{
|
||||||
private readonly IServerConfigurationManager _configurationManager;
|
_configurationManager = configurationManager;
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
_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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ConfigurationController"/> class.
|
/// Updates application configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <param name="configuration">Configuration.</param>
|
||||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
/// <response code="204">Configuration updated.</response>
|
||||||
public ConfigurationController(
|
/// <returns>Update status.</returns>
|
||||||
IServerConfigurationManager configurationManager,
|
[HttpPost("Configuration")]
|
||||||
IMediaEncoder mediaEncoder)
|
[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;
|
throw new ArgumentException("Body doesn't contain a valid configuration");
|
||||||
_mediaEncoder = mediaEncoder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
_configurationManager.SaveConfiguration(key, deserializedConfiguration);
|
||||||
/// Gets application configuration.
|
return NoContent();
|
||||||
/// </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>
|
/// <summary>
|
||||||
/// Updates application configuration.
|
/// Gets a default MetadataOptions object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configuration">Configuration.</param>
|
/// <response code="200">Metadata options returned.</response>
|
||||||
/// <response code="204">Configuration updated.</response>
|
/// <returns>Default MetadataOptions.</returns>
|
||||||
/// <returns>Update status.</returns>
|
[HttpGet("Configuration/MetadataOptions/Default")]
|
||||||
[HttpPost("Configuration")]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
public ActionResult<MetadataOptions> GetDefaultMetadataOptions()
|
||||||
public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration)
|
{
|
||||||
{
|
return new MetadataOptions();
|
||||||
_configurationManager.ReplaceConfiguration(configuration);
|
}
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a named configuration.
|
/// Updates the path to the media encoder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">Configuration key.</param>
|
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
|
||||||
/// <response code="200">Configuration returned.</response>
|
/// <response code="204">Media encoder path updated.</response>
|
||||||
/// <returns>Configuration.</returns>
|
/// <returns>Status.</returns>
|
||||||
[HttpGet("Configuration/{key}")]
|
[HttpPost("MediaEncoder/Path")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
||||||
[ProducesFile(MediaTypeNames.Application.Json)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult<object> GetNamedConfiguration([FromRoute, Required] string key)
|
public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
|
||||||
{
|
{
|
||||||
return _configurationManager.GetConfiguration(key);
|
_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
|
||||||
}
|
return NoContent();
|
||||||
|
|
||||||
/// <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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,103 +14,102 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// The dashboard controller.
|
/// Initializes a new instance of the <see cref="DashboardController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
|
||||||
public class DashboardController : BaseJellyfinApiController
|
/// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
|
||||||
|
public DashboardController(
|
||||||
|
ILogger<DashboardController> logger,
|
||||||
|
IPluginManager pluginManager)
|
||||||
{
|
{
|
||||||
private readonly ILogger<DashboardController> _logger;
|
_logger = logger;
|
||||||
private readonly IPluginManager _pluginManager;
|
_pluginManager = pluginManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DashboardController"/> class.
|
/// Gets the configuration pages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
|
/// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
|
||||||
/// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
|
/// <response code="200">ConfigurationPages returned.</response>
|
||||||
public DashboardController(
|
/// <response code="404">Server still loading.</response>
|
||||||
ILogger<DashboardController> logger,
|
/// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
|
||||||
IPluginManager pluginManager)
|
[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;
|
configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
|
||||||
_pluginManager = pluginManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return configPages;
|
||||||
/// Gets the configuration pages.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
|
/// <summary>
|
||||||
/// <response code="200">ConfigurationPages returned.</response>
|
/// Gets a dashboard configuration page.
|
||||||
/// <response code="404">Server still loading.</response>
|
/// </summary>
|
||||||
/// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
|
/// <param name="name">The name of the page.</param>
|
||||||
[HttpGet("web/ConfigurationPages")]
|
/// <response code="200">ConfigurationPage returned.</response>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <response code="404">Plugin configuration page not found.</response>
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
/// <returns>The configuration page.</returns>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[HttpGet("web/ConfigurationPage")]
|
||||||
public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[FromQuery] bool? enableInMainMenu)
|
[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();
|
return NotFound();
|
||||||
|
|
||||||
if (enableInMainMenu.HasValue)
|
|
||||||
{
|
|
||||||
configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return configPages;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
IPlugin plugin = altPage.Item2;
|
||||||
/// Gets a dashboard configuration page.
|
string resourcePath = altPage.Item1.EmbeddedResourcePath;
|
||||||
/// </summary>
|
Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath);
|
||||||
/// <param name="name">The name of the page.</param>
|
if (stream is null)
|
||||||
/// <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));
|
_logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name);
|
||||||
if (altPage is null)
|
return NotFound();
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
|
||||||
{
|
}
|
||||||
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));
|
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Devices Controller.
|
/// Initializes a new instance of the <see cref="DevicesController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
|
||||||
public class DevicesController : BaseJellyfinApiController
|
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
||||||
|
public DevicesController(
|
||||||
|
IDeviceManager deviceManager,
|
||||||
|
ISessionManager sessionManager)
|
||||||
{
|
{
|
||||||
private readonly IDeviceManager _deviceManager;
|
_deviceManager = deviceManager;
|
||||||
private readonly ISessionManager _sessionManager;
|
_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DevicesController"/> class.
|
/// Get Devices.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
|
/// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
|
||||||
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
/// <param name="userId">Gets or sets the user identifier.</param>
|
||||||
public DevicesController(
|
/// <response code="200">Devices retrieved.</response>
|
||||||
IDeviceManager deviceManager,
|
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
||||||
ISessionManager sessionManager)
|
[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;
|
return NotFound();
|
||||||
_sessionManager = sessionManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return deviceInfo;
|
||||||
/// Get Devices.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
|
/// <summary>
|
||||||
/// <param name="userId">Gets or sets the user identifier.</param>
|
/// Get options for a device.
|
||||||
/// <response code="200">Devices retrieved.</response>
|
/// </summary>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
/// <param name="id">Device Id.</param>
|
||||||
[HttpGet]
|
/// <response code="200">Device options retrieved.</response>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <response code="404">Device not found.</response>
|
||||||
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
|
/// <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>
|
return deviceInfo;
|
||||||
/// 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;
|
/// <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>
|
var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
return deviceInfo;
|
foreach (var session in sessions.Items)
|
||||||
|
{
|
||||||
|
await _sessionManager.Logout(session).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return NoContent();
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,201 +14,200 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// Display Preferences Controller.
|
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
|
||||||
public class DisplayPreferencesController : BaseJellyfinApiController
|
/// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
|
||||||
|
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
|
||||||
{
|
{
|
||||||
private readonly IDisplayPreferencesManager _displayPreferencesManager;
|
_displayPreferencesManager = displayPreferencesManager;
|
||||||
private readonly ILogger<DisplayPreferencesController> _logger;
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
/// Get Display Preferences.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
|
/// <param name="displayPreferencesId">Display preferences id.</param>
|
||||||
/// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
|
/// <param name="userId">User id.</param>
|
||||||
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
|
/// <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;
|
itemId = displayPreferencesId.GetMD5();
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
|
||||||
/// Get Display Preferences.
|
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
|
||||||
/// </summary>
|
itemPreferences.ItemId = itemId;
|
||||||
/// <param name="displayPreferencesId">Display preferences id.</param>
|
|
||||||
/// <param name="userId">User id.</param>
|
var dto = new DisplayPreferencesDto
|
||||||
/// <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))
|
Client = displayPreferences.Client,
|
||||||
{
|
Id = displayPreferences.ItemId.ToString(),
|
||||||
itemId = displayPreferencesId.GetMD5();
|
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);
|
foreach (var homeSection in displayPreferences.HomeSections)
|
||||||
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
|
{
|
||||||
itemPreferences.ItemId = itemId;
|
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
|
||||||
/// Update Display Preferences.
|
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
|
||||||
/// </summary>
|
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
|
||||||
/// <param name="displayPreferencesId">Display preferences id.</param>
|
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
|
||||||
/// <param name="userId">User Id.</param>
|
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
|
||||||
/// <param name="client">Client.</param>
|
dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme;
|
||||||
/// <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))
|
// 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);
|
displayPreferences.CustomPrefs.Remove(key);
|
||||||
existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
|
existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
|
||||||
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
|
}
|
||||||
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
|
|
||||||
|
|
||||||
existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection;
|
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
|
||||||
existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
|
{
|
||||||
&& !string.IsNullOrEmpty(chromecastVersion)
|
if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
|
||||||
? 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);
|
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
|
||||||
if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
|
|
||||||
{
|
|
||||||
type = order < 8 ? defaults[order] : HomeSectionType.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
displayPreferences.CustomPrefs.Remove(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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Dlna Controller.
|
/// Initializes a new instance of the <see cref="DlnaController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||||
public class DlnaController : BaseJellyfinApiController
|
public DlnaController(IDlnaManager dlnaManager)
|
||||||
{
|
{
|
||||||
private readonly IDlnaManager _dlnaManager;
|
_dlnaManager = dlnaManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DlnaController"/> class.
|
/// Get profile infos.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
/// <response code="200">Device profile infos returned.</response>
|
||||||
public DlnaController(IDlnaManager dlnaManager)
|
/// <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>
|
return profile;
|
||||||
/// Get profile infos.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">Device profile infos returned.</response>
|
/// <summary>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns>
|
/// Deletes a profile.
|
||||||
[HttpGet("ProfileInfos")]
|
/// </summary>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <param name="profileId">Profile id.</param>
|
||||||
public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos()
|
/// <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>
|
_dlnaManager.DeleteProfile(profileId);
|
||||||
/// Gets the default profile.
|
return NoContent();
|
||||||
/// </summary>
|
}
|
||||||
/// <response code="200">Default device profile returned.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the default profile.</returns>
|
/// <summary>
|
||||||
[HttpGet("Profiles/Default")]
|
/// Creates a profile.
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// </summary>
|
||||||
public ActionResult<DeviceProfile> GetDefaultProfile()
|
/// <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>
|
_dlnaManager.UpdateProfile(profileId, deviceProfile);
|
||||||
/// Gets a single profile.
|
return NoContent();
|
||||||
/// </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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,311 +14,310 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Dlna Server Controller.
|
/// Initializes a new instance of the <see cref="DlnaServerController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Dlna")]
|
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||||
[DlnaEnabled]
|
public DlnaServerController(IDlnaManager dlnaManager)
|
||||||
[Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
|
|
||||||
public class DlnaServerController : BaseJellyfinApiController
|
|
||||||
{
|
{
|
||||||
private readonly IDlnaManager _dlnaManager;
|
_dlnaManager = dlnaManager;
|
||||||
private readonly IContentDirectory _contentDirectory;
|
_contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
|
||||||
private readonly IConnectionManager _connectionManager;
|
_connectionManager = DlnaEntryPoint.Current.ConnectionManager;
|
||||||
private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
|
_mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DlnaServerController"/> class.
|
/// Get Description Xml.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
public DlnaServerController(IDlnaManager dlnaManager)
|
/// <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;
|
return NotFound();
|
||||||
_contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
|
|
||||||
_connectionManager = DlnaEntryPoint.Current.ConnectionManager;
|
|
||||||
_mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return File(icon.Stream, MimeTypes.GetMimeType(fileName));
|
||||||
/// 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>
|
private string GetAbsoluteUri()
|
||||||
/// Gets Dlna content directory xml.
|
{
|
||||||
/// </summary>
|
return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
|
||||||
/// <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>
|
private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
|
||||||
/// Gets Dlna media receiver registrar xml.
|
{
|
||||||
/// </summary>
|
return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers)
|
||||||
/// <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());
|
InputXml = requestStream,
|
||||||
}
|
TargetServerUuId = id,
|
||||||
|
RequestedUrl = GetAbsoluteUri()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager)
|
||||||
/// Gets Dlna media receiver registrar xml.
|
{
|
||||||
/// </summary>
|
var subscriptionId = Request.Headers["SID"];
|
||||||
/// <param name="serverId">Server UUID.</param>
|
if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
|
||||||
/// <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());
|
var notificationType = Request.Headers["NT"];
|
||||||
}
|
var callback = Request.Headers["CALLBACK"];
|
||||||
|
var timeoutString = Request.Headers["TIMEOUT"];
|
||||||
|
|
||||||
/// <summary>
|
if (string.IsNullOrEmpty(notificationType))
|
||||||
/// 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)
|
|
||||||
{
|
{
|
||||||
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 dlnaEventManager.CancelEventSubscription(subscriptionId);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -12,186 +12,185 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// Environment Controller.
|
/// Initializes a new instance of the <see cref="EnvironmentController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
public class EnvironmentController : BaseJellyfinApiController
|
/// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param>
|
||||||
|
public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger)
|
||||||
{
|
{
|
||||||
private const char UncSeparator = '\\';
|
_fileSystem = fileSystem;
|
||||||
private const string UncStartPrefix = @"\\";
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly IFileSystem _fileSystem;
|
/// <summary>
|
||||||
private readonly ILogger<EnvironmentController> _logger;
|
/// Gets the contents of a given directory in the file system.
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <param name="path">The path.</param>
|
||||||
/// Initializes a new instance of the <see cref="EnvironmentController"/> class.
|
/// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param>
|
||||||
/// </summary>
|
/// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <response code="200">Directory contents returned.</response>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param>
|
/// <returns>Directory contents.</returns>
|
||||||
public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger)
|
[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;
|
return Array.Empty<FileSystemEntryInfo>();
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var entries =
|
||||||
/// Gets the contents of a given directory in the file system.
|
_fileSystem.GetFileSystemEntries(path)
|
||||||
/// </summary>
|
.Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles))
|
||||||
/// <param name="path">The path.</param>
|
.OrderBy(i => i.FullName);
|
||||||
/// <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>
|
return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File));
|
||||||
/// <response code="200">Directory contents returned.</response>
|
}
|
||||||
/// <returns>Directory contents.</returns>
|
|
||||||
[HttpGet("DirectoryContents")]
|
/// <summary>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// Validates path.
|
||||||
public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
|
/// </summary>
|
||||||
[FromQuery, Required] string path,
|
/// <param name="validatePathDto">Validate request object.</param>
|
||||||
[FromQuery] bool includeFiles = false,
|
/// <response code="204">Path validated.</response>
|
||||||
[FromQuery] bool includeDirectories = false)
|
/// <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)
|
if (validatePathDto.IsFile.Value)
|
||||||
&& path.LastIndexOf(UncSeparator) == 1)
|
|
||||||
{
|
{
|
||||||
return Array.Empty<FileSystemEntryInfo>();
|
if (!System.IO.File.Exists(validatePathDto.Path))
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
return NotFound();
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(validatePathDto.Path))
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
|
if (!Directory.Exists(validatePathDto.Path))
|
||||||
{
|
{
|
||||||
return NotFound();
|
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();
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
/// <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");
|
if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
// Check if unc share
|
return NotFound();
|
||||||
var index = path.LastIndexOf(UncSeparator);
|
}
|
||||||
|
|
||||||
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>
|
return NoContent();
|
||||||
/// Get Default directory browser.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">Default directory browser returned.</response>
|
/// <summary>
|
||||||
/// <returns>Default directory browser.</returns>
|
/// Gets network paths.
|
||||||
[HttpGet("DefaultDirectoryBrowser")]
|
/// </summary>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <response code="200">Empty array returned.</response>
|
||||||
public ActionResult<DefaultDirectoryBrowserInfoDto> GetDefaultDirectoryBrowser()
|
/// <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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Filters controller.
|
/// Initializes a new instance of the <see cref="FilterController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
public class FilterController : BaseJellyfinApiController
|
public FilterController(ILibraryManager libraryManager, IUserManager userManager)
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="FilterController"/> class.
|
/// Gets legacy query filters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="userId">Optional. User id.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="parentId">Optional. Parent id.</param>
|
||||||
public FilterController(ILibraryManager libraryManager, IUserManager userManager)
|
/// <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;
|
item = _libraryManager.GetParentItem(parentId, user?.Id);
|
||||||
_userManager = userManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var query = new InternalItemsQuery
|
||||||
/// 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)
|
User = user,
|
||||||
? null
|
MediaTypes = mediaTypes,
|
||||||
: _userManager.GetUserById(userId.Value);
|
IncludeItemTypes = includeItemTypes,
|
||||||
|
Recursive = true,
|
||||||
BaseItem? item = null;
|
EnableTotalRecordCount = false,
|
||||||
if (includeItemTypes.Length != 1
|
DtoOptions = new DtoOptions
|
||||||
|| !(includeItemTypes[0] == BaseItemKind.BoxSet
|
|
||||||
|| includeItemTypes[0] == BaseItemKind.Playlist
|
|
||||||
|| includeItemTypes[0] == BaseItemKind.Trailer
|
|
||||||
|| includeItemTypes[0] == BaseItemKind.Program))
|
|
||||||
{
|
{
|
||||||
item = _libraryManager.GetParentItem(parentId, user?.Id);
|
Fields = new[] { ItemFields.Genres, ItemFields.Tags },
|
||||||
|
EnableImages = false,
|
||||||
|
EnableUserData = false
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var query = new InternalItemsQuery
|
if (item is not Folder folder)
|
||||||
{
|
{
|
||||||
User = user,
|
return new QueryFiltersLegacy();
|
||||||
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()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var itemList = folder.GetItemList(query);
|
||||||
/// Gets query filters.
|
return new QueryFiltersLegacy
|
||||||
/// </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)
|
Years = itemList.Select(i => i.ProductionYear ?? -1)
|
||||||
? null
|
.Where(i => i > 0)
|
||||||
: _userManager.GetUserById(userId.Value);
|
.Distinct()
|
||||||
|
.Order()
|
||||||
|
.ToArray(),
|
||||||
|
|
||||||
BaseItem? parentItem = null;
|
Genres = itemList.SelectMany(i => i.Genres)
|
||||||
if (includeItemTypes.Length == 1
|
.DistinctNames()
|
||||||
&& (includeItemTypes[0] == BaseItemKind.BoxSet
|
.Order()
|
||||||
|| includeItemTypes[0] == BaseItemKind.Playlist
|
.ToArray(),
|
||||||
|| includeItemTypes[0] == BaseItemKind.Trailer
|
|
||||||
|| includeItemTypes[0] == BaseItemKind.Program))
|
|
||||||
{
|
|
||||||
parentItem = null;
|
|
||||||
}
|
|
||||||
else if (parentId.HasValue)
|
|
||||||
{
|
|
||||||
parentItem = _libraryManager.GetItemById(parentId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var filters = new QueryFilters();
|
Tags = itemList
|
||||||
var genreQuery = new InternalItemsQuery(user)
|
.SelectMany(i => i.Tags)
|
||||||
{
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
IncludeItemTypes = includeItemTypes,
|
.Order()
|
||||||
DtoOptions = new DtoOptions
|
.ToArray(),
|
||||||
{
|
|
||||||
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)
|
OfficialRatings = itemList
|
||||||
{
|
.Select(i => i.OfficialRating)
|
||||||
genreQuery.AncestorIds = parentItem is null ? Array.Empty<Guid>() : new[] { parentItem.Id };
|
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||||
}
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
else
|
.Order()
|
||||||
{
|
.ToArray()
|
||||||
genreQuery.Parent = parentItem;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeItemTypes.Length == 1
|
/// <summary>
|
||||||
&& (includeItemTypes[0] == BaseItemKind.MusicAlbum
|
/// Gets query filters.
|
||||||
|| includeItemTypes[0] == BaseItemKind.MusicVideo
|
/// </summary>
|
||||||
|| includeItemTypes[0] == BaseItemKind.MusicArtist
|
/// <param name="userId">Optional. User id.</param>
|
||||||
|| includeItemTypes[0] == BaseItemKind.Audio))
|
/// <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>
|
||||||
filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
|
/// <param name="isAiring">Optional. Is item airing.</param>
|
||||||
{
|
/// <param name="isMovie">Optional. Is item movie.</param>
|
||||||
Name = i.Item.Name,
|
/// <param name="isSports">Optional. Is item sports.</param>
|
||||||
Id = i.Item.Id
|
/// <param name="isKids">Optional. Is item kids.</param>
|
||||||
}).ToArray();
|
/// <param name="isNews">Optional. Is item news.</param>
|
||||||
}
|
/// <param name="isSeries">Optional. Is item series.</param>
|
||||||
else
|
/// <param name="recursive">Optional. Search recursive.</param>
|
||||||
{
|
/// <response code="200">Filters retrieved.</response>
|
||||||
filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
|
/// <returns>Query filters.</returns>
|
||||||
{
|
[HttpGet("Items/Filters2")]
|
||||||
Name = i.Item.Name,
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
Id = i.Item.Id
|
public ActionResult<QueryFilters> GetQueryFilters(
|
||||||
}).ToArray();
|
[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 Microsoft.AspNetCore.Mvc;
|
||||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
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>
|
/// <summary>
|
||||||
/// The genres controller.
|
/// Initializes a new instance of the <see cref="GenresController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
public class GenresController : BaseJellyfinApiController
|
/// <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;
|
_userManager = userManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="GenresController"/> class.
|
/// Gets all genres from a given item, folder, or the entire library.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</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="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="searchTerm">The search term.</param>
|
||||||
public GenresController(
|
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||||
IUserManager userManager,
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||||
IDtoService dtoService)
|
/// <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;
|
ExcludeItemTypes = excludeItemTypes,
|
||||||
_libraryManager = libraryManager;
|
IncludeItemTypes = includeItemTypes,
|
||||||
_dtoService = dtoService;
|
StartIndex = startIndex,
|
||||||
}
|
Limit = limit,
|
||||||
|
IsFavorite = isFavorite,
|
||||||
|
NameLessThan = nameLessThan,
|
||||||
|
NameStartsWith = nameStartsWith,
|
||||||
|
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||||
|
DtoOptions = dtoOptions,
|
||||||
|
SearchTerm = searchTerm,
|
||||||
|
EnableTotalRecordCount = enableTotalRecordCount,
|
||||||
|
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
if (parentId.HasValue)
|
||||||
/// 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 }
|
if (parentItem is Folder)
|
||||||
.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)
|
|
||||||
{
|
{
|
||||||
ExcludeItemTypes = excludeItemTypes,
|
query.AncestorIds = new[] { parentId.Value };
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
result = _libraryManager.GetGenres(query);
|
query.ItemIds = new[] { parentId.Value };
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
|
||||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
QueryResult<(BaseItem, ItemCounts)> result;
|
||||||
/// Gets a genre, by name.
|
if (parentItem is ICollectionFolder parentCollectionFolder
|
||||||
/// </summary>
|
&& (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
|
||||||
/// <param name="genreName">The genre name.</param>
|
|| string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
|
||||||
/// <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()
|
result = _libraryManager.GetMusicGenres(query);
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
|
|
||||||
where T : BaseItem, new()
|
|
||||||
{
|
{
|
||||||
var result = libraryManager.GetItemList(new InternalItemsQuery
|
result = _libraryManager.GetGenres(query);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The hls segment controller.
|
/// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
public class HlsSegmentController : BaseJellyfinApiController
|
/// <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;
|
_fileSystem = fileSystem;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
_transcodingJobHelper = transcodingJobHelper;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
|
/// Gets the specified audio segment for an audio item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <param name="segmentId">The segment id.</param>
|
||||||
/// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param>
|
/// <response code="200">Hls audio segment returned.</response>
|
||||||
public HlsSegmentController(
|
/// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
|
||||||
IFileSystem fileSystem,
|
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
// [Authenticated]
|
||||||
TranscodingJobHelper transcodingJobHelper)
|
[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;
|
return BadRequest("Invalid segment.");
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
_transcodingJobHelper = transcodingJobHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file));
|
||||||
/// Gets the specified audio segment for an audio item.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">The item id.</param>
|
/// <summary>
|
||||||
/// <param name="segmentId">The segment id.</param>
|
/// Gets a hls video playlist.
|
||||||
/// <response code="200">Hls audio segment returned.</response>
|
/// </summary>
|
||||||
/// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
|
/// <param name="itemId">The video id.</param>
|
||||||
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
/// <param name="playlistId">The playlist id.</param>
|
||||||
// [Authenticated]
|
/// <response code="200">Hls video playlist returned.</response>
|
||||||
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
|
/// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
|
||||||
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
|
[HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesAudioFile]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
[ProducesPlaylistFile]
|
||||||
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
[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
|
return BadRequest("Invalid segment.");
|
||||||
var file = segmentId + Path.GetExtension(Request.Path);
|
}
|
||||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
|
||||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
return GetFileResult(file, file);
|
||||||
var fileDir = Path.GetDirectoryName(file);
|
}
|
||||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
|
|
||||||
|
/// <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>
|
return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path));
|
||||||
/// 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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The instant mix controller.
|
/// Initializes a new instance of the <see cref="InstantMixController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||||
public class InstantMixController : BaseJellyfinApiController
|
/// <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;
|
_userManager = userManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_musicManager = musicManager;
|
||||||
private readonly IMusicManager _musicManager;
|
_libraryManager = libraryManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="InstantMixController"/> class.
|
/// Creates an instant playlist based on a given song.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="id">The item id.</param>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||||
/// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
public InstantMixController(
|
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||||
IUserManager userManager,
|
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||||
IDtoService dtoService,
|
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||||
IMusicManager musicManager,
|
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||||
ILibraryManager libraryManager)
|
/// <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;
|
list = list.GetRange(0, limit.Value);
|
||||||
_dtoService = dtoService;
|
|
||||||
_musicManager = musicManager;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
|
||||||
/// 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>
|
var result = new QueryResult<BaseItemDto>(
|
||||||
/// Creates an instant playlist based on a given album.
|
0,
|
||||||
/// </summary>
|
totalCount,
|
||||||
/// <param name="id">The item id.</param>
|
returnList);
|
||||||
/// <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>
|
return result;
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,257 +18,256 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// Item lookup controller.
|
/// Initializes a new instance of the <see cref="ItemLookupController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
public class ItemLookupController : BaseJellyfinApiController
|
/// <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;
|
_providerManager = providerManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
_fileSystem = fileSystem;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly ILogger<ItemLookupController> _logger;
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ItemLookupController"/> class.
|
/// Get the item's external id info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
/// <param name="itemId">Item id.</param>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <response code="200">External id info retrieved.</response>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <response code="404">Item not found.</response>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param>
|
/// <returns>List of external id info.</returns>
|
||||||
public ItemLookupController(
|
[HttpGet("Items/{itemId}/ExternalIdInfos")]
|
||||||
IProviderManager providerManager,
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
IFileSystem fileSystem,
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
ILibraryManager libraryManager,
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
ILogger<ItemLookupController> logger)
|
public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
if (item is null)
|
||||||
{
|
{
|
||||||
_providerManager = providerManager;
|
return NotFound();
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return Ok(_providerManager.GetExternalIdInfos(item));
|
||||||
/// Get the item's external id info.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">Item id.</param>
|
/// <summary>
|
||||||
/// <response code="200">External id info retrieved.</response>
|
/// Get movie remote search.
|
||||||
/// <response code="404">Item not found.</response>
|
/// </summary>
|
||||||
/// <returns>List of external id info.</returns>
|
/// <param name="query">Remote search query.</param>
|
||||||
[HttpGet("Items/{itemId}/ExternalIdInfos")]
|
/// <response code="200">Movie remote search executed.</response>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <returns>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||||
public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
|
/// </returns>
|
||||||
{
|
[HttpPost("Items/RemoteSearch/Movie")]
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MovieInfo> query)
|
||||||
if (item is null)
|
{
|
||||||
|
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));
|
return NoContent();
|
||||||
}
|
|
||||||
|
|
||||||
/// <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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,78 +9,77 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Item Refresh Controller.
|
/// Initializes a new instance of the <see cref="ItemRefreshController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Items")]
|
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
|
||||||
public class ItemRefreshController : BaseJellyfinApiController
|
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
||||||
|
public ItemRefreshController(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IProviderManager providerManager,
|
||||||
|
IFileSystem fileSystem)
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IProviderManager _providerManager;
|
_providerManager = providerManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
_fileSystem = fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ItemRefreshController"/> class.
|
/// Refreshes metadata for an item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="itemId">Item id.</param>
|
||||||
/// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
|
/// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param>
|
||||||
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
|
||||||
public ItemRefreshController(
|
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
|
||||||
IProviderManager providerManager,
|
/// <response code="204">Item metadata refresh queued.</response>
|
||||||
IFileSystem fileSystem)
|
/// <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;
|
return NotFound();
|
||||||
_providerManager = providerManager;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||||
/// 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);
|
MetadataRefreshMode = metadataRefreshMode,
|
||||||
if (item is null)
|
ImageRefreshMode = imageRefreshMode,
|
||||||
{
|
ReplaceAllImages = replaceAllImages,
|
||||||
return NotFound();
|
ReplaceAllMetadata = replaceAllMetadata,
|
||||||
}
|
ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh
|
||||||
|
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|
||||||
|
|| replaceAllImages
|
||||||
|
|| replaceAllMetadata,
|
||||||
|
IsAutomated = false
|
||||||
|
};
|
||||||
|
|
||||||
var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
||||||
{
|
return NoContent();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,332 +20,332 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Item update controller.
|
/// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
public class ItemUpdateController : BaseJellyfinApiController
|
/// <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;
|
_libraryManager = libraryManager;
|
||||||
private readonly IProviderManager _providerManager;
|
_providerManager = providerManager;
|
||||||
private readonly ILocalizationManager _localizationManager;
|
_localizationManager = localizationManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
_fileSystem = fileSystem;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
|
/// Updates an item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="request">The new item properties.</param>
|
||||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
/// <response code="204">Item updated.</response>
|
||||||
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
/// <response code="404">Item not found.</response>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
||||||
public ItemUpdateController(
|
[HttpPost("Items/{itemId}")]
|
||||||
IFileSystem fileSystem,
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
ILibraryManager libraryManager,
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
IProviderManager providerManager,
|
public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
|
||||||
ILocalizationManager localizationManager,
|
{
|
||||||
IServerConfigurationManager serverConfigurationManager)
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
if (item is null)
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
return NotFound();
|
||||||
_providerManager = providerManager;
|
|
||||||
_localizationManager = localizationManager;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var newLockData = request.LockData ?? false;
|
||||||
/// Updates an item.
|
var isLockedChanged = item.IsLocked != newLockData;
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">The item id.</param>
|
var series = item as Series;
|
||||||
/// <param name="request">The new item properties.</param>
|
var displayOrderChanged = series is not null && !string.Equals(
|
||||||
/// <response code="204">Item updated.</response>
|
series.DisplayOrder ?? string.Empty,
|
||||||
/// <response code="404">Item not found.</response>
|
request.DisplayOrder ?? string.Empty,
|
||||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
StringComparison.OrdinalIgnoreCase);
|
||||||
[HttpPost("Items/{itemId}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
// Do this first so that metadata savers can pull the updates from the database.
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
if (request.People is not null)
|
||||||
public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
|
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
_libraryManager.UpdatePeople(
|
||||||
if (item is null)
|
item,
|
||||||
{
|
request.People.Select(x => new PersonInfo
|
||||||
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())
|
|
||||||
{
|
{
|
||||||
child.IsLocked = newLockData;
|
Name = x.Name,
|
||||||
await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
Role = x.Role,
|
||||||
}
|
Type = x.Type
|
||||||
}
|
}).ToList());
|
||||||
|
|
||||||
if (displayOrderChanged)
|
|
||||||
{
|
|
||||||
_providerManager.QueueRefresh(
|
|
||||||
series!.Id,
|
|
||||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
|
||||||
{
|
|
||||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
|
|
||||||
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
|
|
||||||
ReplaceAllMetadata = true
|
|
||||||
},
|
|
||||||
RefreshPriority.High);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
UpdateItem(request, item);
|
||||||
/// Gets metadata editor info for an item.
|
|
||||||
/// </summary>
|
item.OnMetadataChanged();
|
||||||
/// <param name="itemId">The item id.</param>
|
|
||||||
/// <response code="200">Item metadata editor returned.</response>
|
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||||
/// <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>
|
if (isLockedChanged && item.IsFolder)
|
||||||
[HttpGet("Items/{itemId}/MetadataEditor")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
|
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
var folder = (Folder)item;
|
||||||
|
|
||||||
var info = new MetadataEditorInfo
|
foreach (var child in folder.GetRecursiveChildren())
|
||||||
{
|
{
|
||||||
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
|
child.IsLocked = newLockData;
|
||||||
ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
|
await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (displayOrderChanged)
|
||||||
/// 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);
|
_providerManager.QueueRefresh(
|
||||||
if (item is null)
|
series!.Id,
|
||||||
{
|
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||||
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
|
|
||||||
{
|
{
|
||||||
Name = path,
|
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||||
Value = contentType
|
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||||
});
|
ReplaceAllMetadata = true
|
||||||
}
|
},
|
||||||
|
RefreshPriority.High);
|
||||||
_serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
|
|
||||||
_serverConfigurationManager.SaveConfiguration();
|
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
|
||||||
item.ForcedSortName = request.ForcedSortName;
|
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;
|
if (string.IsNullOrWhiteSpace(inheritedContentType) ||
|
||||||
|
!string.IsNullOrWhiteSpace(configuredContentType))
|
||||||
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;
|
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
|
||||||
episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber;
|
info.ContentType = configuredContentType;
|
||||||
episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.Tags = request.Tags;
|
if (string.IsNullOrWhiteSpace(inheritedContentType)
|
||||||
|
|| string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||||
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);
|
info.ContentTypeOptions = info.ContentTypeOptions
|
||||||
}
|
.Where(i => string.IsNullOrWhiteSpace(i.Value)
|
||||||
}
|
|| string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
||||||
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)
|
|
||||||
.ToArray();
|
.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)
|
Name = path,
|
||||||
{
|
Value = contentType
|
||||||
hasArtists.Artists = request
|
});
|
||||||
.ArtistItems
|
}
|
||||||
.Select(i => i.Name)
|
|
||||||
.ToArray();
|
_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:
|
hasAlbumArtists.AlbumArtists = request
|
||||||
song.Album = request.Album;
|
.AlbumArtists
|
||||||
break;
|
.Select(i => i.Name)
|
||||||
case MusicVideo musicVideo:
|
.ToArray();
|
||||||
musicVideo.Album = request.Album;
|
}
|
||||||
break;
|
}
|
||||||
case Series series:
|
|
||||||
|
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);
|
series.Status = GetSeriesStatus(request);
|
||||||
|
|
||||||
@ -357,93 +357,92 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
break;
|
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The library structure controller.
|
/// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Library/VirtualFolders")]
|
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||||
public class LibraryStructureController : BaseJellyfinApiController
|
/// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
|
||||||
|
public LibraryStructureController(
|
||||||
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
ILibraryMonitor libraryMonitor)
|
||||||
{
|
{
|
||||||
private readonly IServerApplicationPaths _appPaths;
|
_appPaths = serverConfigurationManager.ApplicationPaths;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly ILibraryMonitor _libraryMonitor;
|
_libraryMonitor = libraryMonitor;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
|
/// Gets all virtual folders.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <response code="200">Virtual folders retrieved.</response>
|
||||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
/// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
|
||||||
/// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
|
[HttpGet]
|
||||||
public LibraryStructureController(
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders()
|
||||||
ILibraryManager libraryManager,
|
{
|
||||||
ILibraryMonitor libraryMonitor)
|
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;
|
libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_libraryMonitor = libraryMonitor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
|
||||||
/// Gets all virtual folders.
|
|
||||||
/// </summary>
|
return NoContent();
|
||||||
/// <response code="200">Virtual folders retrieved.</response>
|
}
|
||||||
/// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
|
|
||||||
[HttpGet]
|
/// <summary>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// Removes a virtual folder.
|
||||||
public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders()
|
/// </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>
|
if (string.IsNullOrWhiteSpace(newName))
|
||||||
/// 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();
|
throw new ArgumentNullException(nameof(newName));
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var rootFolderPath = _appPaths.DefaultUserViewsPath;
|
||||||
/// Removes a virtual folder.
|
|
||||||
/// </summary>
|
var currentPath = Path.Combine(rootFolderPath, name);
|
||||||
/// <param name="name">The name of the folder.</param>
|
var newPath = Path.Combine(rootFolderPath, newName);
|
||||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
|
||||||
/// <response code="204">Folder removed.</response>
|
if (!Directory.Exists(currentPath))
|
||||||
/// <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 NotFound("The media collection does not exist.");
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
|
||||||
/// 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 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));
|
// No need to start if scanning the library because it will handle it
|
||||||
}
|
if (refreshLibrary)
|
||||||
|
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
var tempPath = Path.Combine(
|
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||||
rootFolderPath,
|
|
||||||
Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
|
|
||||||
Directory.Move(currentPath, tempPath);
|
|
||||||
currentPath = tempPath;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
Directory.Move(currentPath, newPath);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
CollectionFolder.OnCollectionFolderChange();
|
|
||||||
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
// No need to start if scanning the library because it will handle it
|
// Need to add a delay here or directory watchers may still pick up the changes
|
||||||
if (refreshLibrary)
|
// Have to block here to allow exceptions to bubble
|
||||||
{
|
await Task.Delay(1000).ConfigureAwait(false);
|
||||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
_libraryMonitor.Start();
|
||||||
}
|
}
|
||||||
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>
|
return NoContent();
|
||||||
/// Add a media path to a library.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="mediaPathDto">The media path dto.</param>
|
/// <summary>
|
||||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
/// Add a media path to a library.
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// </summary>
|
||||||
/// <response code="204">Media path added.</response>
|
/// <param name="mediaPathDto">The media path dto.</param>
|
||||||
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
||||||
[HttpPost("Paths")]
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
/// <response code="204">Media path added.</response>
|
||||||
public ActionResult AddMediaPath(
|
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
||||||
[FromBody, Required] MediaPathDto mediaPathDto,
|
[HttpPost("Paths")]
|
||||||
[FromQuery] bool refreshLibrary = false)
|
[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."));
|
// No need to start if scanning the library because it will handle it
|
||||||
|
if (refreshLibrary)
|
||||||
_libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
// No need to start if scanning the library because it will handle it
|
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||||
if (refreshLibrary)
|
}
|
||||||
{
|
else
|
||||||
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 () =>
|
|
||||||
{
|
{
|
||||||
// No need to start if scanning the library because it will handle it
|
// Need to add a delay here or directory watchers may still pick up the changes
|
||||||
if (refreshLibrary)
|
// Have to block here to allow exceptions to bubble
|
||||||
{
|
await Task.Delay(1000).ConfigureAwait(false);
|
||||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
_libraryMonitor.Start();
|
||||||
}
|
}
|
||||||
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>
|
return NoContent();
|
||||||
/// Update library options.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The library name and options.</param>
|
/// <summary>
|
||||||
/// <response code="204">Library updated.</response>
|
/// Updates a media path.
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// </summary>
|
||||||
[HttpPost("LibraryOptions")]
|
/// <param name="mediaPathRequestDto">The name of the library and path infos.</param>
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
public ActionResult UpdateLibraryOptions(
|
/// <response code="204">Media path updated.</response>
|
||||||
[FromBody] UpdateLibraryOptionsDto request)
|
/// <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);
|
throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
|
||||||
|
|
||||||
collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
|
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Localization controller.
|
/// Initializes a new instance of the <see cref="LocalizationController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
|
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||||
public class LocalizationController : BaseJellyfinApiController
|
public LocalizationController(ILocalizationManager localization)
|
||||||
{
|
{
|
||||||
private readonly ILocalizationManager _localization;
|
_localization = localization;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="LocalizationController"/> class.
|
/// Gets known cultures.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
/// <response code="200">Known cultures returned.</response>
|
||||||
public LocalizationController(ILocalizationManager localization)
|
/// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns>
|
||||||
{
|
[HttpGet("Cultures")]
|
||||||
_localization = localization;
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
}
|
public ActionResult<IEnumerable<CultureDto>> GetCultures()
|
||||||
|
{
|
||||||
|
return Ok(_localization.GetCultures());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets known cultures.
|
/// Gets known countries.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Known cultures returned.</response>
|
/// <response code="200">Known countries returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the list of countries.</returns>
|
||||||
[HttpGet("Cultures")]
|
[HttpGet("Countries")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<IEnumerable<CultureDto>> GetCultures()
|
public ActionResult<IEnumerable<CountryInfo>> GetCountries()
|
||||||
{
|
{
|
||||||
return Ok(_localization.GetCultures());
|
return Ok(_localization.GetCountries());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets known countries.
|
/// Gets known parental ratings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Known countries returned.</response>
|
/// <response code="200">Known parental ratings returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the list of countries.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns>
|
||||||
[HttpGet("Countries")]
|
[HttpGet("ParentalRatings")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<IEnumerable<CountryInfo>> GetCountries()
|
public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings()
|
||||||
{
|
{
|
||||||
return Ok(_localization.GetCountries());
|
return Ok(_localization.GetParentalRatings());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets known parental ratings.
|
/// Gets localization options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Known parental ratings returned.</response>
|
/// <response code="200">Localization options returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the list of localization options.</returns>
|
||||||
[HttpGet("ParentalRatings")]
|
[HttpGet("Options")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings()
|
public ActionResult<IEnumerable<LocalizationOption>> GetLocalizationOptions()
|
||||||
{
|
{
|
||||||
return Ok(_localization.GetParentalRatings());
|
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.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// The media info controller.
|
/// Initializes a new instance of the <see cref="MediaInfoController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
||||||
public class MediaInfoController : BaseJellyfinApiController
|
/// <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;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
private readonly IDeviceManager _deviceManager;
|
_deviceManager = deviceManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly ILogger<MediaInfoController> _logger;
|
_logger = logger;
|
||||||
private readonly MediaInfoHelper _mediaInfoHelper;
|
_mediaInfoHelper = mediaInfoHelper;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MediaInfoController"/> class.
|
/// Gets live playback media info for an item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
/// <param name="userId">The user id.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <response code="200">Playback info returned.</response>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
|
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
|
||||||
/// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
|
[HttpGet("Items/{itemId}/PlaybackInfo")]
|
||||||
public MediaInfoController(
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
IMediaSourceManager mediaSourceManager,
|
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId)
|
||||||
IDeviceManager deviceManager,
|
{
|
||||||
ILibraryManager libraryManager,
|
return await _mediaInfoHelper.GetPlaybackInfo(
|
||||||
ILogger<MediaInfoController> logger,
|
itemId,
|
||||||
MediaInfoHelper mediaInfoHelper)
|
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;
|
var caps = _deviceManager.GetCapabilities(User.GetDeviceId());
|
||||||
_deviceManager = deviceManager;
|
if (caps is not null)
|
||||||
_libraryManager = libraryManager;
|
{
|
||||||
_logger = logger;
|
profile = caps.DeviceProfile;
|
||||||
_mediaInfoHelper = mediaInfoHelper;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// Copy params from posted body
|
||||||
/// Gets live playback media info for an item.
|
// TODO clean up when breaking API compatibility.
|
||||||
/// </summary>
|
userId ??= playbackInfoDto?.UserId;
|
||||||
/// <param name="itemId">The item id.</param>
|
maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate;
|
||||||
/// <param name="userId">The user id.</param>
|
startTimeTicks ??= playbackInfoDto?.StartTimeTicks;
|
||||||
/// <response code="200">Playback info returned.</response>
|
audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex;
|
||||||
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
|
subtitleStreamIndex ??= playbackInfoDto?.SubtitleStreamIndex;
|
||||||
[HttpGet("Items/{itemId}/PlaybackInfo")]
|
maxAudioChannels ??= playbackInfoDto?.MaxAudioChannels;
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
mediaSourceId ??= playbackInfoDto?.MediaSourceId;
|
||||||
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId)
|
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;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (profile is not null)
|
||||||
/// 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
|
// set device specific data
|
||||||
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
|
||||||
|
foreach (var mediaSource in info.MediaSources)
|
||||||
{
|
{
|
||||||
OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
|
_mediaInfoHelper.SetDeviceSpecificData(
|
||||||
UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
|
item,
|
||||||
PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
|
mediaSource,
|
||||||
MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
|
profile,
|
||||||
StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
|
User,
|
||||||
AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex,
|
maxStreamingBitrate ?? profile.MaxStreamingBitrate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex,
|
startTimeTicks ?? 0,
|
||||||
MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels,
|
mediaSourceId ?? string.Empty,
|
||||||
ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty,
|
audioStreamIndex,
|
||||||
DeviceProfile = openLiveStreamDto?.DeviceProfile,
|
subtitleStreamIndex,
|
||||||
EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true,
|
maxAudioChannels,
|
||||||
EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
|
info.PlaySessionId!,
|
||||||
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
|
userId ?? Guid.Empty,
|
||||||
};
|
enableDirectPlay.Value,
|
||||||
return await _mediaInfoHelper.OpenMediaSource(HttpContext, request).ConfigureAwait(false);
|
enableDirectStream.Value,
|
||||||
|
enableTranscoding.Value,
|
||||||
|
allowVideoStreamCopy.Value,
|
||||||
|
allowAudioStreamCopy.Value,
|
||||||
|
Request.HttpContext.GetNormalizedRemoteIp());
|
||||||
|
}
|
||||||
|
|
||||||
|
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (autoOpenLiveStream.Value)
|
||||||
/// 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);
|
var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal));
|
||||||
return NoContent();
|
|
||||||
|
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>
|
return info;
|
||||||
/// Tests the network with a request with the size of the bitrate.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="size">The bitrate. Defaults to 102400.</param>
|
/// <summary>
|
||||||
/// <response code="200">Test buffer returned.</response>
|
/// Opens a media source.
|
||||||
/// <returns>A <see cref="FileResult"/> with specified bitrate.</returns>
|
/// </summary>
|
||||||
[HttpGet("Playback/BitrateTest")]
|
/// <param name="openToken">The open token.</param>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <param name="userId">The user id.</param>
|
||||||
[ProducesFile(MediaTypeNames.Application.Octet)]
|
/// <param name="playSessionId">The play session id.</param>
|
||||||
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)
|
/// <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);
|
OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
|
||||||
try
|
UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
|
||||||
{
|
PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
|
||||||
Random.Shared.NextBytes(buffer);
|
MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
|
||||||
return File(buffer, MediaTypeNames.Application.Octet);
|
StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
|
||||||
}
|
AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex,
|
||||||
finally
|
SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex,
|
||||||
{
|
MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels,
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
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.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
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;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MoviesController"/> class.
|
/// Movies controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
public class MoviesController : BaseJellyfinApiController
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
{
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
private readonly IUserManager _userManager;
|
||||||
public MoviesController(
|
private readonly ILibraryManager _libraryManager;
|
||||||
IUserManager userManager,
|
private readonly IDtoService _dtoService;
|
||||||
ILibraryManager libraryManager,
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
IDtoService dtoService,
|
|
||||||
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;
|
IncludeItemTypes = new[]
|
||||||
_libraryManager = libraryManager;
|
{
|
||||||
_dtoService = dtoService;
|
BaseItemKind.Movie,
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
// 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>
|
var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||||
/// 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)
|
IncludeItemTypes = itemTypes.ToArray(),
|
||||||
? null
|
IsMovie = true,
|
||||||
: _userManager.GetUserById(userId.Value);
|
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
Limit = 10,
|
||||||
.AddClientFields(User);
|
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)
|
var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
|
||||||
{
|
var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator();
|
||||||
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 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 };
|
var categoryTypes = new List<IEnumerator<RecommendationDto>>
|
||||||
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>>
|
|
||||||
{
|
{
|
||||||
// Give this extra weight
|
// Give this extra weight
|
||||||
similarToRecentlyPlayed,
|
similarToRecentlyPlayed,
|
||||||
@ -146,181 +146,180 @@ namespace Jellyfin.Api.Controllers
|
|||||||
hasActorFromRecentlyPlayed
|
hasActorFromRecentlyPlayed
|
||||||
};
|
};
|
||||||
|
|
||||||
while (categories.Count < categoryLimit)
|
while (categories.Count < categoryLimit)
|
||||||
|
{
|
||||||
|
var allEmpty = true;
|
||||||
|
|
||||||
|
foreach (var category in categoryTypes)
|
||||||
{
|
{
|
||||||
var allEmpty = true;
|
if (category.MoveNext())
|
||||||
|
|
||||||
foreach (var category in categoryTypes)
|
|
||||||
{
|
{
|
||||||
if (category.MoveNext())
|
categories.Add(category.Current);
|
||||||
{
|
allEmpty = false;
|
||||||
categories.Add(category.Current);
|
|
||||||
allEmpty = false;
|
|
||||||
|
|
||||||
if (categories.Count >= categoryLimit)
|
if (categories.Count >= categoryLimit)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allEmpty)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable());
|
if (allEmpty)
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
break;
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
|
return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable());
|
||||||
{
|
}
|
||||||
var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
|
|
||||||
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
|
|
||||||
{
|
|
||||||
itemTypes.Add(BaseItemKind.Trailer);
|
|
||||||
itemTypes.Add(BaseItemKind.LiveTvProgram);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var name in names)
|
private IEnumerable<RecommendationDto> GetWithDirector(
|
||||||
{
|
User? user,
|
||||||
var items = _libraryManager.GetItemList(new InternalItemsQuery(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,
|
Person = name,
|
||||||
// Account for duplicates by IMDb id, since the database doesn't support this yet
|
// Account for duplicates by IMDb id, since the database doesn't support this yet
|
||||||
Limit = itemLimit + 2,
|
Limit = itemLimit + 2,
|
||||||
|
PersonTypes = new[] { PersonType.Director },
|
||||||
IncludeItemTypes = itemTypes.ToArray(),
|
IncludeItemTypes = itemTypes.ToArray(),
|
||||||
IsMovie = true,
|
IsMovie = true,
|
||||||
EnableGroupByMetadataKey = true,
|
EnableGroupByMetadataKey = true,
|
||||||
DtoOptions = dtoOptions
|
DtoOptions = dtoOptions
|
||||||
}).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
|
}).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
|
||||||
.Take(itemLimit)
|
.Take(itemLimit)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (items.Count > 0)
|
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);
|
var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
|
||||||
itemTypes.Add(BaseItemKind.LiveTvProgram);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in baselineItems)
|
yield return new RecommendationDto
|
||||||
{
|
|
||||||
var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
|
||||||
{
|
{
|
||||||
Limit = itemLimit,
|
BaselineItemName = name,
|
||||||
IncludeItemTypes = itemTypes.ToArray(),
|
CategoryId = name.GetMD5(),
|
||||||
IsMovie = true,
|
RecommendationType = type,
|
||||||
SimilarTo = item,
|
Items = returnItems
|
||||||
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)
|
|
||||||
{
|
private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
|
||||||
var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director })
|
{
|
||||||
{
|
var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
|
||||||
MaxListOrder = 3
|
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
|
||||||
});
|
{
|
||||||
|
itemTypes.Add(BaseItemKind.Trailer);
|
||||||
var itemIds = items.Select(i => i.Id).ToList();
|
itemTypes.Add(BaseItemKind.LiveTvProgram);
|
||||||
|
}
|
||||||
return people
|
|
||||||
.Where(i => itemIds.Contains(i.ItemId))
|
foreach (var name in names)
|
||||||
.Select(i => i.Name)
|
{
|
||||||
.DistinctNames();
|
var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||||
}
|
{
|
||||||
|
Person = name,
|
||||||
private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
|
// Account for duplicates by IMDb id, since the database doesn't support this yet
|
||||||
{
|
Limit = itemLimit + 2,
|
||||||
var people = _libraryManager.GetPeople(new InternalPeopleQuery(
|
IncludeItemTypes = itemTypes.ToArray(),
|
||||||
new[] { PersonType.Director },
|
IsMovie = true,
|
||||||
Array.Empty<string>()));
|
EnableGroupByMetadataKey = true,
|
||||||
|
DtoOptions = dtoOptions
|
||||||
var itemIds = items.Select(i => i.Id).ToList();
|
}).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
|
||||||
|
.Take(itemLimit)
|
||||||
return people
|
.ToList();
|
||||||
.Where(i => itemIds.Contains(i.ItemId))
|
|
||||||
.Select(i => i.Name)
|
if (items.Count > 0)
|
||||||
.DistinctNames();
|
{
|
||||||
}
|
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The music genres controller.
|
/// Initializes a new instance of the <see cref="MusicGenresController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||||
public class MusicGenresController : BaseJellyfinApiController
|
/// <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;
|
_libraryManager = libraryManager;
|
||||||
private readonly IDtoService _dtoService;
|
_userManager = userManager;
|
||||||
private readonly IUserManager _userManager;
|
_dtoService = dtoService;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MusicGenresController"/> class.
|
/// Gets all music genres from a given item, folder, or the entire library.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</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="userManager">Instance of <see cref="IUserManager"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
|
/// <param name="searchTerm">The search term.</param>
|
||||||
public MusicGenresController(
|
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
IUserManager userManager,
|
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||||
IDtoService dtoService)
|
/// <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;
|
ExcludeItemTypes = excludeItemTypes,
|
||||||
_userManager = userManager;
|
IncludeItemTypes = includeItemTypes,
|
||||||
_dtoService = dtoService;
|
StartIndex = startIndex,
|
||||||
}
|
Limit = limit,
|
||||||
|
IsFavorite = isFavorite,
|
||||||
|
NameLessThan = nameLessThan,
|
||||||
|
NameStartsWith = nameStartsWith,
|
||||||
|
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||||
|
DtoOptions = dtoOptions,
|
||||||
|
SearchTerm = searchTerm,
|
||||||
|
EnableTotalRecordCount = enableTotalRecordCount,
|
||||||
|
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
if (parentId.HasValue)
|
||||||
/// 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 }
|
if (parentItem is Folder)
|
||||||
.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)
|
|
||||||
{
|
{
|
||||||
ExcludeItemTypes = excludeItemTypes,
|
query.AncestorIds = new[] { parentId.Value };
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
else
|
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)
|
var result = _libraryManager.GetMusicGenres(query);
|
||||||
where T : BaseItem, new()
|
|
||||||
|
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
|
item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Package Controller.
|
/// Initializes a new instance of the <see cref="PackageController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
public class PackageController : BaseJellyfinApiController
|
public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager)
|
||||||
{
|
{
|
||||||
private readonly IInstallationManager _installationManager;
|
_installationManager = installationManager;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PackageController"/> class.
|
/// Gets a package by name or assembly GUID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
|
/// <param name="name">The name of the package.</param>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <param name="assemblyGuid">The GUID of the associated assembly.</param>
|
||||||
public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager)
|
/// <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;
|
return NotFound();
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return result;
|
||||||
/// Gets a package by name or assembly GUID.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">The name of the package.</param>
|
/// <summary>
|
||||||
/// <param name="assemblyGuid">The GUID of the associated assembly.</param>
|
/// Gets available packages.
|
||||||
/// <response code="200">Package retrieved.</response>
|
/// </summary>
|
||||||
/// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
|
/// <response code="200">Available packages returned.</response>
|
||||||
[HttpGet("Packages/{name}")]
|
/// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[HttpGet("Packages")]
|
||||||
public async Task<ActionResult<PackageInfo>> GetPackageInfo(
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[FromRoute, Required] string name,
|
public async Task<IEnumerable<PackageInfo>> GetPackages()
|
||||||
[FromQuery] Guid? assemblyGuid)
|
{
|
||||||
|
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);
|
packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)))
|
||||||
var result = _installationManager.FilterPackages(
|
.ToList();
|
||||||
packages,
|
|
||||||
name,
|
|
||||||
assemblyGuid ?? default)
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
if (result is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var package = _installationManager.GetCompatibleVersions(
|
||||||
/// Gets available packages.
|
packages,
|
||||||
/// </summary>
|
name,
|
||||||
/// <response code="200">Available packages returned.</response>
|
assemblyGuid ?? Guid.Empty,
|
||||||
/// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
|
specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version))
|
||||||
[HttpGet("Packages")]
|
.FirstOrDefault();
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public async Task<IEnumerable<PackageInfo>> GetPackages()
|
|
||||||
{
|
|
||||||
IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
|
||||||
|
|
||||||
return packages;
|
if (package is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
await _installationManager.InstallPackage(package).ConfigureAwait(false);
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
var package = _installationManager.GetCompatibleVersions(
|
return NoContent();
|
||||||
packages,
|
}
|
||||||
name,
|
|
||||||
assemblyGuid ?? Guid.Empty,
|
|
||||||
specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version))
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
if (package is null)
|
/// <summary>
|
||||||
{
|
/// Cancels a package installation.
|
||||||
return NotFound();
|
/// </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>
|
||||||
}
|
/// Sets the enabled and existing package repositories.
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <param name="repositoryInfos">The list of package repositories.</param>
|
||||||
/// Cancels a package installation.
|
/// <response code="204">Package repositories saved.</response>
|
||||||
/// </summary>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
/// <param name="packageId">Installation Id.</param>
|
[HttpPost("Repositories")]
|
||||||
/// <response code="204">Installation cancelled.</response>
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
/// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[HttpDelete("Packages/Installing/{packageId}")]
|
public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos)
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
{
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
|
||||||
public ActionResult CancelPackageInstallation(
|
_serverConfigurationManager.SaveConfiguration();
|
||||||
[FromRoute, Required] Guid packageId)
|
return NoContent();
|
||||||
{
|
|
||||||
_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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,125 +15,124 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Persons controller.
|
/// Initializes a new instance of the <see cref="PersonsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
public class PersonsController : BaseJellyfinApiController
|
/// <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;
|
_libraryManager = libraryManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PersonsController"/> class.
|
/// Gets all persons.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="searchTerm">The search term.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
public PersonsController(
|
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param>
|
||||||
IDtoService dtoService,
|
/// <param name="enableUserData">Optional, include user data.</param>
|
||||||
IUserManager userManager)
|
/// <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;
|
NameContains = searchTerm,
|
||||||
_dtoService = dtoService;
|
User = user,
|
||||||
_userManager = userManager;
|
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>
|
if (userId.HasValue && !userId.Value.Equals(default))
|
||||||
/// 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 }
|
var user = _userManager.GetUserById(userId.Value);
|
||||||
.AddClientFields(User)
|
return _dtoService.GetBaseItemDto(item, dtoOptions, 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,202 +20,201 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
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>
|
/// <summary>
|
||||||
/// Playlists controller.
|
/// Initializes a new instance of the <see cref="PlaylistsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||||
public class PlaylistsController : BaseJellyfinApiController
|
/// <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;
|
_dtoService = dtoService;
|
||||||
private readonly IDtoService _dtoService;
|
_playlistManager = playlistManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PlaylistsController"/> class.
|
/// Creates a new playlist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <remarks>
|
||||||
/// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
|
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// Query parameters are obsolete.
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// </remarks>
|
||||||
public PlaylistsController(
|
/// <param name="name">The playlist name.</param>
|
||||||
IDtoService dtoService,
|
/// <param name="ids">The item ids.</param>
|
||||||
IPlaylistManager playlistManager,
|
/// <param name="userId">The user id.</param>
|
||||||
IUserManager userManager,
|
/// <param name="mediaType">The media type.</param>
|
||||||
ILibraryManager libraryManager)
|
/// <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;
|
ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
|
||||||
_playlistManager = playlistManager;
|
|
||||||
_userManager = userManager;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
|
||||||
/// 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)
|
Name = name ?? createPlaylistRequest?.Name,
|
||||||
{
|
ItemIdList = ids,
|
||||||
ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
|
UserId = userId ?? createPlaylistRequest?.UserId ?? default,
|
||||||
}
|
MediaType = mediaType ?? createPlaylistRequest?.MediaType
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
|
return result;
|
||||||
{
|
}
|
||||||
Name = name ?? createPlaylistRequest?.Name,
|
|
||||||
ItemIdList = ids,
|
|
||||||
UserId = userId ?? createPlaylistRequest?.UserId ?? default,
|
|
||||||
MediaType = mediaType ?? createPlaylistRequest?.MediaType
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
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>
|
var user = userId.Equals(default)
|
||||||
/// Adds items to a playlist.
|
? null
|
||||||
/// </summary>
|
: _userManager.GetUserById(userId);
|
||||||
/// <param name="playlistId">The playlist id.</param>
|
|
||||||
/// <param name="ids">Item id, comma delimited.</param>
|
var items = playlist.GetManageableItems().ToArray();
|
||||||
/// <param name="userId">The userId.</param>
|
|
||||||
/// <response code="204">Items added to playlist.</response>
|
var count = items.Length;
|
||||||
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
|
|
||||||
[HttpPost("{playlistId}/Items")]
|
if (startIndex.HasValue)
|
||||||
[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);
|
items = items.Skip(startIndex.Value).ToArray();
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (limit.HasValue)
|
||||||
/// 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);
|
items = items.Take(limit.Value).ToArray();
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
/// Removes items from a playlist.
|
.AddClientFields(User)
|
||||||
/// </summary>
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
/// <param name="playlistId">The playlist id.</param>
|
|
||||||
/// <param name="entryIds">The item ids, comma delimited.</param>
|
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
|
||||||
/// <response code="204">Items removed.</response>
|
|
||||||
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
|
for (int index = 0; index < dtos.Count; index++)
|
||||||
[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);
|
dtos[index].PlaylistItemId = items[index].Item1.Id;
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var result = new QueryResult<BaseItemDto>(
|
||||||
/// Gets the original items of a playlist.
|
startIndex,
|
||||||
/// </summary>
|
count,
|
||||||
/// <param name="playlistId">The playlist id.</param>
|
dtos);
|
||||||
/// <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 user = userId.Equals(default)
|
return result;
|
||||||
? 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,366 +17,365 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// Playstate controller.
|
/// Initializes a new instance of the <see cref="PlaystateController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
||||||
public class PlaystateController : BaseJellyfinApiController
|
/// <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;
|
_userManager = userManager;
|
||||||
private readonly IUserDataManager _userDataRepository;
|
_userDataRepository = userDataRepository;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly ISessionManager _sessionManager;
|
_sessionManager = sessionManager;
|
||||||
private readonly ILogger<PlaystateController> _logger;
|
_logger = loggerFactory.CreateLogger<PlaystateController>();
|
||||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
|
||||||
|
|
||||||
/// <summary>
|
_transcodingJobHelper = transcodingJobHelper;
|
||||||
/// Initializes a new instance of the <see cref="PlaystateController"/> class.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <summary>
|
||||||
/// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
/// Marks an item as played for user.
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// </summary>
|
||||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
/// <param name="userId">User id.</param>
|
||||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
/// <param name="itemId">Item id.</param>
|
||||||
/// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param>
|
/// <param name="datePlayed">Optional. The date the item was played.</param>
|
||||||
public PlaystateController(
|
/// <response code="200">Item marked as played.</response>
|
||||||
IUserManager userManager,
|
/// <response code="404">Item not found.</response>
|
||||||
IUserDataManager userDataRepository,
|
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns>
|
||||||
ILibraryManager libraryManager,
|
[HttpPost("Users/{userId}/PlayedItems/{itemId}")]
|
||||||
ISessionManager sessionManager,
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
ILoggerFactory loggerFactory,
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
TranscodingJobHelper transcodingJobHelper)
|
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;
|
return NotFound();
|
||||||
_userDataRepository = userDataRepository;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_sessionManager = sessionManager;
|
|
||||||
_logger = loggerFactory.CreateLogger<PlaystateController>();
|
|
||||||
|
|
||||||
_transcodingJobHelper = transcodingJobHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var dto = UpdatePlayedStatus(user, item, true, datePlayed);
|
||||||
/// Marks an item as played for user.
|
foreach (var additionalUserInfo in session.AdditionalUsers)
|
||||||
/// </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 additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
|
||||||
var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
UpdatePlayedStatus(additionalUser, item, true, datePlayed);
|
||||||
|
}
|
||||||
|
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
return dto;
|
||||||
if (item is null)
|
}
|
||||||
|
|
||||||
|
/// <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>
|
return method;
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,250 +17,249 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Plugins controller.
|
/// Initializes a new instance of the <see cref="PluginsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
|
||||||
public class PluginsController : BaseJellyfinApiController
|
/// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
|
||||||
|
public PluginsController(
|
||||||
|
IInstallationManager installationManager,
|
||||||
|
IPluginManager pluginManager)
|
||||||
{
|
{
|
||||||
private readonly IInstallationManager _installationManager;
|
_installationManager = installationManager;
|
||||||
private readonly IPluginManager _pluginManager;
|
_pluginManager = pluginManager;
|
||||||
private readonly JsonSerializerOptions _serializerOptions;
|
_serializerOptions = JsonDefaults.Options;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PluginsController"/> class.
|
/// Gets a list of currently installed plugins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
|
/// <response code="200">Installed plugins returned.</response>
|
||||||
/// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
|
/// <returns>List of currently installed plugins.</returns>
|
||||||
public PluginsController(
|
[HttpGet]
|
||||||
IInstallationManager installationManager,
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
IPluginManager pluginManager)
|
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;
|
return NotFound();
|
||||||
_pluginManager = pluginManager;
|
|
||||||
_serializerOptions = JsonDefaults.Options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
_pluginManager.EnablePlugin(plugin);
|
||||||
/// Gets a list of currently installed plugins.
|
return NoContent();
|
||||||
/// </summary>
|
}
|
||||||
/// <response code="200">Installed plugins returned.</response>
|
|
||||||
/// <returns>List of currently installed plugins.</returns>
|
/// <summary>
|
||||||
[HttpGet]
|
/// Disable a plugin.
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// </summary>
|
||||||
public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
|
/// <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
|
return NotFound();
|
||||||
.OrderBy(p => p.Name)
|
|
||||||
.Select(p => p.GetPluginInfo()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
_pluginManager.DisablePlugin(plugin);
|
||||||
/// Enables a disabled plugin.
|
return NoContent();
|
||||||
/// </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.EnablePlugin(plugin);
|
/// <summary>
|
||||||
return NoContent();
|
/// 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>
|
_installationManager.UninstallPlugin(plugin);
|
||||||
/// Disable a plugin.
|
return NoContent();
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="pluginId">Plugin id.</param>
|
|
||||||
/// <param name="version">Plugin version.</param>
|
/// <summary>
|
||||||
/// <response code="204">Plugin disabled.</response>
|
/// Uninstalls a plugin.
|
||||||
/// <response code="404">Plugin not found.</response>
|
/// </summary>
|
||||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
|
/// <param name="pluginId">Plugin id.</param>
|
||||||
[HttpPost("{pluginId}/{version}/Disable")]
|
/// <response code="204">Plugin uninstalled.</response>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <response code="404">Plugin not found.</response>
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[HttpDelete("{pluginId}")]
|
||||||
public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
|
[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);
|
_installationManager.UninstallPlugin(plugin);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return NotFound();
|
||||||
/// Uninstalls a plugin.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="pluginId">Plugin id.</param>
|
/// <summary>
|
||||||
/// <response code="204">Plugin uninstalled.</response>
|
/// Gets plugin configuration.
|
||||||
/// <response code="404">Plugin not found.</response>
|
/// </summary>
|
||||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
|
/// <param name="pluginId">Plugin id.</param>
|
||||||
[HttpDelete("{pluginId}")]
|
/// <response code="200">Plugin configuration returned.</response>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <response code="404">Plugin not found or plugin configuration not found.</response>
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
/// <returns>Plugin configuration.</returns>
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[HttpGet("{pluginId}/Configuration")]
|
||||||
[Obsolete("Please use the UninstallPluginByVersion API.")]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
|
[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.
|
return configPlugin.Configuration;
|
||||||
var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList();
|
}
|
||||||
|
|
||||||
// Select the un-instanced one first.
|
return NotFound();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// <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();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
|
||||||
/// Gets plugin configuration.
|
.ConfigureAwait(false);
|
||||||
/// </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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
|
||||||
/// Updates plugin configuration.
|
if (plugin.Manifest.ImagePath is null || !System.IO.File.Exists(imagePath))
|
||||||
/// </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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Quick connect controller.
|
/// Initializes a new instance of the <see cref="QuickConnectController"/> class.
|
||||||
/// </summary>
|
/// </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;
|
_quickConnect = quickConnect;
|
||||||
private readonly IAuthorizationContext _authContext;
|
_authContext = authContext;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="QuickConnectController"/> class.
|
/// Gets the current quick connect state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
|
/// <response code="200">Quick connect state returned.</response>
|
||||||
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
/// <returns>Whether Quick Connect is enabled on the server or not.</returns>
|
||||||
public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext)
|
[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;
|
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||||
_authContext = authContext;
|
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>
|
try
|
||||||
/// 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;
|
return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
catch (AuthenticationException)
|
||||||
/// <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
|
return Unauthorized("Quick connect is disabled");
|
||||||
{
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,165 +15,164 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Remote Images Controller.
|
/// Initializes a new instance of the <see cref="RemoteImageController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||||
public class RemoteImageController : BaseJellyfinApiController
|
/// <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;
|
_providerManager = providerManager;
|
||||||
private readonly IServerApplicationPaths _applicationPaths;
|
_applicationPaths = applicationPaths;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="RemoteImageController"/> class.
|
/// Gets available remote images for an item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
/// <param name="itemId">Item Id.</param>
|
||||||
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
/// <param name="type">The image type.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||||
public RemoteImageController(
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
IProviderManager providerManager,
|
/// <param name="providerName">Optional. The image provider to use.</param>
|
||||||
IServerApplicationPaths applicationPaths,
|
/// <param name="includeAllLanguages">Optional. Include all languages.</param>
|
||||||
ILibraryManager libraryManager)
|
/// <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;
|
return NotFound();
|
||||||
_applicationPaths = applicationPaths;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var images = await _providerManager.GetAvailableRemoteImages(
|
||||||
/// Gets available remote images for an item.
|
item,
|
||||||
/// </summary>
|
new RemoteImageQuery(providerName ?? string.Empty)
|
||||||
/// <param name="itemId">Item Id.</param>
|
{
|
||||||
/// <param name="type">The image type.</param>
|
IncludeAllLanguages = includeAllLanguages,
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
IncludeDisabledProviders = true,
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
ImageType = type
|
||||||
/// <param name="providerName">Optional. The image provider to use.</param>
|
},
|
||||||
/// <param name="includeAllLanguages">Optional. Include all languages.</param>
|
CancellationToken.None)
|
||||||
/// <response code="200">Remote Images returned.</response>
|
.ConfigureAwait(false);
|
||||||
/// <response code="404">Item not found.</response>
|
|
||||||
/// <returns>Remote Image Result.</returns>
|
var imageArray = images.ToArray();
|
||||||
[HttpGet("Items/{itemId}/RemoteImages")]
|
var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
if (type.HasValue)
|
||||||
[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);
|
allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var result = new RemoteImageResult
|
||||||
/// 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);
|
TotalRecordCount = imageArray.Length,
|
||||||
if (item is null)
|
Providers = allProviders.Select(o => o.Name)
|
||||||
{
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
return NotFound();
|
.ToArray()
|
||||||
}
|
};
|
||||||
|
|
||||||
return Ok(_providerManager.GetRemoteImageProviderInfo(item));
|
if (startIndex.HasValue)
|
||||||
|
{
|
||||||
|
imageArray = imageArray.Skip(startIndex.Value).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (limit.HasValue)
|
||||||
/// 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);
|
imageArray = imageArray.Take(limit.Value).ToArray();
|
||||||
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>
|
result.Images = imageArray;
|
||||||
/// Gets the full cache path.
|
return result;
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="filename">The filename.</param>
|
|
||||||
/// <returns>System.String.</returns>
|
/// <summary>
|
||||||
private string GetFullCachePath(string filename)
|
/// 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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Scheduled Tasks Controller.
|
/// Initializes a new instance of the <see cref="ScheduledTasksController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
|
||||||
public class ScheduledTasksController : BaseJellyfinApiController
|
public ScheduledTasksController(ITaskManager taskManager)
|
||||||
{
|
{
|
||||||
private readonly ITaskManager _taskManager;
|
_taskManager = taskManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ScheduledTasksController"/> class.
|
/// Get tasks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
|
/// <param name="isHidden">Optional filter tasks that are hidden, or not.</param>
|
||||||
public ScheduledTasksController(ITaskManager taskManager)
|
/// <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;
|
if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask)
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden)
|
||||||
{
|
{
|
||||||
if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden)
|
continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yield return ScheduledTaskHelpers.GetTaskInfo(task);
|
if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled)
|
||||||
}
|
{
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
/// <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);
|
yield 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Search controller.
|
/// Initializes a new instance of the <see cref="SearchController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Search/Hints")]
|
/// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||||
public class SearchController : BaseJellyfinApiController
|
/// <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;
|
_searchEngine = searchEngine;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
private readonly IImageProcessor _imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SearchController"/> class.
|
/// Gets the search hint result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</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="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
|
/// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
|
||||||
/// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
|
/// <param name="searchTerm">The search term to filter on.</param>
|
||||||
public SearchController(
|
/// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimited.</param>
|
||||||
ISearchEngine searchEngine,
|
/// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimited.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimited.</param>
|
||||||
IDtoService dtoService,
|
/// <param name="parentId">If specified, only children of the parent are returned.</param>
|
||||||
IImageProcessor imageProcessor)
|
/// <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;
|
Limit = limit,
|
||||||
_libraryManager = libraryManager;
|
SearchTerm = searchTerm,
|
||||||
_dtoService = dtoService;
|
IncludeArtists = includeArtists,
|
||||||
_imageProcessor = imageProcessor;
|
IncludeGenres = includeGenres,
|
||||||
}
|
IncludeMedia = includeMedia,
|
||||||
|
IncludePeople = includePeople,
|
||||||
|
IncludeStudios = includeStudios,
|
||||||
|
StartIndex = startIndex,
|
||||||
|
UserId = userId ?? Guid.Empty,
|
||||||
|
IncludeItemTypes = includeItemTypes,
|
||||||
|
ExcludeItemTypes = excludeItemTypes,
|
||||||
|
MediaTypes = mediaTypes,
|
||||||
|
ParentId = parentId,
|
||||||
|
|
||||||
/// <summary>
|
IsKids = isKids,
|
||||||
/// Gets the search hint result.
|
IsMovie = isMovie,
|
||||||
/// </summary>
|
IsNews = isNews,
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
IsSeries = isSeries,
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
IsSports = isSports
|
||||||
/// <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>
|
return new SearchHintResult(result.Items.Select(GetSearchHintResult).ToArray(), result.TotalRecordCount);
|
||||||
/// <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>
|
/// <summary>
|
||||||
/// <param name="isMovie">Optional filter for movies.</param>
|
/// Gets the search hint result.
|
||||||
/// <param name="isSeries">Optional filter for series.</param>
|
/// </summary>
|
||||||
/// <param name="isNews">Optional filter for news.</param>
|
/// <param name="hintInfo">The hint info.</param>
|
||||||
/// <param name="isKids">Optional filter for kids.</param>
|
/// <returns>SearchHintResult.</returns>
|
||||||
/// <param name="isSports">Optional filter for sports.</param>
|
private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
|
||||||
/// <param name="includePeople">Optional filter whether to include people.</param>
|
{
|
||||||
/// <param name="includeMedia">Optional filter whether to include media.</param>
|
var item = hintInfo.Item;
|
||||||
/// <param name="includeGenres">Optional filter whether to include genres.</param>
|
|
||||||
/// <param name="includeStudios">Optional filter whether to include studios.</param>
|
var result = new SearchHint
|
||||||
/// <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
|
Name = item.Name,
|
||||||
{
|
IndexNumber = item.IndexNumber,
|
||||||
Limit = limit,
|
ParentIndexNumber = item.ParentIndexNumber,
|
||||||
SearchTerm = searchTerm,
|
Id = item.Id,
|
||||||
IncludeArtists = includeArtists,
|
Type = item.GetBaseItemKind(),
|
||||||
IncludeGenres = includeGenres,
|
MediaType = item.MediaType,
|
||||||
IncludeMedia = includeMedia,
|
MatchedTerm = hintInfo.MatchedTerm,
|
||||||
IncludePeople = includePeople,
|
RunTimeTicks = item.RunTimeTicks,
|
||||||
IncludeStudios = includeStudios,
|
ProductionYear = item.ProductionYear,
|
||||||
StartIndex = startIndex,
|
ChannelId = item.ChannelId,
|
||||||
UserId = userId ?? Guid.Empty,
|
EndDate = item.EndDate
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
// Kept for compatibility with older clients
|
// Kept for compatibility with older clients
|
||||||
result.ItemId = result.Id;
|
result.ItemId = result.Id;
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
if (item.IsFolder)
|
if (item.IsFolder)
|
||||||
{
|
{
|
||||||
result.IsFolder = true;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
SetThumbImageInfo(result, item);
|
||||||
{
|
SetBackdropImageInfo(result, item);
|
||||||
itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
|
|
||||||
}
|
|
||||||
|
|
||||||
itemWithImage ??= GetParentWithImage<BaseItem>(item, ImageType.Thumb);
|
switch (item)
|
||||||
|
{
|
||||||
if (itemWithImage is not null)
|
case IHasSeries hasSeries:
|
||||||
{
|
result.Series = hasSeries.SeriesName;
|
||||||
var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
|
break;
|
||||||
|
case LiveTvProgram program:
|
||||||
if (tag is not null)
|
result.StartDate = program.StartDate;
|
||||||
|
break;
|
||||||
|
case Series series:
|
||||||
|
if (series.Status.HasValue)
|
||||||
{
|
{
|
||||||
hint.ThumbImageTag = tag;
|
result.Status = series.Status.Value.ToString();
|
||||||
hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
|
break;
|
||||||
{
|
case MusicAlbum album:
|
||||||
var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
|
result.Artists = album.Artists;
|
||||||
?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
|
result.AlbumArtist = album.AlbumArtist;
|
||||||
|
break;
|
||||||
|
case Audio song:
|
||||||
|
result.AlbumArtist = song.AlbumArtists?.FirstOrDefault();
|
||||||
|
result.Artists = song.Artists;
|
||||||
|
|
||||||
if (itemWithImage is not null)
|
MusicAlbum musicAlbum = song.AlbumEntity;
|
||||||
{
|
|
||||||
var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
|
|
||||||
|
|
||||||
if (tag is not null)
|
if (musicAlbum is not null)
|
||||||
{
|
{
|
||||||
hint.BackdropImageTag = tag;
|
result.Album = musicAlbum.Name;
|
||||||
hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
|
result.AlbumId = musicAlbum.Id;
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
|
result.Album = song.Album;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
private T? GetParentWithImage<T>(BaseItem item, ImageType type)
|
if (!item.ChannelId.Equals(default))
|
||||||
where T : BaseItem
|
|
||||||
{
|
{
|
||||||
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The session controller.
|
/// Initializes a new instance of the <see cref="SessionController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
||||||
public class SessionController : BaseJellyfinApiController
|
/// <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;
|
_sessionManager = sessionManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
private readonly IDeviceManager _deviceManager;
|
_deviceManager = deviceManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SessionController"/> class.
|
/// Gets a list of sessions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
/// <param name="controllableByUserId">Filter by sessions that a given user is allowed to remote control.</param>
|
||||||
/// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
|
/// <param name="deviceId">Filter by device Id.</param>
|
||||||
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
|
/// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param>
|
||||||
public SessionController(
|
/// <response code="200">List of sessions returned.</response>
|
||||||
ISessionManager sessionManager,
|
/// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns>
|
||||||
IUserManager userManager,
|
[HttpGet("Sessions")]
|
||||||
IDeviceManager deviceManager)
|
[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;
|
result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
|
||||||
_userManager = userManager;
|
|
||||||
_deviceManager = deviceManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (controllableByUserId.HasValue && !controllableByUserId.Equals(default))
|
||||||
/// 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;
|
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 (!_deviceManager.CanAccessDevice(user, i.DeviceId))
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
return false;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
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 NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return Ok(result);
|
||||||
/// Updates capabilities for a device.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">The session id.</param>
|
/// <summary>
|
||||||
/// <param name="capabilities">The <see cref="ClientCapabilities"/>.</param>
|
/// Instructs a session to browse to an item or view.
|
||||||
/// <response code="204">Capabilities updated.</response>
|
/// </summary>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <param name="sessionId">The session Id.</param>
|
||||||
[HttpPost("Sessions/Capabilities/Full")]
|
/// <param name="itemType">The type of item to browse to.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="itemId">The Id of the item.</param>
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
/// <param name="itemName">The name of the item.</param>
|
||||||
public async Task<ActionResult> PostFullCapabilities(
|
/// <response code="204">Instruction sent to session.</response>
|
||||||
[FromQuery] string? id,
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[FromBody, Required] ClientCapabilitiesDto capabilities)
|
[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>
|
||||||
}
|
/// Issues a system command to a client.
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <param name="sessionId">The session id.</param>
|
||||||
/// Reports that a session is viewing an item.
|
/// <param name="command">The command to send.</param>
|
||||||
/// </summary>
|
/// <response code="204">System command sent to session.</response>
|
||||||
/// <param name="sessionId">The session id.</param>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
/// <param name="itemId">The item id.</param>
|
[HttpPost("Sessions/{sessionId}/System/{command}")]
|
||||||
/// <response code="204">Session reported to server.</response>
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[HttpPost("Sessions/Viewing")]
|
public async Task<ActionResult> SendSystemCommand(
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[FromRoute, Required] string sessionId,
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[FromRoute, Required] GeneralCommandType command)
|
||||||
public async Task<ActionResult> ReportViewing(
|
{
|
||||||
[FromQuery] string? sessionId,
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
[FromQuery, Required] string? itemId)
|
var generalCommand = new GeneralCommand
|
||||||
{
|
{
|
||||||
string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
Name = command,
|
||||||
|
ControllingUserId = currentSession.UserId
|
||||||
|
};
|
||||||
|
|
||||||
_sessionManager.ReportNowViewingItem(session, itemId);
|
await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None).ConfigureAwait(false);
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
return NoContent();
|
||||||
/// Reports that a session has ended.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <response code="204">Session end reported to server.</response>
|
/// <summary>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// Issues a general command to a client.
|
||||||
[HttpPost("Sessions/Logout")]
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="sessionId">The session id.</param>
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
/// <param name="command">The command to send.</param>
|
||||||
public async Task<ActionResult> ReportSessionEnded()
|
/// <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);
|
Name = command,
|
||||||
return NoContent();
|
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>
|
await _sessionManager.SendMessageCommand(
|
||||||
/// Get all auth providers.
|
await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
|
||||||
/// </summary>
|
sessionId,
|
||||||
/// <response code="200">Auth providers retrieved.</response>
|
command,
|
||||||
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns>
|
CancellationToken.None)
|
||||||
[HttpGet("Auth/Providers")]
|
.ConfigureAwait(false);
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
return NoContent();
|
||||||
public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders()
|
}
|
||||||
|
|
||||||
|
/// <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>
|
_sessionManager.ReportCapabilities(id, new ClientCapabilities
|
||||||
/// 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();
|
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The startup wizard controller.
|
/// Initializes a new instance of the <see cref="StartupController" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
/// <param name="config">The server configuration manager.</param>
|
||||||
public class StartupController : BaseJellyfinApiController
|
/// <param name="userManager">The user manager.</param>
|
||||||
|
public StartupController(IServerConfigurationManager config, IUserManager userManager)
|
||||||
{
|
{
|
||||||
private readonly IServerConfigurationManager _config;
|
_config = config;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="StartupController" /> class.
|
/// Completes the startup wizard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="config">The server configuration manager.</param>
|
/// <response code="204">Startup wizard completed.</response>
|
||||||
/// <param name="userManager">The user manager.</param>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
public StartupController(IServerConfigurationManager config, IUserManager userManager)
|
[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;
|
UICulture = _config.Configuration.UICulture,
|
||||||
_userManager = userManager;
|
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>
|
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||||
/// Completes the startup wizard.
|
|
||||||
/// </summary>
|
if (!string.IsNullOrEmpty(startupUserDto.Password))
|
||||||
/// <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;
|
await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
|
||||||
_config.SaveConfiguration();
|
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return NoContent();
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,141 +16,140 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Studios controller.
|
/// Initializes a new instance of the <see cref="StudiosController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
public class StudiosController : BaseJellyfinApiController
|
/// <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;
|
_libraryManager = libraryManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="StudiosController"/> class.
|
/// Gets all studios from a given item, folder, or the entire library.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</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="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="searchTerm">Optional. Search term.</param>
|
||||||
public StudiosController(
|
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
IUserManager userManager,
|
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||||
IDtoService dtoService)
|
/// <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;
|
ExcludeItemTypes = excludeItemTypes,
|
||||||
_userManager = userManager;
|
IncludeItemTypes = includeItemTypes,
|
||||||
_dtoService = dtoService;
|
StartIndex = startIndex,
|
||||||
}
|
Limit = limit,
|
||||||
|
IsFavorite = isFavorite,
|
||||||
|
NameLessThan = nameLessThan,
|
||||||
|
NameStartsWith = nameStartsWith,
|
||||||
|
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||||
|
DtoOptions = dtoOptions,
|
||||||
|
SearchTerm = searchTerm,
|
||||||
|
EnableTotalRecordCount = enableTotalRecordCount
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
if (parentId.HasValue)
|
||||||
/// 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 }
|
if (parentItem is Folder)
|
||||||
.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)
|
|
||||||
{
|
{
|
||||||
ExcludeItemTypes = excludeItemTypes,
|
query.AncestorIds = new[] { parentId.Value };
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
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);
|
query.ItemIds = new[] { parentId.Value };
|
||||||
|
|
||||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// Subtitle controller.
|
/// Initializes a new instance of the <see cref="SubtitleController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
public class SubtitleController : BaseJellyfinApiController
|
/// <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;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly ISubtitleManager _subtitleManager;
|
_subtitleManager = subtitleManager;
|
||||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
_subtitleEncoder = subtitleEncoder;
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
private readonly IProviderManager _providerManager;
|
_providerManager = providerManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
_fileSystem = fileSystem;
|
||||||
private readonly ILogger<SubtitleController> _logger;
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SubtitleController"/> class.
|
/// Deletes an external subtitle file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="index">The index of the subtitle file.</param>
|
||||||
/// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
|
/// <response code="204">Subtitle deleted.</response>
|
||||||
/// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
|
/// <response code="404">Item not found.</response>
|
||||||
/// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
/// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
|
[HttpDelete("Videos/{itemId}/Subtitles/{index}")]
|
||||||
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
/// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public SubtitleController(
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
public ActionResult<Task> DeleteSubtitle(
|
||||||
ILibraryManager libraryManager,
|
[FromRoute, Required] Guid itemId,
|
||||||
ISubtitleManager subtitleManager,
|
[FromRoute, Required] int index)
|
||||||
ISubtitleEncoder subtitleEncoder,
|
{
|
||||||
IMediaSourceManager mediaSourceManager,
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
IProviderManager providerManager,
|
|
||||||
IFileSystem fileSystem,
|
if (item is null)
|
||||||
ILogger<SubtitleController> logger)
|
|
||||||
{
|
{
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
return NotFound();
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_subtitleManager = subtitleManager;
|
|
||||||
_subtitleEncoder = subtitleEncoder;
|
|
||||||
_mediaSourceManager = mediaSourceManager;
|
|
||||||
_providerManager = providerManager;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
_subtitleManager.DeleteSubtitles(item, index);
|
||||||
/// Deletes an external subtitle file.
|
return NoContent();
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="itemId">The item id.</param>
|
|
||||||
/// <param name="index">The index of the subtitle file.</param>
|
/// <summary>
|
||||||
/// <response code="204">Subtitle deleted.</response>
|
/// Search remote subtitles.
|
||||||
/// <response code="404">Item not found.</response>
|
/// </summary>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <param name="itemId">The item id.</param>
|
||||||
[HttpDelete("Videos/{itemId}/Subtitles/{index}")]
|
/// <param name="language">The language of the subtitles.</param>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
/// <response code="200">Subtitles retrieved.</response>
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
|
||||||
public ActionResult<Task> DeleteSubtitle(
|
[HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
|
||||||
[FromRoute, Required] Guid itemId,
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[FromRoute, Required] int index)
|
[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)
|
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||||
{
|
}
|
||||||
return NotFound();
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
|
_logger.LogError(ex, "Error downloading subtitles");
|
||||||
_subtitleManager.DeleteSubtitles(item, index);
|
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return NoContent();
|
||||||
/// 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>
|
||||||
|
/// 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>
|
if (string.IsNullOrEmpty(format))
|
||||||
/// 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);
|
var item = (Video)_libraryManager.GetItemById(itemId.Value);
|
||||||
|
|
||||||
try
|
var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture);
|
||||||
{
|
var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
|
||||||
await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
|
.First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
var subtitleStream = mediaSource.MediaStreams
|
||||||
}
|
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error downloading subtitles");
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoContent();
|
return PhysicalFile(subtitleStream.Path, MimeTypes.GetMimeType(subtitleStream.Path));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
|
||||||
/// 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);
|
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>
|
return File(
|
||||||
/// Gets subtitles in a specified format.
|
await EncodeSubtitles(
|
||||||
/// </summary>
|
itemId.Value,
|
||||||
/// <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,
|
|
||||||
mediaSourceId,
|
mediaSourceId,
|
||||||
index,
|
index.Value,
|
||||||
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,
|
|
||||||
format,
|
format,
|
||||||
startPositionTicks,
|
startPositionTicks,
|
||||||
endPositionTicks ?? 0,
|
endPositionTicks,
|
||||||
copyTimestamps,
|
copyTimestamps).ConfigureAwait(false),
|
||||||
CancellationToken.None);
|
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>
|
var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks;
|
||||||
/// Gets a list of available fallback font files.
|
if (segmentLengthTicks <= 0)
|
||||||
/// </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();
|
throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
|
||||||
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>
|
var builder = new StringBuilder();
|
||||||
/// Gets a fallback font file.
|
builder.AppendLine("#EXTM3U")
|
||||||
/// </summary>
|
.Append("#EXT-X-TARGETDURATION:")
|
||||||
/// <param name="name">The name of the fallback font file to get.</param>
|
.Append(segmentLength)
|
||||||
/// <response code="200">Fallback font file retrieved.</response>
|
.AppendLine()
|
||||||
/// <returns>The fallback font file.</returns>
|
.AppendLine("#EXT-X-VERSION:3")
|
||||||
[HttpGet("FallbackFont/Fonts/{name}")]
|
.AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesFile("font/*")]
|
long positionTicks = 0;
|
||||||
public ActionResult GetFallbackFont([FromRoute, Required] string name)
|
|
||||||
|
var accessToken = User.GetToken();
|
||||||
|
|
||||||
|
while (positionTicks < runtime)
|
||||||
{
|
{
|
||||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
var remaining = runtime - positionTicks;
|
||||||
var fallbackFontPath = encodingOptions.FallbackFontPath;
|
var lengthTicks = Math.Min(remaining, segmentLengthTicks);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(fallbackFontPath))
|
builder.Append("#EXTINF:")
|
||||||
{
|
.Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds)
|
||||||
var fontFile = _fileSystem.GetFiles(fallbackFontPath)
|
.Append(',')
|
||||||
.First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
|
.AppendLine();
|
||||||
var fileSize = fontFile?.Length;
|
|
||||||
|
|
||||||
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);
|
Format = body.Format,
|
||||||
return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
|
Language = body.Language,
|
||||||
}
|
IsForced = body.IsForced,
|
||||||
else
|
Stream = memoryStream
|
||||||
{
|
}).ConfigureAwait(false);
|
||||||
_logger.LogWarning("The selected font is null or empty");
|
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning("The path of fallback font folder has not been set");
|
|
||||||
encodingOptions.EnableFallbackFont = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// returning HTTP 204 will break the SubtitlesOctopus
|
return NoContent();
|
||||||
return Ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The suggestions controller.
|
/// Initializes a new instance of the <see cref="SuggestionsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
public class SuggestionsController : BaseJellyfinApiController
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
public SuggestionsController(
|
||||||
|
IDtoService dtoService,
|
||||||
|
IUserManager userManager,
|
||||||
|
ILibraryManager libraryManager)
|
||||||
{
|
{
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SuggestionsController"/> class.
|
/// Gets suggestions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="userId">The user id.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="mediaType">The media types.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="type">The type.</param>
|
||||||
public SuggestionsController(
|
/// <param name="startIndex">Optional. The start index.</param>
|
||||||
IDtoService dtoService,
|
/// <param name="limit">Optional. The limit.</param>
|
||||||
IUserManager userManager,
|
/// <param name="enableTotalRecordCount">Whether to enable the total record count.</param>
|
||||||
ILibraryManager libraryManager)
|
/// <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;
|
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
|
||||||
_userManager = userManager;
|
MediaTypes = mediaType,
|
||||||
_libraryManager = libraryManager;
|
IncludeItemTypes = type,
|
||||||
}
|
IsVirtualItem = false,
|
||||||
|
StartIndex = startIndex,
|
||||||
|
Limit = limit,
|
||||||
|
DtoOptions = dtoOptions,
|
||||||
|
EnableTotalRecordCount = enableTotalRecordCount,
|
||||||
|
Recursive = true
|
||||||
|
});
|
||||||
|
|
||||||
/// <summary>
|
var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
|
||||||
/// 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);
|
return new QueryResult<BaseItemDto>(
|
||||||
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
|
startIndex,
|
||||||
{
|
result.TotalRecordCount,
|
||||||
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
|
dtoList);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,409 +16,408 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The sync play controller.
|
/// Initializes a new instance of the <see cref="SyncPlayController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.SyncPlayHasAccess)]
|
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||||
public class SyncPlayController : BaseJellyfinApiController
|
/// <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;
|
_sessionManager = sessionManager;
|
||||||
private readonly ISyncPlayManager _syncPlayManager;
|
_syncPlayManager = syncPlayManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SyncPlayController"/> class.
|
/// Create a new SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
/// <param name="requestData">The settings of the new group.</param>
|
||||||
/// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
|
/// <response code="204">New group created.</response>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
public SyncPlayController(
|
[HttpPost("New")]
|
||||||
ISessionManager sessionManager,
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
ISyncPlayManager syncPlayManager,
|
[Authorize(Policy = Policies.SyncPlayCreateGroup)]
|
||||||
IUserManager userManager)
|
public async Task<ActionResult> SyncPlayCreateGroup(
|
||||||
{
|
[FromBody, Required] NewGroupRequestDto requestData)
|
||||||
_sessionManager = sessionManager;
|
{
|
||||||
_syncPlayManager = syncPlayManager;
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
_userManager = userManager;
|
var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
|
||||||
}
|
_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new SyncPlay group.
|
/// Join an existing SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The settings of the new group.</param>
|
/// <param name="requestData">The group to join.</param>
|
||||||
/// <response code="204">New group created.</response>
|
/// <response code="204">Group join successful.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("New")]
|
[HttpPost("Join")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayCreateGroup)]
|
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
||||||
public async Task<ActionResult> SyncPlayCreateGroup(
|
public async Task<ActionResult> SyncPlayJoinGroup(
|
||||||
[FromBody, Required] NewGroupRequestDto requestData)
|
[FromBody, Required] JoinGroupRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
|
var syncPlayRequest = new JoinGroupRequest(requestData.GroupId);
|
||||||
_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Join an existing SyncPlay group.
|
/// Leave the joined SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The group to join.</param>
|
/// <response code="204">Group leave successful.</response>
|
||||||
/// <response code="204">Group join successful.</response>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
[HttpPost("Leave")]
|
||||||
[HttpPost("Join")]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
public async Task<ActionResult> SyncPlayLeaveGroup()
|
||||||
public async Task<ActionResult> SyncPlayJoinGroup(
|
{
|
||||||
[FromBody, Required] JoinGroupRequestDto requestData)
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
{
|
var syncPlayRequest = new LeaveGroupRequest();
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
_syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
var syncPlayRequest = new JoinGroupRequest(requestData.GroupId);
|
return NoContent();
|
||||||
_syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None);
|
}
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Leave the joined SyncPlay group.
|
/// Gets all SyncPlay groups.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="204">Group leave successful.</response>
|
/// <response code="200">Groups returned.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
|
||||||
[HttpPost("Leave")]
|
[HttpGet("List")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
||||||
public async Task<ActionResult> SyncPlayLeaveGroup()
|
public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups()
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new LeaveGroupRequest();
|
var syncPlayRequest = new ListGroupsRequest();
|
||||||
_syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None);
|
return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable());
|
||||||
return NoContent();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all SyncPlay groups.
|
/// Request to set new playlist in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Groups returned.</response>
|
/// <param name="requestData">The new playlist to play in the group.</param>
|
||||||
/// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
|
/// <response code="204">Queue update sent to all group members.</response>
|
||||||
[HttpGet("List")]
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[HttpPost("SetNewQueue")]
|
||||||
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups()
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
{
|
public async Task<ActionResult> SyncPlaySetNewQueue(
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
[FromBody, Required] PlayRequestDto requestData)
|
||||||
var syncPlayRequest = new ListGroupsRequest();
|
{
|
||||||
return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable());
|
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>
|
/// <summary>
|
||||||
/// Request to set new playlist in SyncPlay group.
|
/// Request to change playlist item in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The new playlist to play in the group.</param>
|
/// <param name="requestData">The new item to play.</param>
|
||||||
/// <response code="204">Queue update sent to all group members.</response>
|
/// <response code="204">Queue update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetNewQueue")]
|
[HttpPost("SetPlaylistItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlaySetNewQueue(
|
public async Task<ActionResult> SyncPlaySetPlaylistItem(
|
||||||
[FromBody, Required] PlayRequestDto requestData)
|
[FromBody, Required] SetPlaylistItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new PlayGroupRequest(
|
var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId);
|
||||||
requestData.PlayingQueue,
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
requestData.PlayingItemPosition,
|
return NoContent();
|
||||||
requestData.StartPositionTicks);
|
}
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request to change playlist item in SyncPlay group.
|
/// Request to remove items from the playlist in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The new item to play.</param>
|
/// <param name="requestData">The items to remove.</param>
|
||||||
/// <response code="204">Queue update sent to all group members.</response>
|
/// <response code="204">Queue update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetPlaylistItem")]
|
[HttpPost("RemoveFromPlaylist")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlaySetPlaylistItem(
|
public async Task<ActionResult> SyncPlayRemoveFromPlaylist(
|
||||||
[FromBody, Required] SetPlaylistItemRequestDto requestData)
|
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId);
|
var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem);
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request to remove items from the playlist in SyncPlay group.
|
/// Request to move an item in the playlist in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The items to remove.</param>
|
/// <param name="requestData">The new position for the item.</param>
|
||||||
/// <response code="204">Queue update sent to all group members.</response>
|
/// <response code="204">Queue update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("RemoveFromPlaylist")]
|
[HttpPost("MovePlaylistItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlayRemoveFromPlaylist(
|
public async Task<ActionResult> SyncPlayMovePlaylistItem(
|
||||||
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
|
[FromBody, Required] MovePlaylistItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem);
|
var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex);
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request to move an item in the playlist in SyncPlay group.
|
/// Request to queue items to the playlist of a SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The new position for the item.</param>
|
/// <param name="requestData">The items to add.</param>
|
||||||
/// <response code="204">Queue update sent to all group members.</response>
|
/// <response code="204">Queue update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("MovePlaylistItem")]
|
[HttpPost("Queue")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlayMovePlaylistItem(
|
public async Task<ActionResult> SyncPlayQueue(
|
||||||
[FromBody, Required] MovePlaylistItemRequestDto requestData)
|
[FromBody, Required] QueueRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex);
|
var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode);
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request to queue items to the playlist of a SyncPlay group.
|
/// Request unpause in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The items to add.</param>
|
/// <response code="204">Unpause update sent to all group members.</response>
|
||||||
/// <response code="204">Queue update sent to all group members.</response>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
[HttpPost("Unpause")]
|
||||||
[HttpPost("Queue")]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
public async Task<ActionResult> SyncPlayUnpause()
|
||||||
public async Task<ActionResult> SyncPlayQueue(
|
{
|
||||||
[FromBody, Required] QueueRequestDto requestData)
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
{
|
var syncPlayRequest = new UnpauseGroupRequest();
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode);
|
return NoContent();
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
}
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request unpause in SyncPlay group.
|
/// Request pause in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="204">Unpause update sent to all group members.</response>
|
/// <response code="204">Pause update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Unpause")]
|
[HttpPost("Pause")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlayUnpause()
|
public async Task<ActionResult> SyncPlayPause()
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new UnpauseGroupRequest();
|
var syncPlayRequest = new PauseGroupRequest();
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request pause in SyncPlay group.
|
/// Request stop in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="204">Pause update sent to all group members.</response>
|
/// <response code="204">Stop update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Pause")]
|
[HttpPost("Stop")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlayPause()
|
public async Task<ActionResult> SyncPlayStop()
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new PauseGroupRequest();
|
var syncPlayRequest = new StopGroupRequest();
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request stop in SyncPlay group.
|
/// Request seek in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="204">Stop update sent to all group members.</response>
|
/// <param name="requestData">The new playback position.</param>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <response code="204">Seek update sent to all group members.</response>
|
||||||
[HttpPost("Stop")]
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[HttpPost("Seek")]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> SyncPlayStop()
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
{
|
public async Task<ActionResult> SyncPlaySeek(
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
[FromBody, Required] SeekRequestDto requestData)
|
||||||
var syncPlayRequest = new StopGroupRequest();
|
{
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
return NoContent();
|
var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks);
|
||||||
}
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request seek in SyncPlay group.
|
/// Notify SyncPlay group that member is buffering.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The new playback position.</param>
|
/// <param name="requestData">The player status.</param>
|
||||||
/// <response code="204">Seek update sent to all group members.</response>
|
/// <response code="204">Group state update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Seek")]
|
[HttpPost("Buffering")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlaySeek(
|
public async Task<ActionResult> SyncPlayBuffering(
|
||||||
[FromBody, Required] SeekRequestDto requestData)
|
[FromBody, Required] BufferRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks);
|
var syncPlayRequest = new BufferGroupRequest(
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
requestData.When,
|
||||||
return NoContent();
|
requestData.PositionTicks,
|
||||||
}
|
requestData.IsPlaying,
|
||||||
|
requestData.PlaylistItemId);
|
||||||
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Notify SyncPlay group that member is buffering.
|
/// Notify SyncPlay group that member is ready for playback.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The player status.</param>
|
/// <param name="requestData">The player status.</param>
|
||||||
/// <response code="204">Group state update sent to all group members.</response>
|
/// <response code="204">Group state update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Buffering")]
|
[HttpPost("Ready")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlayBuffering(
|
public async Task<ActionResult> SyncPlayReady(
|
||||||
[FromBody, Required] BufferRequestDto requestData)
|
[FromBody, Required] ReadyRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new BufferGroupRequest(
|
var syncPlayRequest = new ReadyGroupRequest(
|
||||||
requestData.When,
|
requestData.When,
|
||||||
requestData.PositionTicks,
|
requestData.PositionTicks,
|
||||||
requestData.IsPlaying,
|
requestData.IsPlaying,
|
||||||
requestData.PlaylistItemId);
|
requestData.PlaylistItemId);
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Notify SyncPlay group that member is ready for playback.
|
/// Request SyncPlay group to ignore member during group-wait.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The player status.</param>
|
/// <param name="requestData">The settings to set.</param>
|
||||||
/// <response code="204">Group state update sent to all group members.</response>
|
/// <response code="204">Member state updated.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Ready")]
|
[HttpPost("SetIgnoreWait")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlayReady(
|
public async Task<ActionResult> SyncPlaySetIgnoreWait(
|
||||||
[FromBody, Required] ReadyRequestDto requestData)
|
[FromBody, Required] IgnoreWaitRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new ReadyGroupRequest(
|
var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait);
|
||||||
requestData.When,
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
requestData.PositionTicks,
|
return NoContent();
|
||||||
requestData.IsPlaying,
|
}
|
||||||
requestData.PlaylistItemId);
|
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request SyncPlay group to ignore member during group-wait.
|
/// Request next item in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The settings to set.</param>
|
/// <param name="requestData">The current item information.</param>
|
||||||
/// <response code="204">Member state updated.</response>
|
/// <response code="204">Next item update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetIgnoreWait")]
|
[HttpPost("NextItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlaySetIgnoreWait(
|
public async Task<ActionResult> SyncPlayNextItem(
|
||||||
[FromBody, Required] IgnoreWaitRequestDto requestData)
|
[FromBody, Required] NextItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait);
|
var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId);
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request next item in SyncPlay group.
|
/// Request previous item in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The current item information.</param>
|
/// <param name="requestData">The current item information.</param>
|
||||||
/// <response code="204">Next item update sent to all group members.</response>
|
/// <response code="204">Previous item update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("NextItem")]
|
[HttpPost("PreviousItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlayNextItem(
|
public async Task<ActionResult> SyncPlayPreviousItem(
|
||||||
[FromBody, Required] NextItemRequestDto requestData)
|
[FromBody, Required] PreviousItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId);
|
var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId);
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request previous item in SyncPlay group.
|
/// Request to set repeat mode in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The current item information.</param>
|
/// <param name="requestData">The new repeat mode.</param>
|
||||||
/// <response code="204">Previous item update sent to all group members.</response>
|
/// <response code="204">Play queue update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("PreviousItem")]
|
[HttpPost("SetRepeatMode")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlayPreviousItem(
|
public async Task<ActionResult> SyncPlaySetRepeatMode(
|
||||||
[FromBody, Required] PreviousItemRequestDto requestData)
|
[FromBody, Required] SetRepeatModeRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId);
|
var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode);
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request to set repeat mode in SyncPlay group.
|
/// Request to set shuffle mode in SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The new repeat mode.</param>
|
/// <param name="requestData">The new shuffle mode.</param>
|
||||||
/// <response code="204">Play queue update sent to all group members.</response>
|
/// <response code="204">Play queue update sent to all group members.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetRepeatMode")]
|
[HttpPost("SetShuffleMode")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public async Task<ActionResult> SyncPlaySetRepeatMode(
|
public async Task<ActionResult> SyncPlaySetShuffleMode(
|
||||||
[FromBody, Required] SetRepeatModeRequestDto requestData)
|
[FromBody, Required] SetShuffleModeRequestDto requestData)
|
||||||
{
|
{
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode);
|
var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode);
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request to set shuffle mode in SyncPlay group.
|
/// Update session ping.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestData">The new shuffle mode.</param>
|
/// <param name="requestData">The new ping.</param>
|
||||||
/// <response code="204">Play queue update sent to all group members.</response>
|
/// <response code="204">Ping updated.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetShuffleMode")]
|
[HttpPost("Ping")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
public async Task<ActionResult> SyncPlayPing(
|
||||||
public async Task<ActionResult> SyncPlaySetShuffleMode(
|
[FromBody, Required] PingRequestDto requestData)
|
||||||
[FromBody, Required] SetShuffleModeRequestDto requestData)
|
{
|
||||||
{
|
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
||||||
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
|
var syncPlayRequest = new PingGroupRequest(requestData.Ping);
|
||||||
var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode);
|
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
||||||
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
|
return NoContent();
|
||||||
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.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// The system controller.
|
/// Initializes a new instance of the <see cref="SystemController"/> class.
|
||||||
/// </summary>
|
/// </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;
|
_appPaths = serverConfigurationManager.ApplicationPaths;
|
||||||
private readonly IApplicationPaths _appPaths;
|
_appHost = appHost;
|
||||||
private readonly IFileSystem _fileSystem;
|
_fileSystem = fileSystem;
|
||||||
private readonly INetworkManager _network;
|
_network = network;
|
||||||
private readonly ILogger<SystemController> _logger;
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SystemController"/> class.
|
/// Gets information about the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <response code="200">Information retrieved.</response>
|
||||||
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
/// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
|
||||||
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
[HttpGet("Info")]
|
||||||
/// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
|
[Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
|
||||||
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public SystemController(
|
public ActionResult<SystemInfo> GetSystemInfo()
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
{
|
||||||
IServerApplicationHost appHost,
|
return _appHost.GetSystemInfo(Request);
|
||||||
IFileSystem fileSystem,
|
}
|
||||||
INetworkManager network,
|
|
||||||
ILogger<SystemController> logger)
|
/// <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;
|
await Task.Delay(100).ConfigureAwait(false);
|
||||||
_appHost = appHost;
|
_appHost.Restart();
|
||||||
_fileSystem = fileSystem;
|
});
|
||||||
_network = network;
|
return NoContent();
|
||||||
_logger = logger;
|
}
|
||||||
|
|
||||||
|
/// <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>
|
var result = files.Select(i => new LogFile
|
||||||
/// 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);
|
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>
|
return result;
|
||||||
/// Gets public information about the server.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">Information retrieved.</response>
|
/// <summary>
|
||||||
/// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns>
|
/// Gets information about the request endpoint.
|
||||||
[HttpGet("Info/Public")]
|
/// </summary>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <response code="200">Information retrieved.</response>
|
||||||
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
/// <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>
|
/// <summary>
|
||||||
/// Pings the system.
|
/// Gets a log file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Information retrieved.</response>
|
/// <param name="name">The name of the log file to get.</param>
|
||||||
/// <returns>The server name.</returns>
|
/// <response code="200">Log file retrieved.</response>
|
||||||
[HttpGet("Ping", Name = "GetPingSystem")]
|
/// <returns>The log file.</returns>
|
||||||
[HttpPost("Ping", Name = "PostPingSystem")]
|
[HttpGet("Logs/Log")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
public ActionResult<string> PingSystem()
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
{
|
[ProducesFile(MediaTypeNames.Text.Plain)]
|
||||||
return _appHost.Name;
|
public ActionResult GetLogFile([FromQuery, Required] string name)
|
||||||
}
|
{
|
||||||
|
var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
|
||||||
|
.First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
/// <summary>
|
// For older files, assume fully static
|
||||||
/// Restarts the application.
|
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
|
||||||
/// </summary>
|
FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||||
/// <response code="204">Server restarted.</response>
|
return File(stream, "text/plain; charset=utf-8");
|
||||||
/// <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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shuts down the application.
|
/// Gets wake on lan information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="204">Server shut down.</response>
|
/// <response code="200">Information retrieved.</response>
|
||||||
/// <returns>No content. Server shut down.</returns>
|
/// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns>
|
||||||
[HttpPost("Shutdown")]
|
[HttpGet("WakeOnLanInfo")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[Obsolete("This endpoint is obsolete.")]
|
||||||
public ActionResult ShutdownApplication()
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
{
|
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
|
||||||
Task.Run(async () =>
|
{
|
||||||
{
|
var result = _network.GetMacAddresses()
|
||||||
await Task.Delay(100).ConfigureAwait(false);
|
.Select(i => new WakeOnLanInfo(i));
|
||||||
await _appHost.Shutdown().ConfigureAwait(false);
|
return Ok(result);
|
||||||
});
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,32 +3,31 @@ using MediaBrowser.Model.SyncPlay;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time sync controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("")]
|
||||||
|
public class TimeSyncController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time sync controller.
|
/// Gets the current UTC time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <response code="200">Time returned.</response>
|
||||||
public class TimeSyncController : BaseJellyfinApiController
|
/// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns>
|
||||||
|
[HttpGet("GetUtcTime")]
|
||||||
|
[ProducesResponseType(statusCode: StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<UtcTimeResponse> GetUtcTime()
|
||||||
{
|
{
|
||||||
/// <summary>
|
// Important to keep the following line at the beginning
|
||||||
/// Gets the current UTC time.
|
var requestReceptionTime = DateTime.UtcNow;
|
||||||
/// </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 end
|
// Important to keep the following line at the end
|
||||||
var responseTransmissionTime = DateTime.UtcNow;
|
var responseTransmissionTime = DateTime.UtcNow;
|
||||||
|
|
||||||
// Implementing NTP on such a high level results in this useless
|
// Implementing NTP on such a high level results in this useless
|
||||||
// information being sent. On the other hand it enables future additions.
|
// information being sent. On the other hand it enables future additions.
|
||||||
return new UtcTimeResponse(requestReceptionTime, responseTransmissionTime);
|
return new UtcTimeResponse(requestReceptionTime, responseTransmissionTime);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,290 +10,289 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The trailers controller.
|
/// Initializes a new instance of the <see cref="TrailersController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="itemsController">Instance of <see cref="ItemsController"/>.</param>
|
||||||
public class TrailersController : BaseJellyfinApiController
|
public TrailersController(ItemsController itemsController)
|
||||||
{
|
{
|
||||||
private readonly ItemsController _itemsController;
|
_itemsController = itemsController;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TrailersController"/> class.
|
/// Finds movies and trailers similar to a given trailer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="itemsController">Instance of <see cref="ItemsController"/>.</param>
|
/// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
|
||||||
public TrailersController(ItemsController itemsController)
|
/// <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>
|
||||||
_itemsController = itemsController;
|
/// <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>
|
return _itemsController
|
||||||
/// Finds movies and trailers similar to a given trailer.
|
.GetItems(
|
||||||
/// </summary>
|
userId,
|
||||||
/// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
|
maxOfficialRating,
|
||||||
/// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
|
hasThemeSong,
|
||||||
/// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
|
hasThemeVideo,
|
||||||
/// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
|
hasSubtitles,
|
||||||
/// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
|
hasSpecialFeature,
|
||||||
/// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
|
hasTrailer,
|
||||||
/// <param name="hasTrailer">Optional filter by items with trailers.</param>
|
adjacentTo,
|
||||||
/// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
|
parentIndexNumber,
|
||||||
/// <param name="parentIndexNumber">Optional filter by parent index number.</param>
|
hasParentalRating,
|
||||||
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
|
isHd,
|
||||||
/// <param name="isHd">Optional filter by items that are HD or not.</param>
|
is4K,
|
||||||
/// <param name="is4K">Optional filter by items that are 4K or not.</param>
|
locationTypes,
|
||||||
/// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
|
excludeLocationTypes,
|
||||||
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
|
isMissing,
|
||||||
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
|
isUnaired,
|
||||||
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
|
minCommunityRating,
|
||||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
minCriticRating,
|
||||||
/// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
|
minPremiereDate,
|
||||||
/// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
|
minDateLastSaved,
|
||||||
/// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
|
minDateLastSavedForUser,
|
||||||
/// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
|
maxPremiereDate,
|
||||||
/// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
|
hasOverview,
|
||||||
/// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
|
hasImdbId,
|
||||||
/// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
|
hasTmdbId,
|
||||||
/// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
|
hasTvdbId,
|
||||||
/// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
|
isMovie,
|
||||||
/// <param name="isMovie">Optional filter for live tv movies.</param>
|
isSeries,
|
||||||
/// <param name="isSeries">Optional filter for live tv series.</param>
|
isNews,
|
||||||
/// <param name="isNews">Optional filter for live tv news.</param>
|
isKids,
|
||||||
/// <param name="isKids">Optional filter for live tv kids.</param>
|
isSports,
|
||||||
/// <param name="isSports">Optional filter for live tv sports.</param>
|
excludeItemIds,
|
||||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
|
startIndex,
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
limit,
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
recursive,
|
||||||
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
|
searchTerm,
|
||||||
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
|
sortOrder,
|
||||||
/// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
|
parentId,
|
||||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
fields,
|
||||||
/// <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>
|
excludeItemTypes,
|
||||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
includeItemTypes,
|
||||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
filters,
|
||||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
isFavorite,
|
||||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
mediaTypes,
|
||||||
/// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
|
imageTypes,
|
||||||
/// <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>
|
sortBy,
|
||||||
/// <param name="isPlayed">Optional filter by items that are played, or not.</param>
|
isPlayed,
|
||||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
genres,
|
||||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
officialRatings,
|
||||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
tags,
|
||||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
|
years,
|
||||||
/// <param name="enableUserData">Optional, include user data.</param>
|
enableUserData,
|
||||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
imageTypeLimit,
|
||||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
enableImageTypes,
|
||||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
person,
|
||||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
|
personIds,
|
||||||
/// <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>
|
personTypes,
|
||||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
studios,
|
||||||
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
|
artists,
|
||||||
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
|
excludeArtistIds,
|
||||||
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
|
artistIds,
|
||||||
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
|
albumArtistIds,
|
||||||
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
|
contributingArtistIds,
|
||||||
/// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
|
albums,
|
||||||
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
|
albumIds,
|
||||||
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
|
ids,
|
||||||
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
|
videoTypes,
|
||||||
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
|
minOfficialRating,
|
||||||
/// <param name="isLocked">Optional filter by items that are locked.</param>
|
isLocked,
|
||||||
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
|
isPlaceHolder,
|
||||||
/// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
|
hasOfficialRating,
|
||||||
/// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
|
collapseBoxSetItems,
|
||||||
/// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
|
minWidth,
|
||||||
/// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
|
minHeight,
|
||||||
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
|
maxWidth,
|
||||||
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
|
maxHeight,
|
||||||
/// <param name="is3D">Optional filter by items that are 3D, or not.</param>
|
is3D,
|
||||||
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
|
seriesStatus,
|
||||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
nameStartsWithOrGreater,
|
||||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
nameStartsWith,
|
||||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
nameLessThan,
|
||||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
studioIds,
|
||||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
genreIds,
|
||||||
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
enableTotalRecordCount,
|
||||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
enableImages);
|
||||||
/// <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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,366 +19,365 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// The tv shows controller.
|
/// Initializes a new instance of the <see cref="TvShowsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Shows")]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
public class TvShowsController : BaseJellyfinApiController
|
/// <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;
|
_userManager = userManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
private readonly ITVSeriesManager _tvSeriesManager;
|
_tvSeriesManager = tvSeriesManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TvShowsController"/> class.
|
/// Gets a list of next up episodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="userId">The user id of the user to get the next up episodes for.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</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="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <param name="tvSeriesManager">Instance of the <see cref="ITVSeriesManager"/> interface.</param>
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
public TvShowsController(
|
/// <param name="seriesId">Optional. Filter by series id.</param>
|
||||||
IUserManager userManager,
|
/// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||||
IDtoService dtoService,
|
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||||
ITVSeriesManager tvSeriesManager)
|
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||||
{
|
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||||
_userManager = userManager;
|
/// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
|
||||||
_libraryManager = libraryManager;
|
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
|
||||||
_dtoService = dtoService;
|
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
|
||||||
_tvSeriesManager = tvSeriesManager;
|
/// <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>
|
var result = _tvSeriesManager.GetNextUp(
|
||||||
/// Gets a list of next up episodes.
|
new NextUpQuery
|
||||||
/// </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)
|
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
|
||||||
OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
|
|
||||||
MinPremiereDate = minPremiereDate,
|
|
||||||
StartIndex = startIndex,
|
|
||||||
Limit = limit,
|
Limit = limit,
|
||||||
ParentId = parentIdGuid,
|
ParentId = parentId,
|
||||||
Recursive = true,
|
SeriesId = seriesId,
|
||||||
DtoOptions = options
|
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>(
|
var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user);
|
||||||
startIndex,
|
|
||||||
itemsResult.Count,
|
|
||||||
returnItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
return new QueryResult<BaseItemDto>(
|
||||||
/// Gets episodes for a tv season.
|
startIndex,
|
||||||
/// </summary>
|
result.TotalRecordCount,
|
||||||
/// <param name="seriesId">The series id.</param>
|
returnItems);
|
||||||
/// <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>
|
/// <summary>
|
||||||
/// <param name="seasonId">Optional. Filter by season id.</param>
|
/// Gets a list of upcoming episodes.
|
||||||
/// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
|
/// </summary>
|
||||||
/// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
|
/// <param name="userId">The user id of the user to get the upcoming episodes for.</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="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="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="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
/// <param name="enableImageTypes">Optional. The image types to include in the output.</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>
|
||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
|
||||||
[HttpGet("{seriesId}/Episodes")]
|
[HttpGet("Upcoming")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
|
[FromQuery] Guid? userId,
|
||||||
[FromRoute, Required] Guid seriesId,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] int? limit,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
[FromQuery] int? season,
|
[FromQuery] Guid? parentId,
|
||||||
[FromQuery] Guid? seasonId,
|
[FromQuery] bool? enableImages,
|
||||||
[FromQuery] bool? isMissing,
|
[FromQuery] int? imageTypeLimit,
|
||||||
[FromQuery] Guid? adjacentTo,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||||
[FromQuery] Guid? startItemId,
|
[FromQuery] bool? enableUserData)
|
||||||
[FromQuery] int? startIndex,
|
{
|
||||||
[FromQuery] int? limit,
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
[FromQuery] bool? enableImages,
|
? null
|
||||||
[FromQuery] int? imageTypeLimit,
|
: _userManager.GetUserById(userId.Value);
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
|
||||||
[FromQuery] bool? enableUserData,
|
var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1);
|
||||||
[FromQuery] string? sortBy)
|
|
||||||
|
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)
|
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||||
? null
|
OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||||
: _userManager.GetUserById(userId.Value);
|
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 }
|
return new QueryResult<BaseItemDto>(
|
||||||
.AddClientFields(User)
|
startIndex,
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
itemsResult.Count,
|
||||||
|
returnItems);
|
||||||
|
}
|
||||||
|
|
||||||
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
|
/// <summary>
|
||||||
{
|
/// Gets episodes for a tv season.
|
||||||
var item = _libraryManager.GetItemById(seasonId.Value);
|
/// </summary>
|
||||||
if (item is not Season seasonItem)
|
/// <param name="seriesId">The series id.</param>
|
||||||
{
|
/// <param name="userId">The user id.</param>
|
||||||
return NotFound("No season exists with Id " + seasonId);
|
/// <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);
|
List<BaseItem> episodes;
|
||||||
}
|
|
||||||
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 seasonItem = series
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
.GetSeasons(user, dtoOptions)
|
.AddClientFields(User)
|
||||||
.FirstOrDefault(i => i.IndexNumber == season.Value);
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
episodes = seasonItem is null ?
|
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
var item = _libraryManager.GetItemById(seasonId.Value);
|
||||||
? null
|
if (item is not Season seasonItem)
|
||||||
: _userManager.GetUserById(userId.Value);
|
{
|
||||||
|
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)
|
if (_libraryManager.GetItemById(seriesId) is not Series series)
|
||||||
{
|
{
|
||||||
return NotFound("Series not found");
|
return NotFound("Series not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
var seasons = series.GetItemList(new InternalItemsQuery(user)
|
var seasonItem = series
|
||||||
{
|
.GetSeasons(user, dtoOptions)
|
||||||
IsMissing = isMissing,
|
.FirstOrDefault(i => i.IndexNumber == season.Value);
|
||||||
IsSpecialSeason = isSpecialSeason,
|
|
||||||
AdjacentTo = adjacentTo
|
|
||||||
});
|
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
episodes = seasonItem is null ?
|
||||||
.AddClientFields(User)
|
new List<BaseItem>()
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
: ((Season)seasonItem).GetEpisodes(user, dtoOptions);
|
||||||
|
|
||||||
var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
|
|
||||||
|
|
||||||
return new QueryResult<BaseItemDto>(returnItems);
|
|
||||||
}
|
}
|
||||||
|
else // No season number or season id was supplied. Returning all episodes.
|
||||||
/// <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 (_libraryManager.GetItemById(seriesId) is not Series series)
|
||||||
if (startIndex.HasValue)
|
|
||||||
{
|
{
|
||||||
items = items.Skip(startIndex.Value);
|
return NotFound("Series not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return limit
|
episodes = series.GetEpisodes(user, dtoOptions).ToList();
|
||||||
if (limit.HasValue)
|
|
||||||
{
|
|
||||||
items = items.Take(limit.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
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;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
|
/// The universal audio controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
[Route("")]
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
|
public class UniversalAudioController : BaseJellyfinApiController
|
||||||
/// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
|
{
|
||||||
/// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
|
private readonly ILibraryManager _libraryManager;
|
||||||
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
|
private readonly ILogger<UniversalAudioController> _logger;
|
||||||
public UniversalAudioController(
|
private readonly MediaInfoHelper _mediaInfoHelper;
|
||||||
ILibraryManager libraryManager,
|
private readonly AudioHelper _audioHelper;
|
||||||
ILogger<UniversalAudioController> logger,
|
private readonly DynamicHlsHelper _dynamicHlsHelper;
|
||||||
MediaInfoHelper mediaInfoHelper,
|
|
||||||
AudioHelper audioHelper,
|
/// <summary>
|
||||||
DynamicHlsHelper dynamicHlsHelper)
|
/// 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;
|
userId = User.GetUserId();
|
||||||
_logger = logger;
|
|
||||||
_mediaInfoHelper = mediaInfoHelper;
|
|
||||||
_audioHelper = audioHelper;
|
|
||||||
_dynamicHlsHelper = dynamicHlsHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
|
||||||
/// Gets an audio stream.
|
|
||||||
/// </summary>
|
var info = await _mediaInfoHelper.GetPlaybackInfo(
|
||||||
/// <param name="itemId">The item id.</param>
|
itemId,
|
||||||
/// <param name="container">Optional. The audio container.</param>
|
userId,
|
||||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
mediaSourceId)
|
||||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
.ConfigureAwait(false);
|
||||||
/// <param name="userId">Optional. The user id.</param>
|
|
||||||
/// <param name="audioCodec">Optional. The audio codec to transcode to.</param>
|
// set device specific data
|
||||||
/// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
/// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
|
|
||||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
foreach (var sourceInfo in info.MediaSources)
|
||||||
/// <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);
|
_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))
|
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
|
||||||
{
|
|
||||||
userId = User.GetUserId();
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
|
foreach (var source in info.MediaSources)
|
||||||
|
{
|
||||||
|
_mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video);
|
||||||
|
}
|
||||||
|
|
||||||
var info = await _mediaInfoHelper.GetPlaybackInfo(
|
var mediaSource = info.MediaSources[0];
|
||||||
itemId,
|
if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
|
||||||
userId,
|
{
|
||||||
mediaSourceId)
|
return Redirect(mediaSource.Path);
|
||||||
.ConfigureAwait(false);
|
}
|
||||||
|
|
||||||
// set device specific data
|
var isStatic = mediaSource.SupportsDirectStream;
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
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)
|
var dynamicHlsRequestDto = new HlsAudioRequestDto
|
||||||
{
|
|
||||||
_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
|
|
||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
|
Container = ".m3u8",
|
||||||
Static = isStatic,
|
Static = isStatic,
|
||||||
PlaySessionId = info.PlaySessionId,
|
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,
|
MediaSourceId = mediaSourceId,
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
AudioCodec = audioCodec,
|
AudioCodec = audioCodec,
|
||||||
@ -220,121 +187,153 @@ namespace Jellyfin.Api.Controllers
|
|||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames,
|
BreakOnNonKeyFrames = breakOnNonKeyFrames,
|
||||||
AudioSampleRate = maxAudioSampleRate,
|
AudioSampleRate = maxAudioSampleRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
|
|
||||||
MaxAudioBitDepth = maxAudioBitDepth,
|
MaxAudioBitDepth = maxAudioBitDepth,
|
||||||
AudioChannels = maxAudioChannels,
|
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||||
CopyTimestamps = true,
|
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
SubtitleMethod = SubtitleDeliveryMethod.Embed,
|
SubtitleMethod = SubtitleDeliveryMethod.Hls,
|
||||||
|
RequireAvc = false,
|
||||||
|
DeInterlace = false,
|
||||||
|
RequireNonAnamorphic = false,
|
||||||
|
EnableMpegtsM2TsMode = false,
|
||||||
TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
|
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(
|
var audioStreamingDto = new StreamingRequestDto
|
||||||
string[] containers,
|
|
||||||
string? transcodingContainer,
|
|
||||||
string? audioCodec,
|
|
||||||
string? transcodingProtocol,
|
|
||||||
bool? breakOnNonKeyFrames,
|
|
||||||
int? transcodingAudioChannels,
|
|
||||||
int? maxAudioSampleRate,
|
|
||||||
int? maxAudioBitDepth,
|
|
||||||
int? maxAudioChannels)
|
|
||||||
{
|
{
|
||||||
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;
|
return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false);
|
||||||
var directPlayProfiles = new DirectPlayProfile[len];
|
}
|
||||||
for (int i = 0; i < len; i++)
|
|
||||||
|
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);
|
Type = DlnaProfileType.Audio,
|
||||||
|
Container = parts[0],
|
||||||
var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
|
AudioCodec = audioCodecs
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// User library controller.
|
/// Initializes a new instance of the <see cref="UserLibraryController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
||||||
public class UserLibraryController : 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="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;
|
_userManager = userManager;
|
||||||
private readonly IUserDataManager _userDataRepository;
|
_userDataRepository = userDataRepository;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
private readonly IUserViewManager _userViewManager;
|
_userViewManager = userViewManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
_fileSystem = fileSystem;
|
||||||
private readonly ILyricManager _lyricManager;
|
_lyricManager = lyricManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UserLibraryController"/> class.
|
/// Gets an item from a user's library.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="userId">User id.</param>
|
||||||
/// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
/// <param name="itemId">Item id.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <response code="200">Item returned.</response>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <returns>An <see cref="OkResult"/> containing the d item.</returns>
|
||||||
/// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
|
[HttpGet("Users/{userId}/Items/{itemId}")]
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
|
public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
|
||||||
public UserLibraryController(
|
{
|
||||||
IUserManager userManager,
|
var user = _userManager.GetUserById(userId);
|
||||||
IUserDataManager userDataRepository,
|
|
||||||
ILibraryManager libraryManager,
|
var item = itemId.Equals(default)
|
||||||
IDtoService dtoService,
|
? _libraryManager.GetUserRootFolder()
|
||||||
IUserViewManager userViewManager,
|
: _libraryManager.GetItemById(itemId);
|
||||||
IFileSystem fileSystem,
|
|
||||||
ILyricManager lyricManager)
|
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;
|
var trailers = hasTrailers.LocalTrailers;
|
||||||
_userDataRepository = userDataRepository;
|
return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_dtoService = dtoService;
|
|
||||||
_userViewManager = userViewManager;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_lyricManager = lyricManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return Ok(item.GetExtras()
|
||||||
/// Gets an item from a user's library.
|
.Where(e => e.ExtraType == ExtraType.Trailer)
|
||||||
/// </summary>
|
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
|
||||||
/// <param name="userId">User id.</param>
|
}
|
||||||
/// <param name="itemId">Item id.</param>
|
|
||||||
/// <response code="200">Item returned.</response>
|
/// <summary>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the d item.</returns>
|
/// Gets special features for an item.
|
||||||
[HttpGet("Users/{userId}/Items/{itemId}")]
|
/// </summary>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <param name="userId">User id.</param>
|
||||||
public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
|
/// <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);
|
if (user.HidePlayedInLatest)
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var trailers = hasTrailers.LocalTrailers;
|
isPlayed = false;
|
||||||
return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||||
.Where(e => e.ExtraType == ExtraType.Trailer)
|
|
||||||
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
dto.ChildCount = childCount;
|
||||||
/// Gets special features for an item.
|
|
||||||
/// </summary>
|
return dto;
|
||||||
/// <param name="userId">User id.</param>
|
});
|
||||||
/// <param name="itemId">Item id.</param>
|
|
||||||
/// <response code="200">Special features returned.</response>
|
return Ok(dtos);
|
||||||
/// <returns>An <see cref="OkResult"/> containing the special features.</returns>
|
}
|
||||||
[HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
|
||||||
public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
|
{
|
||||||
|
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)
|
if (!hasMetdata)
|
||||||
? _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 (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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// User views controller.
|
/// Initializes a new instance of the <see cref="UserViewsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
|
||||||
public class UserViewsController : BaseJellyfinApiController
|
/// <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;
|
_userManager = userManager;
|
||||||
private readonly IUserViewManager _userViewManager;
|
_userViewManager = userViewManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UserViewsController"/> class.
|
/// Get user views.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="userId">User id.</param>
|
||||||
/// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
|
/// <param name="includeExternalContent">Whether or not to include external views such as channels or live tv.</param>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="presetViews">Preset views.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="includeHidden">Whether or not to include hidden content.</param>
|
||||||
public UserViewsController(
|
/// <response code="200">User views returned.</response>
|
||||||
IUserManager userManager,
|
/// <returns>An <see cref="OkResult"/> containing the user views.</returns>
|
||||||
IUserViewManager userViewManager,
|
[HttpGet("Users/{userId}/Views")]
|
||||||
IDtoService dtoService,
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
ILibraryManager libraryManager)
|
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;
|
UserId = userId,
|
||||||
_userViewManager = userViewManager;
|
IncludeHidden = includeHidden
|
||||||
_dtoService = dtoService;
|
};
|
||||||
_libraryManager = libraryManager;
|
|
||||||
|
if (includeExternalContent.HasValue)
|
||||||
|
{
|
||||||
|
query.IncludeExternalContent = includeExternalContent.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (presetViews.Length != 0)
|
||||||
/// 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
|
query.PresetViews = presetViews;
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var folders = _userViewManager.GetUserViews(query);
|
||||||
/// 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()
|
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||||
.GetChildren(user, true)
|
var fields = dtoOptions.Fields.ToList();
|
||||||
.OfType<Folder>()
|
|
||||||
.Where(UserView.IsEligibleForGrouping)
|
fields.Add(ItemFields.PrimaryImageAspectRatio);
|
||||||
.Select(i => new SpecialViewOptionDto
|
fields.Add(ItemFields.DisplayPreferencesId);
|
||||||
{
|
fields.Remove(ItemFields.BasicSyncInfo);
|
||||||
Name = i.Name,
|
dtoOptions.Fields = fields.ToArray();
|
||||||
Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
|
|
||||||
})
|
var user = _userManager.GetUserById(userId);
|
||||||
.OrderBy(i => i.Name)
|
|
||||||
.AsEnumerable());
|
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Attachments controller.
|
/// Initializes a new instance of the <see cref="VideoAttachmentsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Videos")]
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
public class VideoAttachmentsController : BaseJellyfinApiController
|
/// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
|
||||||
|
public VideoAttachmentsController(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IAttachmentExtractor attachmentExtractor)
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IAttachmentExtractor _attachmentExtractor;
|
_attachmentExtractor = attachmentExtractor;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="VideoAttachmentsController"/> class.
|
/// Get video attachment.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="videoId">Video ID.</param>
|
||||||
/// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
|
/// <param name="mediaSourceId">Media Source ID.</param>
|
||||||
public VideoAttachmentsController(
|
/// <param name="index">Attachment Index.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <response code="200">Attachment retrieved.</response>
|
||||||
IAttachmentExtractor attachmentExtractor)
|
/// <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;
|
var item = _libraryManager.GetItemById(videoId);
|
||||||
_attachmentExtractor = attachmentExtractor;
|
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)
|
||||||
/// <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
|
return NotFound(e.Message);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Years controller.
|
/// Initializes a new instance of the <see cref="YearsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
public class YearsController : BaseJellyfinApiController
|
/// <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;
|
_libraryManager = libraryManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="YearsController"/> class.
|
/// Get years.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="startIndex">Skips over a given number of items within the results. Use for paging.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||||
public YearsController(
|
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
IUserManager userManager,
|
/// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param>
|
||||||
IDtoService dtoService)
|
/// <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;
|
ExcludeItemTypes = excludeItemTypes,
|
||||||
_userManager = userManager;
|
IncludeItemTypes = includeItemTypes,
|
||||||
_dtoService = dtoService;
|
MediaTypes = mediaTypes,
|
||||||
}
|
DtoOptions = dtoOptions
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
|
||||||
/// Get years.
|
|
||||||
/// </summary>
|
IList<BaseItem> items;
|
||||||
/// <param name="startIndex">Skips over a given number of items within the results. Use for paging.</param>
|
if (parentItem.IsFolder)
|
||||||
/// <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 }
|
var folder = (Folder)parentItem;
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
|
||||||
|
|
||||||
User? user = userId is null || userId.Value.Equals(default)
|
if (userId.Equals(default))
|
||||||
? null
|
|
||||||
: _userManager.GetUserById(userId.Value);
|
|
||||||
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
|
|
||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
|
||||||
{
|
{
|
||||||
ExcludeItemTypes = excludeItemTypes,
|
items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
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;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
/// <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);
|
items = new[] { parentItem }.Where(Filter).ToList();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
if (startIndex.HasValue)
|
||||||
// Exclude item types
|
|
||||||
if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(baseItemKind))
|
|
||||||
{
|
{
|
||||||
return false;
|
ibnItems = ibnItems.Skip(startIndex.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include item types
|
if (limit.HasValue)
|
||||||
if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(baseItemKind))
|
|
||||||
{
|
{
|
||||||
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
|
return NotFound();
|
||||||
.Select(i => i.ProductionYear ?? 0)
|
|
||||||
.Where(i => i > 0)
|
|
||||||
.Distinct()
|
|
||||||
.Select(year => _libraryManager.GetYear(year));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 MediaBrowser.Model.Querying;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Extensions
|
namespace Jellyfin.Api.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dto Extensions.
|
||||||
|
/// </summary>
|
||||||
|
public static class DtoExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dto Extensions.
|
/// Add additional fields depending on client.
|
||||||
/// </summary>
|
/// </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>
|
dtoOptions.Fields ??= Array.Empty<ItemFields>();
|
||||||
/// Add additional fields depending on client.
|
|
||||||
/// </summary>
|
string? client = user.GetClient();
|
||||||
/// <remarks>
|
|
||||||
/// Use in place of GetDtoOptions.
|
// No client in claim
|
||||||
/// Legacy order: 2.
|
if (string.IsNullOrEmpty(client))
|
||||||
/// </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))
|
|
||||||
{
|
|
||||||
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;
|
return dtoOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount))
|
||||||
/// 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 (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||||
|
client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||||
if (imageTypeLimit.HasValue)
|
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.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.Net.Http.Headers;
|
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>
|
/// <summary>
|
||||||
/// Camel Case Json Profile Formatter.
|
/// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CamelCaseJsonProfileFormatter : SystemTextJsonOutputFormatter
|
public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCaseOptions)
|
||||||
{
|
{
|
||||||
/// <summary>
|
SupportedMediaTypes.Clear();
|
||||||
/// Initializes a new instance of the <see cref="CamelCaseJsonProfileFormatter"/> class.
|
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType));
|
||||||
/// </summary>
|
|
||||||
public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCaseOptions)
|
|
||||||
{
|
|
||||||
SupportedMediaTypes.Clear();
|
|
||||||
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(JsonDefaults.CamelCaseMediaType));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,34 +3,33 @@ using System.Threading.Tasks;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Formatters
|
namespace Jellyfin.Api.Formatters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Css output formatter.
|
||||||
|
/// </summary>
|
||||||
|
public class CssOutputFormatter : TextOutputFormatter
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Css output formatter.
|
/// Initializes a new instance of the <see cref="CssOutputFormatter"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CssOutputFormatter : TextOutputFormatter
|
public CssOutputFormatter()
|
||||||
{
|
{
|
||||||
/// <summary>
|
SupportedMediaTypes.Add("text/css");
|
||||||
/// Initializes a new instance of the <see cref="CssOutputFormatter"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public CssOutputFormatter()
|
|
||||||
{
|
|
||||||
SupportedMediaTypes.Add("text/css");
|
|
||||||
|
|
||||||
SupportedEncodings.Add(Encoding.UTF8);
|
SupportedEncodings.Add(Encoding.UTF8);
|
||||||
SupportedEncodings.Add(Encoding.Unicode);
|
SupportedEncodings.Add(Encoding.Unicode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Write context object to stream.
|
/// Write context object to stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">Writer context.</param>
|
/// <param name="context">Writer context.</param>
|
||||||
/// <param name="selectedEncoding">Unused. Writer encoding.</param>
|
/// <param name="selectedEncoding">Unused. Writer encoding.</param>
|
||||||
/// <returns>Write stream task.</returns>
|
/// <returns>Write stream task.</returns>
|
||||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||||
{
|
{
|
||||||
var stringResponse = context.Object?.ToString();
|
var stringResponse = context.Object?.ToString();
|
||||||
return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
|
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.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.Net.Http.Headers;
|
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>
|
/// <summary>
|
||||||
/// Pascal Case Json Profile Formatter.
|
/// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PascalCaseJsonProfileFormatter : SystemTextJsonOutputFormatter
|
public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCaseOptions)
|
||||||
{
|
{
|
||||||
/// <summary>
|
SupportedMediaTypes.Clear();
|
||||||
/// Initializes a new instance of the <see cref="PascalCaseJsonProfileFormatter"/> class.
|
// Add application/json for default formatter
|
||||||
/// </summary>
|
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Application.Json));
|
||||||
public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCaseOptions)
|
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Formatters
|
namespace Jellyfin.Api.Formatters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Xml output formatter.
|
||||||
|
/// </summary>
|
||||||
|
public class XmlOutputFormatter : TextOutputFormatter
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Xml output formatter.
|
/// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class XmlOutputFormatter : TextOutputFormatter
|
public XmlOutputFormatter()
|
||||||
{
|
{
|
||||||
/// <summary>
|
SupportedMediaTypes.Clear();
|
||||||
/// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
|
SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
|
||||||
/// </summary>
|
|
||||||
public XmlOutputFormatter()
|
|
||||||
{
|
|
||||||
SupportedMediaTypes.Clear();
|
|
||||||
SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
|
|
||||||
|
|
||||||
SupportedEncodings.Add(Encoding.UTF8);
|
SupportedEncodings.Add(Encoding.UTF8);
|
||||||
SupportedEncodings.Add(Encoding.Unicode);
|
SupportedEncodings.Add(Encoding.Unicode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
|
||||||
{
|
{
|
||||||
var stringResponse = context.Object?.ToString();
|
var stringResponse = context.Object?.ToString();
|
||||||
return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
|
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.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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>
|
/// <summary>
|
||||||
/// Audio helper.
|
/// Initializes a new instance of the <see cref="AudioHelper"/> class.
|
||||||
/// </summary>
|
/// </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;
|
_dlnaManager = dlnaManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
private readonly IDeviceManager _deviceManager;
|
_deviceManager = deviceManager;
|
||||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
_transcodingJobHelper = transcodingJobHelper;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
private readonly EncodingHelper _encodingHelper;
|
_encodingHelper = encodingHelper;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AudioHelper"/> class.
|
/// Get audio stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
/// <param name="transcodingJobType">Transcoding job type.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="streamingRequest">Streaming controller.Request dto.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
|
||||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
public async Task<ActionResult> GetAudioStream(
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
TranscodingJobType transcodingJobType,
|
||||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
StreamingRequestDto streamingRequest)
|
||||||
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
{
|
||||||
/// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
|
if (_httpContextAccessor.HttpContext is null)
|
||||||
/// <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)
|
|
||||||
{
|
{
|
||||||
_dlnaManager = dlnaManager;
|
throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
|
||||||
_userManager = userManager;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_mediaSourceManager = mediaSourceManager;
|
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
_mediaEncoder = mediaEncoder;
|
|
||||||
_deviceManager = deviceManager;
|
|
||||||
_transcodingJobHelper = transcodingJobHelper;
|
|
||||||
_httpClientFactory = httpClientFactory;
|
|
||||||
_httpContextAccessor = httpContextAccessor;
|
|
||||||
_encodingHelper = encodingHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head;
|
||||||
/// 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;
|
// CTS lifecycle is managed internally.
|
||||||
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
// CTS lifecycle is managed internally.
|
using var state = await StreamingHelpers.GetStreamingState(
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
streamingRequest,
|
||||||
|
|
||||||
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,
|
|
||||||
_httpContextAccessor.HttpContext,
|
_httpContextAccessor.HttpContext,
|
||||||
|
_mediaSourceManager,
|
||||||
|
_userManager,
|
||||||
|
_libraryManager,
|
||||||
|
_serverConfigurationManager,
|
||||||
|
_mediaEncoder,
|
||||||
|
_encodingHelper,
|
||||||
|
_dlnaManager,
|
||||||
|
_deviceManager,
|
||||||
_transcodingJobHelper,
|
_transcodingJobHelper,
|
||||||
ffmpegCommandLineArguments,
|
|
||||||
transcodingJobType,
|
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.AspNetCore.Mvc;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Helpers
|
namespace Jellyfin.Api.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The stream response helpers.
|
||||||
|
/// </summary>
|
||||||
|
public static class FileStreamResponseHelpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The stream response helpers.
|
/// Returns a static file from a remote source.
|
||||||
/// </summary>
|
/// </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>
|
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
|
||||||
/// 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))
|
httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// Can't dispose the response as it's required up the call chain.
|
||||||
/// Returns a static file from the server.
|
var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
|
||||||
/// </summary>
|
var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
|
||||||
/// <param name="path">The path to the file.</param>
|
|
||||||
/// <param name="contentType">The content type of the file.</param>
|
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||||
/// <returns>An <see cref="ActionResult"/> the file.</returns>
|
|
||||||
public static ActionResult GetStaticFileResult(
|
return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
|
||||||
string path,
|
}
|
||||||
string 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>
|
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
|
||||||
/// Returns a transcoded file from the server.
|
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||||
/// </summary>
|
try
|
||||||
/// <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
|
TranscodingJobDto? job;
|
||||||
var outputPath = state.OutputFilePath;
|
if (!File.Exists(outputPath))
|
||||||
|
|
||||||
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
|
||||||
|
|
||||||
var contentType = state.GetMimeType(outputPath);
|
|
||||||
|
|
||||||
// Headers only
|
|
||||||
if (isHeadRequest)
|
|
||||||
{
|
{
|
||||||
httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
|
job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false);
|
||||||
return new OkResult();
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
|
||||||
|
state.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
|
var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
|
||||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
return new FileStreamResult(stream, contentType);
|
||||||
try
|
}
|
||||||
{
|
finally
|
||||||
TranscodingJobDto? job;
|
{
|
||||||
if (!File.Exists(outputPath))
|
transcodingLock.Release();
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,182 +2,181 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Helpers
|
namespace Jellyfin.Api.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hls Codec string helpers.
|
||||||
|
/// </summary>
|
||||||
|
public static class HlsCodecStringHelpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hls Codec string helpers.
|
/// Codec name for MP3.
|
||||||
/// </summary>
|
/// </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>
|
return MP3;
|
||||||
/// Codec name for MP3.
|
}
|
||||||
/// </summary>
|
|
||||||
public const string MP3 = "mp4a.40.34";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Codec name for AC-3.
|
/// Gets an AAC codec string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string AC3 = "mp4a.a5";
|
/// <param name="profile">AAC profile.</param>
|
||||||
|
/// <returns>AAC codec string.</returns>
|
||||||
|
public static string GetAACString(string? profile)
|
||||||
|
{
|
||||||
|
StringBuilder result = new StringBuilder("mp4a", 9);
|
||||||
|
|
||||||
/// <summary>
|
if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
|
||||||
/// 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()
|
|
||||||
{
|
{
|
||||||
return MP3;
|
result.Append(".40.5");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Default to LC if profile is invalid
|
||||||
|
result.Append(".40.2");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return result.ToString();
|
||||||
/// Gets an AAC codec string.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="profile">AAC profile.</param>
|
/// <summary>
|
||||||
/// <returns>AAC codec string.</returns>
|
/// Gets an AC-3 codec string.
|
||||||
public static string GetAACString(string? profile)
|
/// </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);
|
result.Append(".6400");
|
||||||
|
}
|
||||||
if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
result.Append(".40.5");
|
result.Append(".4D40");
|
||||||
}
|
}
|
||||||
else
|
else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Default to LC if profile is invalid
|
result.Append(".42E0");
|
||||||
result.Append(".40.2");
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
return result.ToString();
|
// Default to constrained baseline if profile is invalid
|
||||||
|
result.Append(".4240");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
string levelHex = level.ToString("X2", CultureInfo.InvariantCulture);
|
||||||
/// Gets an AC-3 codec string.
|
result.Append(levelHex);
|
||||||
/// </summary>
|
|
||||||
/// <returns>AC-3 codec string.</returns>
|
return result.ToString();
|
||||||
public static string GetAC3String()
|
}
|
||||||
|
|
||||||
|
/// <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>
|
result.Append(".L")
|
||||||
/// Gets an E-AC-3 codec string.
|
.Append(level)
|
||||||
/// </summary>
|
.Append(".B0");
|
||||||
/// <returns>E-AC-3 codec string.</returns>
|
|
||||||
public static string GetEAC3String()
|
|
||||||
{
|
|
||||||
return EAC3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
return result.ToString();
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,131 +8,130 @@ using MediaBrowser.Controller.MediaEncoding;
|
|||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Helpers
|
namespace Jellyfin.Api.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The hls helpers.
|
||||||
|
/// </summary>
|
||||||
|
public static class HlsHelpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The hls helpers.
|
/// Waits for a minimum number of segments to be available.
|
||||||
/// </summary>
|
/// </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>
|
logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist);
|
||||||
/// Waits for a minimum number of segments to be available.
|
|
||||||
/// </summary>
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
/// <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);
|
try
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
{
|
||||||
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
|
using var reader = new StreamReader(fileStream);
|
||||||
var fileStream = new FileStream(
|
var count = 0;
|
||||||
playlist,
|
|
||||||
FileMode.Open,
|
while (!reader.EndOfStream)
|
||||||
FileAccess.Read,
|
|
||||||
FileShare.ReadWrite,
|
|
||||||
IODefaults.FileStreamBufferSize,
|
|
||||||
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
|
||||||
await using (fileStream.ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(fileStream);
|
var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||||
var count = 0;
|
if (line is null)
|
||||||
|
|
||||||
while (!reader.EndOfStream)
|
|
||||||
{
|
{
|
||||||
var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
// Nothing currently in buffer.
|
||||||
if (line is null)
|
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++;
|
logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
|
||||||
if (count >= segmentCount)
|
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>
|
/// <summary>
|
||||||
/// Gets the #EXT-X-MAP string.
|
/// Gets the #EXT-X-MAP string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="outputPath">The output path of the file.</param>
|
/// <param name="outputPath">The output path of the file.</param>
|
||||||
/// <param name="state">The <see cref="StreamState"/>.</param>
|
/// <param name="state">The <see cref="StreamState"/>.</param>
|
||||||
/// <param name="isOsDepends">Get a normal string or depends on OS.</param>
|
/// <param name="isOsDepends">Get a normal string or depends on OS.</param>
|
||||||
/// <returns>The string text of #EXT-X-MAP.</returns>
|
/// <returns>The string text of #EXT-X-MAP.</returns>
|
||||||
public static string GetFmp4InitFileName(string outputPath, StreamState state, bool isOsDepends)
|
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;
|
return fmp4InitFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (OperatingSystem.IsWindows())
|
||||||
/// 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);
|
// on Windows
|
||||||
|
// #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4"
|
||||||
var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
|
fmp4InitFileName = outputPrefix + "-1" + outputExtension;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// Media info helper.
|
/// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
|
||||||
/// </summary>
|
/// </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;
|
_userManager = userManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
private readonly ILogger<MediaInfoHelper> _logger;
|
_logger = logger;
|
||||||
private readonly INetworkManager _networkManager;
|
_networkManager = networkManager;
|
||||||
private readonly IDeviceManager _deviceManager;
|
_deviceManager = deviceManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MediaInfoHelper"/> class.
|
/// Get playback info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="id">Item id.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="userId">User Id.</param>
|
||||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
/// <param name="mediaSourceId">Media source id.</param>
|
||||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
/// <param name="liveStreamId">Live stream id.</param>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoHelper}"/> interface.</param>
|
public async Task<PlaybackInfoResponse> GetPlaybackInfo(
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
Guid id,
|
||||||
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
Guid? userId,
|
||||||
public MediaInfoHelper(
|
string? mediaSourceId = null,
|
||||||
IUserManager userManager,
|
string? liveStreamId = null)
|
||||||
ILibraryManager libraryManager,
|
{
|
||||||
IMediaSourceManager mediaSourceManager,
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
IMediaEncoder mediaEncoder,
|
? null
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
: _userManager.GetUserById(userId.Value);
|
||||||
ILogger<MediaInfoHelper> logger,
|
var item = _libraryManager.GetItemById(id);
|
||||||
INetworkManager networkManager,
|
var result = new PlaybackInfoResponse();
|
||||||
IDeviceManager deviceManager)
|
|
||||||
|
MediaSourceInfo[] mediaSources;
|
||||||
|
if (string.IsNullOrWhiteSpace(liveStreamId))
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
// TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
|
||||||
_libraryManager = libraryManager;
|
var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
|
||||||
_mediaSourceManager = mediaSourceManager;
|
|
||||||
_mediaEncoder = mediaEncoder;
|
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
_logger = logger;
|
|
||||||
_networkManager = networkManager;
|
|
||||||
_deviceManager = deviceManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
if (string.IsNullOrWhiteSpace(mediaSourceId))
|
||||||
/// 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))
|
|
||||||
{
|
{
|
||||||
// TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
|
mediaSources = mediaSourcesList.ToArray();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
|
mediaSources = mediaSourcesList
|
||||||
|
.Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
|
||||||
mediaSources = new[] { mediaSource };
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
if (mediaSources.Length == 0)
|
mediaSources = new[] { mediaSource };
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (mediaSources.Length == 0)
|
||||||
/// 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);
|
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 },
|
result.MediaSources = mediaSourcesClone;
|
||||||
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);
|
result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
if (!enableDirectPlay)
|
return result;
|
||||||
{
|
}
|
||||||
mediaSource.SupportsDirectPlay = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enableDirectStream || !allowVideoStreamCopy)
|
/// <summary>
|
||||||
{
|
/// SetDeviceSpecificData.
|
||||||
mediaSource.SupportsDirectStream = false;
|
/// </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)
|
var options = new MediaOptions
|
||||||
{
|
{
|
||||||
mediaSource.SupportsTranscoding = false;
|
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)
|
if (item is Audio)
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
|
||||||
"User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
|
{
|
||||||
user.Username,
|
mediaSource.SupportsTranscoding = false;
|
||||||
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
|
}
|
||||||
|
}
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
|
||||||
"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 (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
|
streamInfo.PlayMethod = PlayMethod.Transcode;
|
||||||
{
|
|
||||||
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 = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-');
|
||||||
mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
|
|
||||||
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
|
if (!allowVideoStreamCopy)
|
||||||
mediaSource.TranscodingContainer = streamInfo.Container;
|
|
||||||
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
|
|
||||||
{
|
{
|
||||||
streamInfo.PlayMethod = PlayMethod.Transcode;
|
mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1;
|
if (!allowAudioStreamCopy)
|
||||||
})
|
|
||||||
.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;
|
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
attachment.DeliveryUrl = string.Format(
|
||||||
var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
|
CultureInfo.InvariantCulture,
|
||||||
|
"/Videos/{0}/{1}/Attachments/{2}",
|
||||||
if (remoteClientMaxBitrate <= 0)
|
item.Id,
|
||||||
{
|
mediaSource.Id,
|
||||||
remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
|
attachment.Index);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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 Jellyfin.Api.Models.PlaybackDtos;
|
||||||
using MediaBrowser.Model.IO;
|
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>
|
/// <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>
|
/// </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;
|
_job = job;
|
||||||
private readonly TranscodingJobDto? _job;
|
_transcodingJobHelper = transcodingJobHelper;
|
||||||
private readonly TranscodingJobHelper? _transcodingJobHelper;
|
_timeoutMs = timeoutMs;
|
||||||
private readonly int _timeoutMs;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
/// <summary>
|
_stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||||
/// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="filePath">The path to the transcoded file.</param>
|
/// <summary>
|
||||||
/// <param name="job">The transcoding job information.</param>
|
/// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
|
||||||
/// <param name="transcodingJobHelper">The transcoding job helper.</param>
|
/// </summary>
|
||||||
/// <param name="timeoutMs">The timeout duration in milliseconds.</param>
|
/// <param name="stream">The stream to progressively copy.</param>
|
||||||
public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000)
|
/// <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;
|
totalBytesRead += _stream.Read(buffer);
|
||||||
_transcodingJobHelper = transcodingJobHelper;
|
if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
|
||||||
_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);
|
break;
|
||||||
if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.Sleep(50);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateBytesWritten(totalBytesRead);
|
Thread.Sleep(50);
|
||||||
|
|
||||||
return totalBytesRead;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
UpdateBytesWritten(totalBytesRead);
|
||||||
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
|
||||||
=> await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
return totalBytesRead;
|
||||||
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
}
|
||||||
|
|
||||||
|
/// <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;
|
totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||||
var stopwatch = Stopwatch.StartNew();
|
if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
{
|
||||||
totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
break;
|
||||||
if (StopReading(totalBytesRead, stopwatch.ElapsedMilliseconds))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateBytesWritten(totalBytesRead);
|
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return totalBytesRead;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
UpdateBytesWritten(totalBytesRead);
|
||||||
public override long Seek(long offset, SeekOrigin origin)
|
|
||||||
=> throw new NotSupportedException();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
return totalBytesRead;
|
||||||
public override void SetLength(long value)
|
}
|
||||||
=> throw new NotSupportedException();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(byte[] buffer, int offset, int count)
|
public override long Seek(long offset, SeekOrigin origin)
|
||||||
=> throw new NotSupportedException();
|
=> throw new NotSupportedException();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Dispose(bool disposing)
|
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();
|
_transcodingJobHelper?.OnTranscodeEndRequest(_job);
|
||||||
|
|
||||||
if (_job is not null)
|
|
||||||
{
|
|
||||||
_transcodingJobHelper?.OnTranscodeEndRequest(_job);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
_disposed = true;
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
private void UpdateBytesWritten(int totalBytesRead)
|
|
||||||
{
|
{
|
||||||
if (_job is not null)
|
_disposed = true;
|
||||||
{
|
base.Dispose(disposing);
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 MediaBrowser.Model.Querying;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Helpers
|
namespace Jellyfin.Api.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request Extensions.
|
||||||
|
/// </summary>
|
||||||
|
public static class RequestHelpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request Extensions.
|
/// Get Order By.
|
||||||
/// </summary>
|
/// </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>
|
if (sortBy.Count == 0)
|
||||||
/// 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)
|
return Array.Empty<(string, SortOrder)>();
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var result = new (string, SortOrder)[sortBy.Count];
|
||||||
/// Checks if the user can update an entry.
|
var i = 0;
|
||||||
/// </summary>
|
// Add elements which have a SortOrder specified
|
||||||
/// <param name="userManager">An instance of the <see cref="IUserManager"/> interface.</param>
|
for (; i < requestedSortOrder.Count; i++)
|
||||||
/// <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();
|
result[i] = (sortBy[i], requestedSortOrder[i]);
|
||||||
var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator);
|
}
|
||||||
|
|
||||||
// If they're going to update the record of another user, they must be an administrator
|
// Add remaining elements with the first specified SortOrder
|
||||||
if (!userId.Equals(authenticatedUserId) && !isAdministrator)
|
// 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
|
return dto;
|
||||||
if (!restrictUserPreferences || isAdministrator)
|
});
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = userManager.GetUserById(userId);
|
return new QueryResult<BaseItemDto>(
|
||||||
return user.EnableUserPreferenceAccess;
|
result.StartIndex,
|
||||||
}
|
result.TotalRecordCount,
|
||||||
|
dtos.ToArray());
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 Microsoft.Extensions.Logging;
|
||||||
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
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>
|
/// <summary>
|
||||||
/// Redirect requests without baseurl prefix to the baseurl prefixed URL.
|
/// Initializes a new instance of the <see cref="BaseUrlRedirectionMiddleware"/> class.
|
||||||
/// </summary>
|
/// </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;
|
_next = next;
|
||||||
private readonly ILogger<BaseUrlRedirectionMiddleware> _logger;
|
_logger = logger;
|
||||||
private readonly IConfiguration _configuration;
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BaseUrlRedirectionMiddleware"/> class.
|
/// Executes the middleware action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next">The next delegate in the pipeline.</param>
|
/// <param name="httpContext">The current HTTP context.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="serverConfigurationManager">The server configuration manager.</param>
|
||||||
/// <param name="configuration">The application configuration.</param>
|
/// <returns>The async task.</returns>
|
||||||
public BaseUrlRedirectionMiddleware(
|
public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager)
|
||||||
RequestDelegate next,
|
{
|
||||||
ILogger<BaseUrlRedirectionMiddleware> logger,
|
var localPath = httpContext.Request.Path.ToString();
|
||||||
IConfiguration configuration)
|
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;
|
// Redirect health endpoint
|
||||||
_logger = logger;
|
if (string.Equals(localPath, "/health", StringComparison.OrdinalIgnoreCase)
|
||||||
_configuration = configuration;
|
|| string.Equals(localPath, "/health/", StringComparison.OrdinalIgnoreCase))
|
||||||
}
|
|
||||||
|
|
||||||
/// <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
|
_logger.LogDebug("Redirecting /health check");
|
||||||
if (string.Equals(localPath, "/health", StringComparison.OrdinalIgnoreCase)
|
httpContext.Response.Redirect(baseUrlPrefix + "/health");
|
||||||
|| 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);
|
|
||||||
return;
|
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.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// Exception Middleware.
|
/// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class.
|
||||||
/// </summary>
|
/// </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;
|
_next = next;
|
||||||
private readonly ILogger<ExceptionMiddleware> _logger;
|
_logger = logger;
|
||||||
private readonly IServerConfigurationManager _configuration;
|
_configuration = serverConfigurationManager;
|
||||||
private readonly IWebHostEnvironment _hostEnvironment;
|
_hostEnvironment = hostEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ExceptionMiddleware"/> class.
|
/// Invoke request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next">Next request delegate.</param>
|
/// <param name="context">Request context.</param>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
|
/// <returns>Task.</returns>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
public async Task Invoke(HttpContext context)
|
||||||
/// <param name="hostEnvironment">Instance of the <see cref="IWebHostEnvironment"/> interface.</param>
|
{
|
||||||
public ExceptionMiddleware(
|
try
|
||||||
RequestDelegate next,
|
|
||||||
ILogger<ExceptionMiddleware> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IWebHostEnvironment hostEnvironment)
|
|
||||||
{
|
{
|
||||||
_next = next;
|
await _next(context).ConfigureAwait(false);
|
||||||
_logger = logger;
|
|
||||||
_configuration = serverConfigurationManager;
|
|
||||||
_hostEnvironment = hostEnvironment;
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
/// <summary>
|
|
||||||
/// Invoke request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">Request context.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public async Task Invoke(HttpContext context)
|
|
||||||
{
|
{
|
||||||
try
|
if (context.Response.HasStarted)
|
||||||
{
|
{
|
||||||
await _next(context).ConfigureAwait(false);
|
_logger.LogWarning("The response has already started, the exception middleware will not be executed.");
|
||||||
}
|
throw;
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ex;
|
ex = GetActualException(ex);
|
||||||
}
|
|
||||||
|
|
||||||
private static int GetStatusCode(Exception ex)
|
bool ignoreStackTrace =
|
||||||
{
|
ex is SocketException
|
||||||
switch (ex)
|
|| ex is IOException
|
||||||
|
|| ex is OperationCanceledException
|
||||||
|
|| ex is SecurityException
|
||||||
|
|| ex is AuthenticationException
|
||||||
|
|| ex is FileNotFoundException;
|
||||||
|
|
||||||
|
if (ignoreStackTrace)
|
||||||
{
|
{
|
||||||
case ArgumentException _: return StatusCodes.Status400BadRequest;
|
_logger.LogError(
|
||||||
case AuthenticationException _: return StatusCodes.Status401Unauthorized;
|
"Error processing request: {ExceptionMessage}. URL {Method} {Url}.",
|
||||||
case SecurityException _: return StatusCodes.Status403Forbidden;
|
ex.Message.TrimEnd('.'),
|
||||||
case DirectoryNotFoundException _:
|
context.Request.Method,
|
||||||
case FileNotFoundException _:
|
context.Request.Path);
|
||||||
case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
|
}
|
||||||
case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
|
else
|
||||||
default: return StatusCodes.Status500InternalServerError;
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Error processing request. URL {Method} {Url}.",
|
||||||
|
context.Request.Method,
|
||||||
|
context.Request.Path);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private string NormalizeExceptionMessage(string msg)
|
context.Response.StatusCode = GetStatusCode(ex);
|
||||||
{
|
context.Response.ContentType = MediaTypeNames.Text.Plain;
|
||||||
// Strip any information we don't want to reveal
|
|
||||||
return msg.Replace(
|
// Don't send exception unless the server is in a Development environment
|
||||||
_configuration.ApplicationPaths.ProgramSystemPath,
|
var errorContent = _hostEnvironment.IsDevelopment()
|
||||||
string.Empty,
|
? NormalizeExceptionMessage(ex.Message)
|
||||||
StringComparison.OrdinalIgnoreCase)
|
: "Error processing request.";
|
||||||
.Replace(
|
await context.Response.WriteAsync(errorContent).ConfigureAwait(false);
|
||||||
_configuration.ApplicationPaths.ProgramDataPath,
|
|
||||||
string.Empty,
|
|
||||||
StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 MediaBrowser.Common.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
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>
|
/// <summary>
|
||||||
/// Validates the IP of requests coming from local networks wrt. remote access.
|
/// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class.
|
/// Executes the middleware action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next">The next delegate in the pipeline.</param>
|
/// <param name="httpContext">The current HTTP context.</param>
|
||||||
public IpBasedAccessValidationMiddleware(RequestDelegate next)
|
/// <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;
|
// Running locally.
|
||||||
}
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _next(httpContext).ConfigureAwait(false);
|
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 MediaBrowser.Controller.Configuration;
|
||||||
using Microsoft.AspNetCore.Http;
|
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>
|
/// <summary>
|
||||||
/// Validates the LAN host IP based on application configuration.
|
/// Initializes a new instance of the <see cref="LanFilteringMiddleware"/> class.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="LanFilteringMiddleware"/> class.
|
/// Executes the middleware action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next">The next delegate in the pipeline.</param>
|
/// <param name="httpContext">The current HTTP context.</param>
|
||||||
public LanFilteringMiddleware(RequestDelegate next)
|
/// <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>
|
await _next(httpContext).ConfigureAwait(false);
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,52 +3,51 @@ using System.Threading.Tasks;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// Removes /emby and /mediabrowser from requested route.
|
/// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class.
|
||||||
/// </summary>
|
/// </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";
|
_next = next;
|
||||||
private const string MediabrowserPath = "/mediabrowser";
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly RequestDelegate _next;
|
/// <summary>
|
||||||
private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger;
|
/// Executes the middleware action.
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <param name="httpContext">The current HTTP context.</param>
|
||||||
/// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class.
|
/// <returns>The async task.</returns>
|
||||||
/// </summary>
|
public async Task Invoke(HttpContext httpContext)
|
||||||
/// <param name="next">The next delegate in the pipeline.</param>
|
{
|
||||||
/// <param name="logger">The logger.</param>
|
var localPath = httpContext.Request.Path.ToString();
|
||||||
public LegacyEmbyRouteRewriteMiddleware(
|
if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase))
|
||||||
RequestDelegate next,
|
|
||||||
ILogger<LegacyEmbyRouteRewriteMiddleware> logger)
|
|
||||||
{
|
{
|
||||||
_next = next;
|
httpContext.Request.Path = localPath[EmbyPath.Length..];
|
||||||
_logger = logger;
|
_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>
|
await _next(httpContext).ConfigureAwait(false);
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,38 +2,37 @@ using System.Threading.Tasks;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
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>
|
/// <summary>
|
||||||
/// URL decodes the querystring before binding.
|
/// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="QueryStringDecodingMiddleware"/> class.
|
/// Executes the middleware action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next">The next delegate in the pipeline.</param>
|
/// <param name="httpContext">The current HTTP context.</param>
|
||||||
public QueryStringDecodingMiddleware(RequestDelegate next)
|
/// <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>
|
await _next(httpContext).ConfigureAwait(false);
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,63 +7,62 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Http.Extensions;
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// Response time middleware.
|
/// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
|
||||||
/// </summary>
|
/// </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;
|
/// <summary>
|
||||||
private readonly ILogger<ResponseTimeMiddleware> _logger;
|
/// 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>
|
var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
|
||||||
/// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
|
var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
|
||||||
/// </summary>
|
context.Response.OnStarting(() =>
|
||||||
/// <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)
|
|
||||||
{
|
{
|
||||||
_next = next;
|
var responseTime = Stopwatch.GetElapsedTime(startTimestamp);
|
||||||
_logger = logger;
|
var responseTimeMs = responseTime.TotalMilliseconds;
|
||||||
}
|
if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug))
|
||||||
|
|
||||||
/// <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);
|
_logger.LogDebug(
|
||||||
var responseTimeMs = responseTime.TotalMilliseconds;
|
"Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
|
||||||
if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug))
|
context.Request.GetDisplayUrl(),
|
||||||
{
|
context.GetNormalizedRemoteIp(),
|
||||||
_logger.LogDebug(
|
responseTime,
|
||||||
"Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
|
context.Response.StatusCode);
|
||||||
context.Request.GetDisplayUrl(),
|
}
|
||||||
context.GetNormalizedRemoteIp(),
|
|
||||||
responseTime,
|
|
||||||
context.Response.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Response.Headers[ResponseHeaderResponseTime] = responseTimeMs.ToString(CultureInfo.InvariantCulture);
|
context.Response.Headers[ResponseHeaderResponseTime] = responseTimeMs.ToString(CultureInfo.InvariantCulture);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call the next delegate/middleware in the pipeline
|
// Call the next delegate/middleware in the pipeline
|
||||||
await this._next(context).ConfigureAwait(false);
|
await this._next(context).ConfigureAwait(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,45 +3,44 @@ using System.Threading.Tasks;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// Redirect requests to robots.txt to web/robots.txt.
|
/// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class.
|
||||||
/// </summary>
|
/// </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;
|
_next = next;
|
||||||
private readonly ILogger<RobotsRedirectionMiddleware> _logger;
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class.
|
/// Executes the middleware action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next">The next delegate in the pipeline.</param>
|
/// <param name="httpContext">The current HTTP context.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <returns>The async task.</returns>
|
||||||
public RobotsRedirectionMiddleware(
|
public async Task Invoke(HttpContext httpContext)
|
||||||
RequestDelegate next,
|
{
|
||||||
ILogger<RobotsRedirectionMiddleware> logger)
|
var localPath = httpContext.Request.Path.ToString();
|
||||||
|
if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_next = next;
|
_logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
|
||||||
_logger = logger;
|
httpContext.Response.Redirect("web/robots.txt");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
await _next(httpContext).ConfigureAwait(false);
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,47 +5,46 @@ using MediaBrowser.Controller;
|
|||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using Microsoft.AspNetCore.Http;
|
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>
|
/// <summary>
|
||||||
/// Shows a custom message during server startup.
|
/// Initializes a new instance of the <see cref="ServerStartupMessageMiddleware"/> class.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ServerStartupMessageMiddleware"/> class.
|
/// Executes the middleware action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next">The next delegate in the pipeline.</param>
|
/// <param name="httpContext">The current HTTP context.</param>
|
||||||
public ServerStartupMessageMiddleware(RequestDelegate next)
|
/// <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>
|
var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
|
||||||
/// Executes the middleware action.
|
httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||||
/// </summary>
|
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
|
||||||
/// <param name="httpContext">The current HTTP context.</param>
|
await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false);
|
||||||
/// <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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,79 +6,78 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Middleware
|
namespace Jellyfin.Api.Middleware;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Defines the <see cref="UrlDecodeQueryFeature"/>.
|
|
||||||
/// </summary>
|
|
||||||
public class UrlDecodeQueryFeature : IQueryFeature
|
|
||||||
{
|
|
||||||
private IQueryCollection? _store;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UrlDecodeQueryFeature"/> class.
|
/// Defines the <see cref="UrlDecodeQueryFeature"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="feature">The <see cref="IQueryFeature"/> instance.</param>
|
public class UrlDecodeQueryFeature : IQueryFeature
|
||||||
public UrlDecodeQueryFeature(IQueryFeature feature)
|
{
|
||||||
|
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>
|
set
|
||||||
/// Gets or sets a value indicating the url decoded <see cref="IQueryCollection"/>.
|
|
||||||
/// </summary>
|
|
||||||
public IQueryCollection Query
|
|
||||||
{
|
{
|
||||||
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.
|
_store = value;
|
||||||
if (value.Count != 1)
|
return;
|
||||||
{
|
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
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>
|
/// <summary>
|
||||||
/// Handles WebSocket requests.
|
/// Initializes a new instance of the <see cref="WebSocketHandlerMiddleware"/> class.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="WebSocketHandlerMiddleware"/> class.
|
/// Executes the middleware action.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next">The next delegate in the pipeline.</param>
|
/// <param name="httpContext">The current HTTP context.</param>
|
||||||
public WebSocketHandlerMiddleware(RequestDelegate next)
|
/// <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>
|
await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false);
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,86 +5,85 @@ using System.Threading.Tasks;
|
|||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.Extensions.Logging;
|
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>
|
/// <summary>
|
||||||
/// Comma delimited array model binder.
|
/// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinder"/> class.
|
||||||
/// Returns an empty array of specified type if there is no query parameter.
|
|
||||||
/// </summary>
|
/// </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>
|
/// <inheritdoc/>
|
||||||
/// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinder"/> class.
|
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||||
/// </summary>
|
{
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{CommaDelimitedArrayModelBinder}"/> interface.</param>
|
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||||
public CommaDelimitedArrayModelBinder(ILogger<CommaDelimitedArrayModelBinder> logger)
|
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);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
/// <inheritdoc/>
|
|
||||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
|
||||||
{
|
{
|
||||||
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
var value = valueProviderResult.FirstValue;
|
||||||
var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
|
|
||||||
var converter = TypeDescriptor.GetConverter(elementType);
|
|
||||||
|
|
||||||
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);
|
bindingContext.Result = ModelBindingResult.Success(typedValues);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var value = valueProviderResult.FirstValue;
|
var emptyResult = Array.CreateInstance(elementType, 0);
|
||||||
|
bindingContext.Result = ModelBindingResult.Success(emptyResult);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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];
|
try
|
||||||
var convertedCount = 0;
|
|
||||||
for (var i = 0; i < values.Count; i++)
|
|
||||||
{
|
{
|
||||||
try
|
parsedValues[i] = converter.ConvertFromString(values[i].Trim());
|
||||||
{
|
convertedCount++;
|
||||||
parsedValues[i] = converter.ConvertFromString(values[i].Trim());
|
|
||||||
convertedCount++;
|
|
||||||
}
|
|
||||||
catch (FormatException e)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(e, "Error converting value.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (FormatException e)
|
||||||
var typedValues = Array.CreateInstance(elementType, convertedCount);
|
|
||||||
var typedValueIndex = 0;
|
|
||||||
for (var i = 0; i < parsedValues.Length; i++)
|
|
||||||
{
|
{
|
||||||
if (parsedValues[i] != null)
|
_logger.LogDebug(e, "Error converting value.");
|
||||||
{
|
|
||||||
typedValues.SetValue(parsedValues[i], typedValueIndex);
|
|
||||||
typedValueIndex++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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