aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api/Controllers')
-rw-r--r--Jellyfin.Api/Controllers/ActivityLogController.cs77
-rw-r--r--Jellyfin.Api/Controllers/ApiKeyController.cs109
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs810
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs679
-rw-r--r--Jellyfin.Api/Controllers/BrandingController.cs85
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs416
-rw-r--r--Jellyfin.Api/Controllers/ClientLogController.cs109
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs167
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs209
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs158
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs209
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs342
-rw-r--r--Jellyfin.Api/Controllers/DlnaController.cs207
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs551
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs3660
-rw-r--r--Jellyfin.Api/Controllers/EnvironmentController.cs285
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs360
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs324
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs296
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs3784
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs646
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs464
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs125
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs665
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs1593
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs1541
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs509
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs2182
-rw-r--r--Jellyfin.Api/Controllers/LocalizationController.cs115
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs522
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs470
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs307
-rw-r--r--Jellyfin.Api/Controllers/NotificationsController.cs53
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs269
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs212
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs348
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs635
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs412
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs204
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs277
-rw-r--r--Jellyfin.Api/Controllers/ScheduledTasksController.cs243
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs404
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs833
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs237
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs236
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs887
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs132
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs757
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs355
-rw-r--r--Jellyfin.Api/Controllers/TimeSyncController.cs41
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs559
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs634
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs524
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs966
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs692
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs196
-rw-r--r--Jellyfin.Api/Controllers/VideoAttachmentsController.cs113
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs1175
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs332
59 files changed, 16806 insertions, 16896 deletions
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index ae45f647f..c3d02976e 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -8,50 +8,49 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Activity log controller.
+/// </summary>
+[Route("System/ActivityLog")]
+[Authorize(Policy = Policies.RequiresElevation)]
+public class ActivityLogController : BaseJellyfinApiController
{
+ private readonly IActivityManager _activityManager;
+
/// <summary>
- /// Activity log controller.
+ /// Initializes a new instance of the <see cref="ActivityLogController"/> class.
/// </summary>
- [Route("System/ActivityLog")]
- [Authorize(Policy = Policies.RequiresElevation)]
- public class ActivityLogController : BaseJellyfinApiController
+ /// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
+ public ActivityLogController(IActivityManager activityManager)
{
- private readonly IActivityManager _activityManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ActivityLogController"/> class.
- /// </summary>
- /// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
- public ActivityLogController(IActivityManager activityManager)
- {
- _activityManager = activityManager;
- }
+ _activityManager = activityManager;
+ }
- /// <summary>
- /// Gets activity log entries.
- /// </summary>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
- /// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param>
- /// <response code="200">Activity log returned.</response>
- /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
- [HttpGet("Entries")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<ActivityLogEntry>>> GetLogEntries(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] DateTime? minDate,
- [FromQuery] bool? hasUserId)
+ /// <summary>
+ /// Gets activity log entries.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
+ /// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param>
+ /// <response code="200">Activity log returned.</response>
+ /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
+ [HttpGet("Entries")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<ActivityLogEntry>>> GetLogEntries(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] DateTime? minDate,
+ [FromQuery] bool? hasUserId)
+ {
+ return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
{
- return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
- {
- Skip = startIndex,
- Limit = limit,
- MinDate = minDate,
- HasUserId = hasUserId
- }).ConfigureAwait(false);
- }
+ Skip = startIndex,
+ Limit = limit,
+ MinDate = minDate,
+ HasUserId = hasUserId
+ }).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index 024a15349..991f8cbf2 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -7,70 +7,69 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Authentication controller.
+/// </summary>
+[Route("Auth")]
+public class ApiKeyController : BaseJellyfinApiController
{
+ private readonly IAuthenticationManager _authenticationManager;
+
/// <summary>
- /// Authentication controller.
+ /// Initializes a new instance of the <see cref="ApiKeyController"/> class.
/// </summary>
- [Route("Auth")]
- public class ApiKeyController : BaseJellyfinApiController
+ /// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param>
+ public ApiKeyController(IAuthenticationManager authenticationManager)
{
- private readonly IAuthenticationManager _authenticationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ApiKeyController"/> class.
- /// </summary>
- /// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param>
- public ApiKeyController(IAuthenticationManager authenticationManager)
- {
- _authenticationManager = authenticationManager;
- }
+ _authenticationManager = authenticationManager;
+ }
- /// <summary>
- /// Get all keys.
- /// </summary>
- /// <response code="200">Api keys retrieved.</response>
- /// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns>
- [HttpGet("Keys")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys()
- {
- var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false);
+ /// <summary>
+ /// Get all keys.
+ /// </summary>
+ /// <response code="200">Api keys retrieved.</response>
+ /// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns>
+ [HttpGet("Keys")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys()
+ {
+ var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false);
- return new QueryResult<AuthenticationInfo>(keys);
- }
+ return new QueryResult<AuthenticationInfo>(keys);
+ }
- /// <summary>
- /// Create a new api key.
- /// </summary>
- /// <param name="app">Name of the app using the authentication key.</param>
- /// <response code="204">Api key created.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Keys")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CreateKey([FromQuery, Required] string app)
- {
- await _authenticationManager.CreateApiKey(app).ConfigureAwait(false);
+ /// <summary>
+ /// Create a new api key.
+ /// </summary>
+ /// <param name="app">Name of the app using the authentication key.</param>
+ /// <response code="204">Api key created.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Keys")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CreateKey([FromQuery, Required] string app)
+ {
+ await _authenticationManager.CreateApiKey(app).ConfigureAwait(false);
- return NoContent();
- }
+ return NoContent();
+ }
- /// <summary>
- /// Remove an api key.
- /// </summary>
- /// <param name="key">The access token to delete.</param>
- /// <response code="204">Api key deleted.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Keys/{key}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RevokeKey([FromRoute, Required] string key)
- {
- await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false);
+ /// <summary>
+ /// Remove an api key.
+ /// </summary>
+ /// <param name="key">The access token to delete.</param>
+ /// <response code="204">Api key deleted.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Keys/{key}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> RevokeKey([FromRoute, Required] string key)
+ {
+ await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false);
- return NoContent();
- }
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index c8ac2ed52..11933fd97 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -17,464 +16,463 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The artists controller.
+/// </summary>
+[Route("Artists")]
+[Authorize]
+public class ArtistsController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+
/// <summary>
- /// The artists controller.
+ /// Initializes a new instance of the <see cref="ArtistsController"/> class.
/// </summary>
- [Route("Artists")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ArtistsController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ public ArtistsController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ArtistsController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- public ArtistsController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService)
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ }
+
+ /// <summary>
+ /// Gets all artists from a given item, folder, or the entire library.
+ /// </summary>
+ /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="searchTerm">Optional. Search term.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+ /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
+ /// <param name="enableUserData">Optional, include user data.</param>
+ /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+ /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
+ /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+ /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="userId">User id.</param>
+ /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+ /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+ /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <param name="enableTotalRecordCount">Total record count.</param>
+ /// <response code="200">Artists returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the artists.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetArtists(
+ [FromQuery] double? minCommunityRating,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = null;
+ BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
+
+ if (userId.HasValue && !userId.Equals(default))
{
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
+ user = _userManager.GetUserById(userId.Value);
}
- /// <summary>
- /// Gets all artists from a given item, folder, or the entire library.
- /// </summary>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="searchTerm">Optional. Search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
- /// <param name="enableUserData">Optional, include user data.</param>
- /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
- /// <param name="userId">User id.</param>
- /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
- /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
- /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
- /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
- /// <param name="enableImages">Optional, include image information in output.</param>
- /// <param name="enableTotalRecordCount">Total record count.</param>
- /// <response code="200">Artists returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the artists.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetArtists(
- [FromQuery] double? minCommunityRating,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery] Guid? userId,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery] bool? enableImages = true,
- [FromQuery] bool enableTotalRecordCount = true)
+ var query = new InternalItemsQuery(user)
{
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ MediaTypes = mediaTypes,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ Tags = tags,
+ OfficialRatings = officialRatings,
+ Genres = genres,
+ GenreIds = genreIds,
+ StudioIds = studioIds,
+ Person = person,
+ PersonIds = personIds,
+ PersonTypes = personTypes,
+ Years = years,
+ MinCommunityRating = minCommunityRating,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
+ };
- User? user = null;
- BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
-
- if (userId.HasValue && !userId.Equals(default))
+ if (parentId.HasValue)
+ {
+ if (parentItem is Folder)
{
- user = _userManager.GetUserById(userId.Value);
+ query.AncestorIds = new[] { parentId.Value };
}
-
- var query = new InternalItemsQuery(user)
+ else
{
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- MediaTypes = mediaTypes,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = tags,
- OfficialRatings = officialRatings,
- Genres = genres,
- GenreIds = genreIds,
- StudioIds = studioIds,
- Person = person,
- PersonIds = personIds,
- PersonTypes = personTypes,
- Years = years,
- MinCommunityRating = minCommunityRating,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
- };
-
- if (parentId.HasValue)
+ query.ItemIds = new[] { parentId.Value };
+ }
+ }
+
+ // Studios
+ if (studios.Length != 0)
+ {
+ query.StudioIds = studios.Select(i =>
{
- if (parentItem is Folder)
+ try
{
- 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)
+ foreach (var filter in filters)
+ {
+ switch (filter)
{
- query.StudioIds = studios.Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i is not null).Select(i => i!.Id).ToArray();
+ 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;
}
+ }
- foreach (var filter in filters)
+ 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)
{
- 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;
- }
+ 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;
}
- var result = _libraryManager.GetArtists(query);
+ return dto;
+ });
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, itemCounts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
+ return new QueryResult<BaseItemDto>(
+ query.StartIndex,
+ result.TotalRecordCount,
+ dtos.ToArray());
+ }
- 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;
- }
+ /// <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);
- return dto;
- });
+ User? user = null;
+ BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
- return new QueryResult<BaseItemDto>(
- query.StartIndex,
- result.TotalRecordCount,
- dtos.ToArray());
+ if (userId.HasValue && !userId.Equals(default))
+ {
+ user = _userManager.GetUserById(userId.Value);
}
- /// <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 query = new InternalItemsQuery(user)
{
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
-
- User? user = null;
- BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
+ 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 (userId.HasValue && !userId.Equals(default))
+ if (parentId.HasValue)
+ {
+ if (parentItem is Folder)
{
- user = _userManager.GetUserById(userId.Value);
+ query.AncestorIds = new[] { parentId.Value };
}
-
- var query = new InternalItemsQuery(user)
+ else
{
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- MediaTypes = mediaTypes,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = tags,
- OfficialRatings = officialRatings,
- Genres = genres,
- GenreIds = genreIds,
- StudioIds = studioIds,
- Person = person,
- PersonIds = personIds,
- PersonTypes = personTypes,
- Years = years,
- MinCommunityRating = minCommunityRating,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
- };
-
- if (parentId.HasValue)
+ query.ItemIds = new[] { parentId.Value };
+ }
+ }
+
+ // Studios
+ if (studios.Length != 0)
+ {
+ query.StudioIds = studios.Select(i =>
{
- if (parentItem is Folder)
+ try
{
- 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)
+ foreach (var filter in filters)
+ {
+ switch (filter)
{
- query.StudioIds = studios.Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i is not null).Select(i => i!.Id).ToArray();
+ 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;
}
+ }
- 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 result = _libraryManager.GetAlbumArtists(query);
+ var dtos = result.Items.Select(i =>
+ {
+ var (baseItem, itemCounts) = i;
+ var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
- var dtos = result.Items.Select(i =>
+ if (includeItemTypes.Length != 0)
{
- 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;
- });
+ 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 new QueryResult<BaseItemDto>(
- query.StartIndex,
- result.TotalRecordCount,
- dtos.ToArray());
- }
+ return dto;
+ });
- /// <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);
+ return new QueryResult<BaseItemDto>(
+ query.StartIndex,
+ result.TotalRecordCount,
+ dtos.ToArray());
+ }
- var item = _libraryManager.GetArtist(name, dtoOptions);
+ /// <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);
- if (userId.HasValue && !userId.Value.Equals(default))
- {
- var user = _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetArtist(name, dtoOptions);
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ if (userId.HasValue && !userId.Value.Equals(default))
+ {
+ var user = _userManager.GetUserById(userId.Value);
- return _dtoService.GetBaseItemDto(item, dtoOptions);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
}
}
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 94f7a7b82..968193a6f 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -10,355 +10,354 @@ using MediaBrowser.Model.Dlna;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The audio controller.
+/// </summary>
+// TODO: In order to authenticate this in the future, Dlna playback will require updating
+public class AudioController : BaseJellyfinApiController
{
+ private readonly AudioHelper _audioHelper;
+
+ private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
+
/// <summary>
- /// The audio controller.
+ /// Initializes a new instance of the <see cref="AudioController"/> class.
/// </summary>
- // TODO: In order to authenticate this in the future, Dlna playback will require updating
- public class AudioController : BaseJellyfinApiController
+ /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
+ public AudioController(AudioHelper audioHelper)
{
- private readonly AudioHelper _audioHelper;
-
- private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="AudioController"/> class.
- /// </summary>
- /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
- public AudioController(AudioHelper audioHelper)
- {
- _audioHelper = audioHelper;
- }
+ _audioHelper = audioHelper;
+ }
- /// <summary>
- /// Gets an audio stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="container">The audio container.</param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
- /// <param name="playSessionId">The play session id.</param>
- /// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment length.</param>
- /// <param name="minSegments">The minimum number of segments.</param>
- /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
- /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
- /// <param name="requireAvc">Optional. Whether to require avc.</param>
- /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
- /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
- /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
- /// <param name="liveStreamId">The live stream id.</param>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <param name="streamOptions">Optional. The streaming options.</param>
- /// <response code="200">Audio stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("{itemId}/stream", Name = "GetAudioStream")]
- [HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesAudioFile]
- public async Task<ActionResult> GetAudioStream(
- [FromRoute, Required] Guid itemId,
- [FromQuery] string? container,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string>? streamOptions)
+ /// <summary>
+ /// Gets an audio stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The audio container.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <param name="segmentContainer">The segment container.</param>
+ /// <param name="segmentLength">The segment length.</param>
+ /// <param name="minSegments">The minimum number of segments.</param>
+ /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+ /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+ /// <param name="requireAvc">Optional. Whether to require avc.</param>
+ /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
+ /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+ /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+ /// <param name="liveStreamId">The live stream id.</param>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <response code="200">Audio stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("{itemId}/stream", Name = "GetAudioStream")]
+ [HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
+ public async Task<ActionResult> GetAudioStream(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] string? container,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string>? streamOptions)
+ {
+ StreamingRequestDto streamingRequest = new StreamingRequestDto
{
- StreamingRequestDto streamingRequest = new StreamingRequestDto
- {
- Id = itemId,
- Container = container,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Static,
- StreamOptions = streamOptions
- };
+ Id = itemId,
+ Container = container,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Static,
+ StreamOptions = streamOptions
+ };
- return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
- }
+ 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)
+ /// <summary>
+ /// Gets an audio stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The audio container.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <param name="segmentContainer">The segment container.</param>
+ /// <param name="segmentLength">The segment length.</param>
+ /// <param name="minSegments">The minimum number of segments.</param>
+ /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+ /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+ /// <param name="requireAvc">Optional. Whether to require avc.</param>
+ /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+ /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+ /// <param name="liveStreamId">The live stream id.</param>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <response code="200">Audio stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
+ [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
+ public async Task<ActionResult> GetAudioStreamByContainer(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string container,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string>? streamOptions)
+ {
+ StreamingRequestDto streamingRequest = new StreamingRequestDto
{
- StreamingRequestDto streamingRequest = new StreamingRequestDto
- {
- Id = itemId,
- Container = container,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Static,
- StreamOptions = streamOptions
- };
+ Id = itemId,
+ Container = container,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Static,
+ StreamOptions = streamOptions
+ };
- return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
- }
+ return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs
index d3ea41201..3c2c4b4db 100644
--- a/Jellyfin.Api/Controllers/BrandingController.cs
+++ b/Jellyfin.Api/Controllers/BrandingController.cs
@@ -4,54 +4,53 @@ using MediaBrowser.Model.Branding;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Branding controller.
+/// </summary>
+public class BrandingController : BaseJellyfinApiController
{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
/// <summary>
- /// Branding controller.
+ /// Initializes a new instance of the <see cref="BrandingController"/> class.
/// </summary>
- public class BrandingController : BaseJellyfinApiController
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public BrandingController(IServerConfigurationManager serverConfigurationManager)
{
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="BrandingController"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public BrandingController(IServerConfigurationManager serverConfigurationManager)
- {
- _serverConfigurationManager = serverConfigurationManager;
- }
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- /// <summary>
- /// Gets branding configuration.
- /// </summary>
- /// <response code="200">Branding configuration returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns>
- [HttpGet("Configuration")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BrandingOptions> GetBrandingOptions()
- {
- return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- }
+ /// <summary>
+ /// Gets branding configuration.
+ /// </summary>
+ /// <response code="200">Branding configuration returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns>
+ [HttpGet("Configuration")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<BrandingOptions> GetBrandingOptions()
+ {
+ return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+ }
- /// <summary>
- /// Gets branding css.
- /// </summary>
- /// <response code="200">Branding css returned.</response>
- /// <response code="204">No branding css configured.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the branding css if exist,
- /// or a <see cref="NoContentResult"/> if the css is not configured.
- /// </returns>
- [HttpGet("Css")]
- [HttpGet("Css.css", Name = "GetBrandingCss_2")]
- [Produces("text/css")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult<string> GetBrandingCss()
- {
- var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- return options.CustomCss ?? string.Empty;
- }
+ /// <summary>
+ /// Gets branding css.
+ /// </summary>
+ /// <response code="200">Branding css returned.</response>
+ /// <response code="204">No branding css configured.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the branding css if exist,
+ /// or a <see cref="NoContentResult"/> if the css is not configured.
+ /// </returns>
+ [HttpGet("Css")]
+ [HttpGet("Css.css", Name = "GetBrandingCss_2")]
+ [Produces("text/css")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult<string> GetBrandingCss()
+ {
+ var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+ return options.CustomCss ?? string.Empty;
}
}
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index d5b589a3f..42f072f66 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
@@ -18,234 +17,233 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Channels Controller.
+/// </summary>
+[Authorize]
+public class ChannelsController : BaseJellyfinApiController
{
+ private readonly IChannelManager _channelManager;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// Channels Controller.
+ /// Initializes a new instance of the <see cref="ChannelsController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ChannelsController : BaseJellyfinApiController
+ /// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public ChannelsController(IChannelManager channelManager, IUserManager userManager)
{
- private readonly IChannelManager _channelManager;
- private readonly IUserManager _userManager;
+ _channelManager = channelManager;
+ _userManager = userManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ChannelsController"/> class.
- /// </summary>
- /// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public ChannelsController(IChannelManager channelManager, IUserManager userManager)
+ /// <summary>
+ /// Gets available channels.
+ /// </summary>
+ /// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param>
+ /// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param>
+ /// <param name="isFavorite">Optional. Filter by channels that are favorite.</param>
+ /// <response code="200">Channels returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the channels.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetChannels(
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? supportsLatestItems,
+ [FromQuery] bool? supportsMediaDeletion,
+ [FromQuery] bool? isFavorite)
+ {
+ return _channelManager.GetChannels(new ChannelQuery
{
- _channelManager = channelManager;
- _userManager = userManager;
- }
+ Limit = limit,
+ StartIndex = startIndex,
+ UserId = userId ?? Guid.Empty,
+ SupportsLatestItems = supportsLatestItems,
+ SupportsMediaDeletion = supportsMediaDeletion,
+ IsFavorite = isFavorite
+ });
+ }
- /// <summary>
- /// Gets available channels.
- /// </summary>
- /// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</param>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param>
- /// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param>
- /// <param name="isFavorite">Optional. Filter by channels that are favorite.</param>
- /// <response code="200">Channels returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the channels.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetChannels(
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool? supportsLatestItems,
- [FromQuery] bool? supportsMediaDeletion,
- [FromQuery] bool? isFavorite)
- {
- return _channelManager.GetChannels(new ChannelQuery
- {
- Limit = limit,
- StartIndex = startIndex,
- UserId = userId ?? Guid.Empty,
- SupportsLatestItems = supportsLatestItems,
- SupportsMediaDeletion = supportsMediaDeletion,
- IsFavorite = isFavorite
- });
- }
+ /// <summary>
+ /// Get all channel features.
+ /// </summary>
+ /// <response code="200">All channel features returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
+ [HttpGet("Features")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures()
+ {
+ return _channelManager.GetAllChannelFeatures();
+ }
- /// <summary>
- /// Get all channel features.
- /// </summary>
- /// <response code="200">All channel features returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
- [HttpGet("Features")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures()
- {
- return _channelManager.GetAllChannelFeatures();
- }
+ /// <summary>
+ /// Get channel features.
+ /// </summary>
+ /// <param name="channelId">Channel id.</param>
+ /// <response code="200">Channel features returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
+ [HttpGet("{channelId}/Features")]
+ public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] Guid channelId)
+ {
+ return _channelManager.GetChannelFeatures(channelId);
+ }
- /// <summary>
- /// Get channel features.
- /// </summary>
- /// <param name="channelId">Channel id.</param>
- /// <response code="200">Channel features returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
- [HttpGet("{channelId}/Features")]
- public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] Guid channelId)
- {
- return _channelManager.GetChannelFeatures(channelId);
- }
+ /// <summary>
+ /// Get channel items.
+ /// </summary>
+ /// <param name="channelId">Channel Id.</param>
+ /// <param name="folderId">Optional. Folder Id.</param>
+ /// <param name="userId">Optional. User Id.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <response code="200">Channel items returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> representing the request to get the channel items.
+ /// The task result contains an <see cref="OkResult"/> containing the channel items.
+ /// </returns>
+ [HttpGet("{channelId}/Items")]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
+ [FromRoute, Required] Guid channelId,
+ [FromQuery] Guid? folderId,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- /// <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 query = new InternalItemsQuery(user)
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ Limit = limit,
+ StartIndex = startIndex,
+ ChannelIds = new[] { channelId },
+ ParentId = folderId ?? Guid.Empty,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
+ DtoOptions = new DtoOptions { Fields = fields }
+ };
- 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)
+ foreach (var filter in filters)
+ {
+ switch (filter)
{
- switch (filter)
- {
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- }
+ case ItemFilter.IsFolder:
+ query.IsFolder = true;
+ break;
+ case ItemFilter.IsNotFolder:
+ query.IsFolder = false;
+ break;
+ case ItemFilter.IsUnplayed:
+ query.IsPlayed = false;
+ break;
+ case ItemFilter.IsPlayed:
+ query.IsPlayed = true;
+ break;
+ case ItemFilter.IsFavorite:
+ query.IsFavorite = true;
+ break;
+ case ItemFilter.IsResumable:
+ query.IsResumable = true;
+ break;
+ case ItemFilter.Likes:
+ query.IsLiked = true;
+ break;
+ case ItemFilter.Dislikes:
+ query.IsLiked = false;
+ break;
+ case ItemFilter.IsFavoriteOrLikes:
+ query.IsFavoriteOrLiked = true;
+ break;
}
-
- return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
}
- /// <summary>
- /// Gets latest channel items.
- /// </summary>
- /// <param name="userId">Optional. User Id.</param>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="filters">Optional. Specify additional filters to apply.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
- /// <response code="200">Latest channel items returned.</response>
- /// <returns>
- /// A <see cref="Task"/> representing the request to get the latest channel items.
- /// The task result contains an <see cref="OkResult"/> containing the latest channel items.
- /// </returns>
- [HttpGet("Items/Latest")]
- public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
+ }
- var query = new InternalItemsQuery(user)
- {
- Limit = limit,
- StartIndex = startIndex,
- ChannelIds = channelIds,
- DtoOptions = new DtoOptions { Fields = fields }
- };
+ /// <summary>
+ /// Gets latest channel items.
+ /// </summary>
+ /// <param name="userId">Optional. User Id.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
+ /// <response code="200">Latest channel items returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> representing the request to get the latest channel items.
+ /// The task result contains an <see cref="OkResult"/> containing the latest channel items.
+ /// </returns>
+ [HttpGet("Items/Latest")]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- foreach (var filter in filters)
+ var query = new InternalItemsQuery(user)
+ {
+ Limit = limit,
+ StartIndex = startIndex,
+ ChannelIds = channelIds,
+ DtoOptions = new DtoOptions { Fields = fields }
+ };
+
+ foreach (var filter in filters)
+ {
+ switch (filter)
{
- switch (filter)
- {
- case ItemFilter.IsFolder:
- query.IsFolder = true;
- break;
- case ItemFilter.IsNotFolder:
- query.IsFolder = false;
- break;
- case ItemFilter.IsUnplayed:
- query.IsPlayed = false;
- break;
- case ItemFilter.IsPlayed:
- query.IsPlayed = true;
- break;
- case ItemFilter.IsFavorite:
- query.IsFavorite = true;
- break;
- case ItemFilter.IsResumable:
- query.IsResumable = true;
- break;
- case ItemFilter.Likes:
- query.IsLiked = true;
- break;
- case ItemFilter.Dislikes:
- query.IsLiked = false;
- break;
- case ItemFilter.IsFavoriteOrLikes:
- query.IsFavoriteOrLiked = true;
- break;
- }
+ case ItemFilter.IsFolder:
+ query.IsFolder = true;
+ break;
+ case ItemFilter.IsNotFolder:
+ query.IsFolder = false;
+ break;
+ case ItemFilter.IsUnplayed:
+ query.IsPlayed = false;
+ break;
+ case ItemFilter.IsPlayed:
+ query.IsPlayed = true;
+ break;
+ case ItemFilter.IsFavorite:
+ query.IsFavorite = true;
+ break;
+ case ItemFilter.IsResumable:
+ query.IsResumable = true;
+ break;
+ case ItemFilter.Likes:
+ query.IsLiked = true;
+ break;
+ case ItemFilter.Dislikes:
+ query.IsLiked = false;
+ break;
+ case ItemFilter.IsFavoriteOrLikes:
+ query.IsFavoriteOrLiked = true;
+ break;
}
-
- return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
}
+
+ return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs
index ed073a687..2c5dbacbb 100644
--- a/Jellyfin.Api/Controllers/ClientLogController.cs
+++ b/Jellyfin.Api/Controllers/ClientLogController.cs
@@ -1,9 +1,7 @@
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.ClientLogDtos;
using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Configuration;
@@ -11,71 +9,70 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Client log controller.
+/// </summary>
+[Authorize]
+public class ClientLogController : BaseJellyfinApiController
{
+ private const int MaxDocumentSize = 1_000_000;
+ private readonly IClientEventLogger _clientEventLogger;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
/// <summary>
- /// Client log controller.
+ /// Initializes a new instance of the <see cref="ClientLogController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ClientLogController : BaseJellyfinApiController
+ /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public ClientLogController(
+ IClientEventLogger clientEventLogger,
+ IServerConfigurationManager serverConfigurationManager)
{
- private const int MaxDocumentSize = 1_000_000;
- private readonly IClientEventLogger _clientEventLogger;
- private readonly IServerConfigurationManager _serverConfigurationManager;
+ _clientEventLogger = clientEventLogger;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ClientLogController"/> class.
- /// </summary>
- /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public ClientLogController(
- IClientEventLogger clientEventLogger,
- IServerConfigurationManager serverConfigurationManager)
+ /// <summary>
+ /// Upload a document.
+ /// </summary>
+ /// <response code="200">Document saved.</response>
+ /// <response code="403">Event logging disabled.</response>
+ /// <response code="413">Upload size too large.</response>
+ /// <returns>Create response.</returns>
+ [HttpPost("Document")]
+ [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
+ [AcceptsFile(MediaTypeNames.Text.Plain)]
+ [RequestSizeLimit(MaxDocumentSize)]
+ public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile()
+ {
+ if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
{
- _clientEventLogger = clientEventLogger;
- _serverConfigurationManager = serverConfigurationManager;
+ return Forbid();
}
- /// <summary>
- /// Upload a document.
- /// </summary>
- /// <response code="200">Document saved.</response>
- /// <response code="403">Event logging disabled.</response>
- /// <response code="413">Upload size too large.</response>
- /// <returns>Create response.</returns>
- [HttpPost("Document")]
- [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
- [AcceptsFile(MediaTypeNames.Text.Plain)]
- [RequestSizeLimit(MaxDocumentSize)]
- public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile()
+ if (Request.ContentLength > MaxDocumentSize)
{
- if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
- {
- return Forbid();
- }
-
- if (Request.ContentLength > MaxDocumentSize)
- {
- // Manually validate to return proper status code.
- return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
- }
-
- var (clientName, clientVersion) = GetRequestInformation();
- var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
- .ConfigureAwait(false);
- return Ok(new ClientLogDocumentResponseDto(fileName));
+ // Manually validate to return proper status code.
+ return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
}
- private (string ClientName, string ClientVersion) GetRequestInformation()
- {
- var clientName = HttpContext.User.GetClient() ?? "unknown-client";
- var clientVersion = HttpContext.User.GetIsApiKey()
- ? "apikey"
- : HttpContext.User.GetVersion() ?? "unknown-version";
+ var (clientName, clientVersion) = GetRequestInformation();
+ var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
+ .ConfigureAwait(false);
+ return Ok(new ClientLogDocumentResponseDto(fileName));
+ }
- return (clientName, clientVersion);
- }
+ private (string ClientName, string ClientVersion) GetRequestInformation()
+ {
+ var clientName = HttpContext.User.GetClient() ?? "unknown-client";
+ var clientVersion = HttpContext.User.GetIsApiKey()
+ ? "apikey"
+ : HttpContext.User.GetVersion() ?? "unknown-version";
+
+ return (clientName, clientVersion);
}
}
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index effc9ed7a..2db04afb8 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -11,101 +11,100 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The collection controller.
+/// </summary>
+[Route("Collections")]
+[Authorize(Policy = Policies.CollectionManagement)]
+public class CollectionController : BaseJellyfinApiController
{
+ private readonly ICollectionManager _collectionManager;
+ private readonly IDtoService _dtoService;
+
/// <summary>
- /// The collection controller.
+ /// Initializes a new instance of the <see cref="CollectionController"/> class.
/// </summary>
- [Route("Collections")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class CollectionController : BaseJellyfinApiController
+ /// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param>
+ /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+ public CollectionController(
+ ICollectionManager collectionManager,
+ IDtoService dtoService)
{
- private readonly ICollectionManager _collectionManager;
- private readonly IDtoService _dtoService;
+ _collectionManager = collectionManager;
+ _dtoService = dtoService;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="CollectionController"/> class.
- /// </summary>
- /// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param>
- /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
- public CollectionController(
- ICollectionManager collectionManager,
- IDtoService dtoService)
- {
- _collectionManager = collectionManager;
- _dtoService = dtoService;
- }
+ /// <summary>
+ /// Creates a new collection.
+ /// </summary>
+ /// <param name="name">The name of the collection.</param>
+ /// <param name="ids">Item Ids to add to the collection.</param>
+ /// <param name="parentId">Optional. Create the collection within a specific folder.</param>
+ /// <param name="isLocked">Whether or not to lock the new collection.</param>
+ /// <response code="200">Collection created.</response>
+ /// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
+ [FromQuery] string? name,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
+ [FromQuery] Guid? parentId,
+ [FromQuery] bool isLocked = false)
+ {
+ var userId = User.GetUserId();
- /// <summary>
- /// Creates a new collection.
- /// </summary>
- /// <param name="name">The name of the collection.</param>
- /// <param name="ids">Item Ids to add to the collection.</param>
- /// <param name="parentId">Optional. Create the collection within a specific folder.</param>
- /// <param name="isLocked">Whether or not to lock the new collection.</param>
- /// <response code="200">Collection created.</response>
- /// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
- [HttpPost]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
- [FromQuery] string? name,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
- [FromQuery] Guid? parentId,
- [FromQuery] bool isLocked = false)
+ var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
- var userId = User.GetUserId();
-
- var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
- {
- IsLocked = isLocked,
- Name = name,
- ParentId = parentId,
- ItemIdList = ids,
- UserIds = new[] { userId }
- }).ConfigureAwait(false);
+ IsLocked = isLocked,
+ Name = name,
+ ParentId = parentId,
+ ItemIdList = ids,
+ UserIds = new[] { userId }
+ }).ConfigureAwait(false);
- var dtoOptions = new DtoOptions().AddClientFields(User);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
- var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
+ var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
- return new CollectionCreationResult
- {
- Id = dto.Id
- };
- }
-
- /// <summary>
- /// Adds items to a collection.
- /// </summary>
- /// <param name="collectionId">The collection id.</param>
- /// <param name="ids">Item ids, comma delimited.</param>
- /// <response code="204">Items added to collection.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("{collectionId}/Items")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddToCollection(
- [FromRoute, Required] Guid collectionId,
- [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ return new CollectionCreationResult
{
- await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
- return NoContent();
- }
+ Id = dto.Id
+ };
+ }
- /// <summary>
- /// Removes items from a collection.
- /// </summary>
- /// <param name="collectionId">The collection id.</param>
- /// <param name="ids">Item ids, comma delimited.</param>
- /// <response code="204">Items removed from collection.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpDelete("{collectionId}/Items")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromCollection(
- [FromRoute, Required] Guid collectionId,
- [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
- {
- await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
- return NoContent();
- }
+ /// <summary>
+ /// Adds items to a collection.
+ /// </summary>
+ /// <param name="collectionId">The collection id.</param>
+ /// <param name="ids">Item ids, comma delimited.</param>
+ /// <response code="204">Items added to collection.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("{collectionId}/Items")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> AddToCollection(
+ [FromRoute, Required] Guid collectionId,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ {
+ await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Removes items from a collection.
+ /// </summary>
+ /// <param name="collectionId">The collection id.</param>
+ /// <param name="ids">Item ids, comma delimited.</param>
+ /// <response code="204">Items removed from collection.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpDelete("{collectionId}/Items")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> RemoveFromCollection(
+ [FromRoute, Required] Guid collectionId,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ {
+ await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index a00ac1b0a..9007dfc41 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -13,124 +13,123 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Configuration Controller.
+/// </summary>
+[Route("System")]
+[Authorize]
+public class ConfigurationController : BaseJellyfinApiController
{
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+
+ private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
+
/// <summary>
- /// Configuration Controller.
+ /// Initializes a new instance of the <see cref="ConfigurationController"/> class.
/// </summary>
- [Route("System")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ConfigurationController : BaseJellyfinApiController
+ /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+ public ConfigurationController(
+ IServerConfigurationManager configurationManager,
+ IMediaEncoder mediaEncoder)
{
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IMediaEncoder _mediaEncoder;
+ _configurationManager = configurationManager;
+ _mediaEncoder = mediaEncoder;
+ }
- private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
+ /// <summary>
+ /// Gets application configuration.
+ /// </summary>
+ /// <response code="200">Application configuration returned.</response>
+ /// <returns>Application configuration.</returns>
+ [HttpGet("Configuration")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<ServerConfiguration> GetConfiguration()
+ {
+ return _configurationManager.Configuration;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ConfigurationController"/> class.
- /// </summary>
- /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- public ConfigurationController(
- IServerConfigurationManager configurationManager,
- IMediaEncoder mediaEncoder)
- {
- _configurationManager = configurationManager;
- _mediaEncoder = mediaEncoder;
- }
+ /// <summary>
+ /// Updates application configuration.
+ /// </summary>
+ /// <param name="configuration">Configuration.</param>
+ /// <response code="204">Configuration updated.</response>
+ /// <returns>Update status.</returns>
+ [HttpPost("Configuration")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration)
+ {
+ _configurationManager.ReplaceConfiguration(configuration);
+ return NoContent();
+ }
- /// <summary>
- /// Gets 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>
+ /// 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 application configuration.
- /// </summary>
- /// <param name="configuration">Configuration.</param>
- /// <response code="204">Configuration updated.</response>
- /// <returns>Update status.</returns>
- [HttpPost("Configuration")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration)
- {
- _configurationManager.ReplaceConfiguration(configuration);
- return NoContent();
- }
+ /// <summary>
+ /// 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);
- /// <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)
+ if (deserializedConfiguration is null)
{
- return _configurationManager.GetConfiguration(key);
+ throw new ArgumentException("Body doesn't contain a valid configuration");
}
- /// <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();
- }
+ _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>
+ /// Gets a default MetadataOptions object.
+ /// </summary>
+ /// <response code="200">Metadata options returned.</response>
+ /// <returns>Default MetadataOptions.</returns>
+ [HttpGet("Configuration/MetadataOptions/Default")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<MetadataOptions> GetDefaultMetadataOptions()
+ {
+ return new MetadataOptions();
+ }
- /// <summary>
- /// Updates the path to the media encoder.
- /// </summary>
- /// <param name="mediaEncoderPath">Media encoder path form body.</param>
- /// <response code="204">Media encoder path updated.</response>
- /// <returns>Status.</returns>
- [HttpPost("MediaEncoder/Path")]
- [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
- {
- _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
- return NoContent();
- }
+ /// <summary>
+ /// Updates the path to the media encoder.
+ /// </summary>
+ /// <param name="mediaEncoderPath">Media encoder path form body.</param>
+ /// <response code="204">Media encoder path updated.</response>
+ /// <returns>Status.</returns>
+ [HttpPost("MediaEncoder/Path")]
+ [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
+ {
+ _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 3894e6c5f..076084c7a 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Net.Mime;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Models;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Net;
@@ -14,103 +13,102 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The dashboard controller.
+/// </summary>
+[Route("")]
+public class DashboardController : BaseJellyfinApiController
{
+ private readonly ILogger<DashboardController> _logger;
+ private readonly IPluginManager _pluginManager;
+
/// <summary>
- /// The dashboard controller.
+ /// Initializes a new instance of the <see cref="DashboardController"/> class.
/// </summary>
- [Route("")]
- public class DashboardController : BaseJellyfinApiController
+ /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
+ /// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
+ public DashboardController(
+ ILogger<DashboardController> logger,
+ IPluginManager pluginManager)
{
- private readonly ILogger<DashboardController> _logger;
- private readonly IPluginManager _pluginManager;
+ _logger = logger;
+ _pluginManager = pluginManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="DashboardController"/> class.
- /// </summary>
- /// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
- /// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
- public DashboardController(
- ILogger<DashboardController> logger,
- IPluginManager pluginManager)
- {
- _logger = logger;
- _pluginManager = pluginManager;
- }
+ /// <summary>
+ /// Gets the configuration pages.
+ /// </summary>
+ /// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
+ /// <response code="200">ConfigurationPages returned.</response>
+ /// <response code="404">Server still loading.</response>
+ /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
+ [HttpGet("web/ConfigurationPages")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Authorize]
+ public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
+ [FromQuery] bool? enableInMainMenu)
+ {
+ var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
- /// <summary>
- /// Gets the configuration pages.
- /// </summary>
- /// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
- /// <response code="200">ConfigurationPages returned.</response>
- /// <response code="404">Server still loading.</response>
- /// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
- [HttpGet("web/ConfigurationPages")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
- [FromQuery] bool? enableInMainMenu)
+ if (enableInMainMenu.HasValue)
{
- var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
-
- if (enableInMainMenu.HasValue)
- {
- configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
- }
-
- return configPages;
+ configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
}
- /// <summary>
- /// Gets a dashboard configuration page.
- /// </summary>
- /// <param name="name">The name of the page.</param>
- /// <response code="200">ConfigurationPage returned.</response>
- /// <response code="404">Plugin configuration page not found.</response>
- /// <returns>The configuration page.</returns>
- [HttpGet("web/ConfigurationPage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
- public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
- {
- var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
- if (altPage is null)
- {
- 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 configPages;
+ }
- return File(stream, MimeTypes.GetMimeType(resourcePath));
+ /// <summary>
+ /// Gets a dashboard configuration page.
+ /// </summary>
+ /// <param name="name">The name of the page.</param>
+ /// <response code="200">ConfigurationPage returned.</response>
+ /// <response code="404">Plugin configuration page not found.</response>
+ /// <returns>The configuration page.</returns>
+ [HttpGet("web/ConfigurationPage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
+ public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
+ {
+ var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
+ if (altPage is null)
+ {
+ return NotFound();
}
- private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
+ IPlugin plugin = altPage.Item2;
+ string resourcePath = altPage.Item1.EmbeddedResourcePath;
+ Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath);
+ if (stream is null)
{
- return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
+ _logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name);
+ return NotFound();
}
- private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin)
- {
- if (plugin.Instance is not IHasWebPages hasWebPages)
- {
- return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>();
- }
+ return File(stream, MimeTypes.GetMimeType(resourcePath));
+ }
- return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
- }
+ private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
+ {
+ return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
+ }
- private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
+ private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin)
+ {
+ if (plugin.Instance is not IHasWebPages hasWebPages)
{
- return _pluginManager.Plugins.SelectMany(GetPluginPages);
+ 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);
}
}
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index aad60cf5c..497862236 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -13,129 +13,128 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Devices Controller.
+/// </summary>
+[Authorize(Policy = Policies.RequiresElevation)]
+public class DevicesController : BaseJellyfinApiController
{
+ private readonly IDeviceManager _deviceManager;
+ private readonly ISessionManager _sessionManager;
+
/// <summary>
- /// Devices Controller.
+ /// Initializes a new instance of the <see cref="DevicesController"/> class.
/// </summary>
- [Authorize(Policy = Policies.RequiresElevation)]
- public class DevicesController : BaseJellyfinApiController
+ /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
+ public DevicesController(
+ IDeviceManager deviceManager,
+ ISessionManager sessionManager)
{
- private readonly IDeviceManager _deviceManager;
- private readonly ISessionManager _sessionManager;
+ _deviceManager = deviceManager;
+ _sessionManager = sessionManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="DevicesController"/> class.
- /// </summary>
- /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
- /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
- public DevicesController(
- IDeviceManager deviceManager,
- ISessionManager sessionManager)
- {
- _deviceManager = deviceManager;
- _sessionManager = sessionManager;
- }
+ /// <summary>
+ /// Get Devices.
+ /// </summary>
+ /// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
+ /// <param name="userId">Gets or sets the user identifier.</param>
+ /// <response code="200">Devices retrieved.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+ {
+ return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
+ }
- /// <summary>
- /// Get Devices.
- /// </summary>
- /// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
- /// <param name="userId">Gets or sets the user identifier.</param>
- /// <response code="200">Devices retrieved.</response>
- /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
+ /// <summary>
+ /// Get info for a device.
+ /// </summary>
+ /// <param name="id">Device Id.</param>
+ /// <response code="200">Device info retrieved.</response>
+ /// <response code="404">Device not found.</response>
+ /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+ [HttpGet("Info")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
+ {
+ var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
+ if (deviceInfo is null)
{
- return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
+ return NotFound();
}
- /// <summary>
- /// Get info for a device.
- /// </summary>
- /// <param name="id">Device Id.</param>
- /// <response code="200">Device info retrieved.</response>
- /// <response code="404">Device not found.</response>
- /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
- [HttpGet("Info")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
- {
- var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
- if (deviceInfo is null)
- {
- return NotFound();
- }
+ return deviceInfo;
+ }
- return deviceInfo;
+ /// <summary>
+ /// 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();
}
- /// <summary>
- /// Get options for a device.
- /// </summary>
- /// <param name="id">Device Id.</param>
- /// <response code="200">Device options retrieved.</response>
- /// <response code="404">Device not found.</response>
- /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
- [HttpGet("Options")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
- {
- var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
- if (deviceInfo is null)
- {
- return NotFound();
- }
+ return deviceInfo;
+ }
- return deviceInfo;
- }
+ /// <summary>
+ /// Update device options.
+ /// </summary>
+ /// <param name="id">Device Id.</param>
+ /// <param name="deviceOptions">Device Options.</param>
+ /// <response code="204">Device options updated.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Options")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> UpdateDeviceOptions(
+ [FromQuery, Required] string id,
+ [FromBody, Required] DeviceOptionsDto deviceOptions)
+ {
+ await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// 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)
+ /// <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)
{
- await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false);
- return NoContent();
+ return NotFound();
}
- /// <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);
- }
+ var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
- return NoContent();
+ foreach (var session in sessions.Items)
+ {
+ await _sessionManager.Logout(session).ConfigureAwait(false);
}
+
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 67cceb4a8..6f0006832 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
@@ -14,201 +13,200 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Display Preferences Controller.
+/// </summary>
+[Authorize]
+public class DisplayPreferencesController : BaseJellyfinApiController
{
+ private readonly IDisplayPreferencesManager _displayPreferencesManager;
+ private readonly ILogger<DisplayPreferencesController> _logger;
+
/// <summary>
- /// Display Preferences Controller.
+ /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class DisplayPreferencesController : BaseJellyfinApiController
+ /// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
+ /// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
+ public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
{
- private readonly IDisplayPreferencesManager _displayPreferencesManager;
- private readonly ILogger<DisplayPreferencesController> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
- /// </summary>
- /// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
- /// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
- public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
+ _displayPreferencesManager = displayPreferencesManager;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Get Display Preferences.
+ /// </summary>
+ /// <param name="displayPreferencesId">Display preferences id.</param>
+ /// <param name="userId">User id.</param>
+ /// <param name="client">Client.</param>
+ /// <response code="200">Display preferences retrieved.</response>
+ /// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
+ [HttpGet("{displayPreferencesId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
+ public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
+ [FromRoute, Required] string displayPreferencesId,
+ [FromQuery, Required] Guid userId,
+ [FromQuery, Required] string client)
+ {
+ if (!Guid.TryParse(displayPreferencesId, out var itemId))
{
- _displayPreferencesManager = displayPreferencesManager;
- _logger = logger;
+ itemId = displayPreferencesId.GetMD5();
}
- /// <summary>
- /// Get Display Preferences.
- /// </summary>
- /// <param name="displayPreferencesId">Display preferences id.</param>
- /// <param name="userId">User id.</param>
- /// <param name="client">Client.</param>
- /// <response code="200">Display preferences retrieved.</response>
- /// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
- [HttpGet("{displayPreferencesId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
- public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
- [FromRoute, Required] string displayPreferencesId,
- [FromQuery, Required] Guid userId,
- [FromQuery, Required] string client)
- {
- if (!Guid.TryParse(displayPreferencesId, out var itemId))
- {
- itemId = displayPreferencesId.GetMD5();
- }
+ var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
+ var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
+ itemPreferences.ItemId = itemId;
- var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
- var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
- itemPreferences.ItemId = itemId;
+ var dto = new DisplayPreferencesDto
+ {
+ Client = displayPreferences.Client,
+ Id = displayPreferences.ItemId.ToString(),
+ SortBy = itemPreferences.SortBy,
+ SortOrder = itemPreferences.SortOrder,
+ IndexBy = displayPreferences.IndexBy?.ToString(),
+ RememberIndexing = itemPreferences.RememberIndexing,
+ RememberSorting = itemPreferences.RememberSorting,
+ ScrollDirection = displayPreferences.ScrollDirection,
+ ShowBackdrop = displayPreferences.ShowBackdrop,
+ ShowSidebar = displayPreferences.ShowSidebar
+ };
+
+ foreach (var homeSection in displayPreferences.HomeSections)
+ {
+ dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
+ }
- 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;
- 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);
+ }
- // 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();
- // This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
- _displayPreferencesManager.SaveChanges();
+ return dto;
+ }
- 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();
}
- /// <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)
+ 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)))
{
- 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))
+ var order = int.Parse(key.AsSpan().Slice("homesection".Length), CultureInfo.InvariantCulture);
+ if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
{
- itemId = displayPreferencesId.GetMD5();
+ type = order < 8 ? defaults[order] : HomeSectionType.None;
}
- var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
- existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
- existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
- existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
-
- 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))
- {
- type = order < 8 ? defaults[order] : HomeSectionType.None;
- }
-
- displayPreferences.CustomPrefs.Remove(key);
- existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
- }
+ 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)))
+ 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))
{
- if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
- {
- _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
- displayPreferences.CustomPrefs.Remove(key);
- }
+ _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;
+ 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();
+ // Set all remaining custom preferences.
+ _displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
+ _displayPreferencesManager.SaveChanges();
- return NoContent();
- }
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs
index 07e0590a1..415385463 100644
--- a/Jellyfin.Api/Controllers/DlnaController.cs
+++ b/Jellyfin.Api/Controllers/DlnaController.cs
@@ -7,127 +7,126 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Dlna Controller.
+/// </summary>
+[Authorize(Policy = Policies.RequiresElevation)]
+public class DlnaController : BaseJellyfinApiController
{
+ private readonly IDlnaManager _dlnaManager;
+
/// <summary>
- /// Dlna Controller.
+ /// Initializes a new instance of the <see cref="DlnaController"/> class.
/// </summary>
- [Authorize(Policy = Policies.RequiresElevation)]
- public class DlnaController : BaseJellyfinApiController
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ public DlnaController(IDlnaManager dlnaManager)
{
- private readonly IDlnaManager _dlnaManager;
+ _dlnaManager = dlnaManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="DlnaController"/> class.
- /// </summary>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- public DlnaController(IDlnaManager dlnaManager)
- {
- _dlnaManager = dlnaManager;
- }
+ /// <summary>
+ /// Get profile infos.
+ /// </summary>
+ /// <response code="200">Device profile infos returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns>
+ [HttpGet("ProfileInfos")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos()
+ {
+ return Ok(_dlnaManager.GetProfileInfos());
+ }
- /// <summary>
- /// Get profile infos.
- /// </summary>
- /// <response code="200">Device profile infos returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns>
- [HttpGet("ProfileInfos")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos()
- {
- return Ok(_dlnaManager.GetProfileInfos());
- }
+ /// <summary>
+ /// Gets the default profile.
+ /// </summary>
+ /// <response code="200">Default device profile returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the default profile.</returns>
+ [HttpGet("Profiles/Default")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<DeviceProfile> GetDefaultProfile()
+ {
+ return _dlnaManager.GetDefaultProfile();
+ }
- /// <summary>
- /// Gets 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()
+ /// <summary>
+ /// Gets a single profile.
+ /// </summary>
+ /// <param name="profileId">Profile Id.</param>
+ /// <response code="200">Device profile returned.</response>
+ /// <response code="404">Device profile not found.</response>
+ /// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns>
+ [HttpGet("Profiles/{profileId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId)
+ {
+ var profile = _dlnaManager.GetProfile(profileId);
+ if (profile is null)
{
- return _dlnaManager.GetDefaultProfile();
+ return NotFound();
}
- /// <summary>
- /// Gets a single profile.
- /// </summary>
- /// <param name="profileId">Profile Id.</param>
- /// <response code="200">Device profile returned.</response>
- /// <response code="404">Device profile not found.</response>
- /// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns>
- [HttpGet("Profiles/{profileId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId)
- {
- var profile = _dlnaManager.GetProfile(profileId);
- if (profile is null)
- {
- return NotFound();
- }
+ return profile;
+ }
- 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();
}
- /// <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();
+ }
- _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>
- /// 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)
+ /// <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)
{
- _dlnaManager.CreateProfile(deviceProfile);
- return NoContent();
+ return NotFound();
}
- /// <summary>
- /// Updates a profile.
- /// </summary>
- /// <param name="profileId">Profile id.</param>
- /// <param name="deviceProfile">Device profile.</param>
- /// <response code="204">Device profile updated.</response>
- /// <response code="404">Device profile not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
- [HttpPost("Profiles/{profileId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile)
- {
- var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
- if (existingDeviceProfile is null)
- {
- return NotFound();
- }
-
- _dlnaManager.UpdateProfile(profileId, deviceProfile);
- return NoContent();
- }
+ _dlnaManager.UpdateProfile(profileId, deviceProfile);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 96c492b3e..95b296fae 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -14,311 +14,310 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Dlna Server Controller.
+/// </summary>
+[Route("Dlna")]
+[DlnaEnabled]
+[Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
+public class DlnaServerController : BaseJellyfinApiController
{
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IContentDirectory _contentDirectory;
+ private readonly IConnectionManager _connectionManager;
+ private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
+
/// <summary>
- /// Dlna Server Controller.
+ /// Initializes a new instance of the <see cref="DlnaServerController"/> class.
/// </summary>
- [Route("Dlna")]
- [DlnaEnabled]
- [Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
- public class DlnaServerController : BaseJellyfinApiController
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
+ public DlnaServerController(IDlnaManager dlnaManager)
{
- private readonly IDlnaManager _dlnaManager;
- private readonly IContentDirectory _contentDirectory;
- private readonly IConnectionManager _connectionManager;
- private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
+ _dlnaManager = dlnaManager;
+ _contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
+ _connectionManager = DlnaEntryPoint.Current.ConnectionManager;
+ _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="DlnaServerController"/> class.
- /// </summary>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- public DlnaServerController(IDlnaManager dlnaManager)
- {
- _dlnaManager = dlnaManager;
- _contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
- _connectionManager = DlnaEntryPoint.Current.ConnectionManager;
- _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
- }
+ /// <summary>
+ /// Get Description Xml.
+ /// </summary>
+ /// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Description xml returned.</response>
+ /// <response code="503">DLNA is disabled.</response>
+ /// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
+ [HttpGet("{serverId}/description")]
+ [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
+ public ActionResult<string> GetDescriptionXml([FromRoute, Required] string serverId)
+ {
+ var url = GetAbsoluteUri();
+ var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
+ var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
+ return Ok(xml);
+ }
- /// <summary>
- /// Get Description Xml.
- /// </summary>
- /// <param name="serverId">Server UUID.</param>
- /// <response code="200">Description xml returned.</response>
- /// <response code="503">DLNA is disabled.</response>
- /// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
- [HttpGet("{serverId}/description")]
- [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
- [Produces(MediaTypeNames.Text.Xml)]
- [ProducesFile(MediaTypeNames.Text.Xml)]
- public ActionResult<string> GetDescriptionXml([FromRoute, Required] string serverId)
- {
- var url = GetAbsoluteUri();
- var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
- var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
- return Ok(xml);
- }
+ /// <summary>
+ /// Gets Dlna content directory xml.
+ /// </summary>
+ /// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Dlna content directory returned.</response>
+ /// <response code="503">DLNA is disabled.</response>
+ /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
+ [HttpGet("{serverId}/ContentDirectory")]
+ [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
+ [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
+ public ActionResult<string> GetContentDirectory([FromRoute, Required] string serverId)
+ {
+ return Ok(_contentDirectory.GetServiceXml());
+ }
- /// <summary>
- /// Gets Dlna 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}/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>
- /// 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 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 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>
- /// 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}/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}/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>
- /// 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="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);
+ }
- /// <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)
+ private ActionResult GetIconInternal(string fileName)
+ {
+ var icon = _dlnaManager.GetIcon(fileName);
+ if (icon is null)
{
- return GetIconInternal(fileName);
+ return NotFound();
}
- private ActionResult GetIconInternal(string fileName)
- {
- var icon = _dlnaManager.GetIcon(fileName);
- if (icon is null)
- {
- return NotFound();
- }
+ return File(icon.Stream, MimeTypes.GetMimeType(fileName));
+ }
- return File(icon.Stream, MimeTypes.GetMimeType(fileName));
- }
+ private string GetAbsoluteUri()
+ {
+ return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
+ }
- private string GetAbsoluteUri()
+ private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
+ {
+ return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers)
{
- return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
- }
+ InputXml = requestStream,
+ TargetServerUuId = id,
+ RequestedUrl = GetAbsoluteUri()
+ });
+ }
- private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
+ private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager)
+ {
+ var subscriptionId = Request.Headers["SID"];
+ if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
{
- return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers)
- {
- InputXml = requestStream,
- TargetServerUuId = id,
- RequestedUrl = GetAbsoluteUri()
- });
- }
+ var notificationType = Request.Headers["NT"];
+ var callback = Request.Headers["CALLBACK"];
+ var timeoutString = Request.Headers["TIMEOUT"];
- private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager)
- {
- var subscriptionId = Request.Headers["SID"];
- if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
+ if (string.IsNullOrEmpty(notificationType))
{
- 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.RenewEventSubscription(
+ subscriptionId,
+ notificationType,
+ timeoutString,
+ callback);
}
- return dlnaEventManager.CancelEventSubscription(subscriptionId);
+ return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback);
}
+
+ return dlnaEventManager.CancelEventSubscription(subscriptionId);
}
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index b41e23925..4d8b4de24 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -9,7 +9,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
@@ -30,2026 +29,2025 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Dynamic hls controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class DynamicHlsController : BaseJellyfinApiController
{
+ private const string DefaultVodEncoderPreset = "veryfast";
+ private const string DefaultEventEncoderPreset = "superfast";
+ private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
+
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ILogger<DynamicHlsController> _logger;
+ private readonly EncodingHelper _encodingHelper;
+ private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator;
+ private readonly DynamicHlsHelper _dynamicHlsHelper;
+ private readonly EncodingOptions _encodingOptions;
+
/// <summary>
- /// Dynamic hls controller.
+ /// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class DynamicHlsController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> 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="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
+ /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
+ /// <param name="dynamicHlsPlaylistGenerator">Instance of <see cref="IDynamicHlsPlaylistGenerator"/>.</param>
+ public DynamicHlsController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDlnaManager dlnaManager,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ ILogger<DynamicHlsController> logger,
+ DynamicHlsHelper dynamicHlsHelper,
+ EncodingHelper encodingHelper,
+ IDynamicHlsPlaylistGenerator dynamicHlsPlaylistGenerator)
{
- private const string DefaultVodEncoderPreset = "veryfast";
- private const string DefaultEventEncoderPreset = "superfast";
- private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
-
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDlnaManager _dlnaManager;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly IDeviceManager _deviceManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly ILogger<DynamicHlsController> _logger;
- private readonly EncodingHelper _encodingHelper;
- private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator;
- private readonly DynamicHlsHelper _dynamicHlsHelper;
- private readonly EncodingOptions _encodingOptions;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> 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="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
- /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
- /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
- /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
- /// <param name="dynamicHlsPlaylistGenerator">Instance of <see cref="IDynamicHlsPlaylistGenerator"/>.</param>
- public DynamicHlsController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDlnaManager dlnaManager,
- IMediaSourceManager mediaSourceManager,
- IServerConfigurationManager serverConfigurationManager,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- IDeviceManager deviceManager,
- TranscodingJobHelper transcodingJobHelper,
- ILogger<DynamicHlsController> logger,
- DynamicHlsHelper dynamicHlsHelper,
- EncodingHelper encodingHelper,
- IDynamicHlsPlaylistGenerator dynamicHlsPlaylistGenerator)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dlnaManager = dlnaManager;
- _mediaSourceManager = mediaSourceManager;
- _serverConfigurationManager = serverConfigurationManager;
- _mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _deviceManager = deviceManager;
- _transcodingJobHelper = transcodingJobHelper;
- _logger = logger;
- _dynamicHlsHelper = dynamicHlsHelper;
- _encodingHelper = encodingHelper;
- _dynamicHlsPlaylistGenerator = dynamicHlsPlaylistGenerator;
-
- _encodingOptions = serverConfigurationManager.GetEncodingOptions();
- }
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dlnaManager = dlnaManager;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _logger = logger;
+ _dynamicHlsHelper = dynamicHlsHelper;
+ _encodingHelper = encodingHelper;
+ _dynamicHlsPlaylistGenerator = dynamicHlsPlaylistGenerator;
+
+ _encodingOptions = serverConfigurationManager.GetEncodingOptions();
+ }
- /// <summary>
- /// Gets a hls live stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="container">The audio container.</param>
- /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
- /// <param name="params">The streaming parameters.</param>
- /// <param name="tag">The tag.</param>
- /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
- /// <param name="playSessionId">The play session id.</param>
- /// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment length.</param>
- /// <param name="minSegments">The minimum number of segments.</param>
- /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
- /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
- /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
- /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
- /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
- /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
- /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
- /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
- /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
- /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
- /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
- /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
- /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
- /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
- /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
- /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
- /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
- /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
- /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <param name="maxRefFrames">Optional.</param>
- /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
- /// <param name="requireAvc">Optional. Whether to require avc.</param>
- /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
- /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
- /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
- /// <param name="liveStreamId">The live stream id.</param>
- /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
- /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
- /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
- /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
- /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
- /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
- /// <param name="streamOptions">Optional. The streaming options.</param>
- /// <param name="maxWidth">Optional. The max width.</param>
- /// <param name="maxHeight">Optional. The max height.</param>
- /// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param>
- /// <response code="200">Hls live stream retrieved.</response>
- /// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
- [HttpGet("Videos/{itemId}/live.m3u8")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- public async Task<ActionResult> GetLiveHlsStream(
- [FromRoute, Required] Guid itemId,
- [FromQuery] string? container,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] bool? enableSubtitlesInManifest)
+ /// <summary>
+ /// Gets a hls live stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The audio container.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <param name="segmentContainer">The segment container.</param>
+ /// <param name="segmentLength">The segment length.</param>
+ /// <param name="minSegments">The minimum number of segments.</param>
+ /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+ /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+ /// <param name="requireAvc">Optional. Whether to require avc.</param>
+ /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
+ /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+ /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+ /// <param name="liveStreamId">The live stream id.</param>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="maxWidth">Optional. The max width.</param>
+ /// <param name="maxHeight">Optional. The max height.</param>
+ /// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param>
+ /// <response code="200">Hls live stream retrieved.</response>
+ /// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
+ [HttpGet("Videos/{itemId}/live.m3u8")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetLiveHlsStream(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] string? container,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] bool? enableSubtitlesInManifest)
+ {
+ VideoRequestDto streamingRequest = new VideoRequestDto
{
- VideoRequestDto streamingRequest = new VideoRequestDto
- {
- Id = itemId,
- Container = container,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions,
- MaxHeight = maxHeight,
- MaxWidth = maxWidth,
- EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
- };
-
- // CTS lifecycle is managed internally.
- var cancellationTokenSource = new CancellationTokenSource();
- // Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
- // since it gets disposed when ffmpeg exits
- var cancellationToken = cancellationTokenSource.Token;
- var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- TranscodingJobType,
- cancellationToken)
- .ConfigureAwait(false);
-
- TranscodingJobDto? job = null;
- var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
-
- if (!System.IO.File.Exists(playlistPath))
+ Id = itemId,
+ Container = container,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions,
+ MaxHeight = maxHeight,
+ MaxWidth = maxWidth,
+ EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
+ };
+
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
+ // Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
+ // since it gets disposed when ffmpeg exits
+ var cancellationToken = cancellationTokenSource.Token;
+ var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ TranscodingJobType,
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ TranscodingJobDto? job = null;
+ var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
+
+ if (!System.IO.File.Exists(playlistPath))
+ {
+ var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
+ await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
{
- var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
+ if (!System.IO.File.Exists(playlistPath))
{
- if (!System.IO.File.Exists(playlistPath))
+ // If the playlist doesn't already exist, startup ffmpeg
+ try
{
- // If the playlist doesn't already exist, startup ffmpeg
- try
- {
- job = await _transcodingJobHelper.StartFfMpeg(
- state,
- playlistPath,
- GetCommandLineArguments(playlistPath, state, true, 0),
- Request,
- TranscodingJobType,
- cancellationTokenSource)
- .ConfigureAwait(false);
- job.IsLiveOutput = true;
- }
- catch
- {
- state.Dispose();
- throw;
- }
+ job = await _transcodingJobHelper.StartFfMpeg(
+ state,
+ playlistPath,
+ GetCommandLineArguments(playlistPath, state, true, 0),
+ Request,
+ TranscodingJobType,
+ cancellationTokenSource)
+ .ConfigureAwait(false);
+ job.IsLiveOutput = true;
+ }
+ catch
+ {
+ state.Dispose();
+ throw;
+ }
- minSegments = state.MinSegments;
- if (minSegments > 0)
- {
- await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false);
- }
+ minSegments = state.MinSegments;
+ if (minSegments > 0)
+ {
+ await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false);
}
}
- finally
- {
- transcodingLock.Release();
- }
}
-
- job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
-
- if (job is not null)
+ finally
{
- _transcodingJobHelper.OnTranscodeEndRequest(job);
+ transcodingLock.Release();
}
-
- var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
-
- return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
}
- /// <summary>
- /// Gets a video hls playlist stream.
- /// </summary>
- /// <param name="itemId">The item id.</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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
- /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <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>
- /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
- /// <response code="200">Video stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
- [HttpGet("Videos/{itemId}/master.m3u8")]
- [HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- public async Task<ActionResult> GetMasterHlsVideoPlaylist(
- [FromRoute, Required] Guid itemId,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery, Required] string mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true)
- {
- var streamingRequest = new HlsVideoRequestDto
- {
- Id = itemId,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- MaxWidth = maxWidth,
- MaxHeight = maxHeight,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions,
- EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
- };
+ job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ if (job is not null)
+ {
+ _transcodingJobHelper.OnTranscodeEndRequest(job);
}
- /// <summary>
- /// Gets an audio hls playlist stream.
- /// </summary>
- /// <param name="itemId">The item id.</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="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="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>
- /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
- /// <response code="200">Audio stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
- [HttpGet("Audio/{itemId}/master.m3u8")]
- [HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- public async Task<ActionResult> GetMasterHlsAudioPlaylist(
- [FromRoute, Required] Guid itemId,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery, Required] string mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? maxStreamingBitrate,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true)
- {
- var streamingRequest = new HlsAudioRequestDto
- {
- Id = itemId,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate ?? maxStreamingBitrate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions,
- EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
- };
+ var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
- return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
- }
+ return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
+ }
- /// <summary>
- /// Gets a video stream using HTTP live streaming.
- /// </summary>
- /// <param name="itemId">The item id.</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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
- /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <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">Video stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("Videos/{itemId}/main.m3u8")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- public async Task<ActionResult> GetVariantHlsVideoPlaylist(
- [FromRoute, Required] Guid itemId,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ /// <summary>
+ /// Gets a video hls playlist stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <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>
+ /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+ /// <response code="200">Video stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
+ [HttpGet("Videos/{itemId}/master.m3u8")]
+ [HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetMasterHlsVideoPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery, Required] string mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+ {
+ var streamingRequest = new HlsVideoRequestDto
{
- using var cancellationTokenSource = new CancellationTokenSource();
- var streamingRequest = new VideoRequestDto
- {
- Id = itemId,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- MaxWidth = maxWidth,
- MaxHeight = maxHeight,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
- };
-
- return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
- .ConfigureAwait(false);
- }
+ Id = itemId,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions,
+ EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+ };
+
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets an audio stream using HTTP live streaming.
- /// </summary>
- /// <param name="itemId">The item id.</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="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="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, vpx, 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("Audio/{itemId}/main.m3u8")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- public async Task<ActionResult> GetVariantHlsAudioPlaylist(
- [FromRoute, Required] Guid itemId,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? maxStreamingBitrate,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ /// <summary>
+ /// Gets an audio hls playlist stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</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="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="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>
+ /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+ /// <response code="200">Audio stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
+ [HttpGet("Audio/{itemId}/master.m3u8")]
+ [HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetMasterHlsAudioPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery, Required] string mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+ {
+ var streamingRequest = new HlsAudioRequestDto
{
- using var cancellationTokenSource = new CancellationTokenSource();
- var streamingRequest = new StreamingRequestDto
- {
- Id = itemId,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate ?? maxStreamingBitrate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
- };
+ Id = itemId,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions,
+ EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+ };
+
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ }
- return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
- .ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets a video stream using HTTP live streaming.
+ /// </summary>
+ /// <param name="itemId">The item id.</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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <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">Video stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("Videos/{itemId}/main.m3u8")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetVariantHlsVideoPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ using var cancellationTokenSource = new CancellationTokenSource();
+ var streamingRequest = new VideoRequestDto
+ {
+ Id = itemId,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions
+ };
+
+ return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
+ .ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets a video stream using HTTP live streaming.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="playlistId">The playlist id.</param>
- /// <param name="segmentId">The segment id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
- /// <param name="runtimeTicks">The position of the requested segment in ticks.</param>
- /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</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 desired 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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
- /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <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">Video stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesVideoFile]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> GetHlsVideoSegment(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string playlistId,
- [FromRoute, Required] int segmentId,
- [FromRoute, Required] string container,
- [FromQuery, Required] long runtimeTicks,
- [FromQuery, Required] long actualSegmentLengthTicks,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ /// <summary>
+ /// Gets an audio stream using HTTP live streaming.
+ /// </summary>
+ /// <param name="itemId">The item id.</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="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="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, vpx, 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("Audio/{itemId}/main.m3u8")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
+ public async Task<ActionResult> GetVariantHlsAudioPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ using var cancellationTokenSource = new CancellationTokenSource();
+ var streamingRequest = new StreamingRequestDto
{
- var streamingRequest = new VideoRequestDto
- {
- Id = itemId,
- CurrentRuntimeTicks = runtimeTicks,
- ActualSegmentLengthTicks = actualSegmentLengthTicks,
- Container = container,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- MaxWidth = maxWidth,
- MaxHeight = maxHeight,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
- };
+ Id = itemId,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions
+ };
+
+ return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
+ .ConfigureAwait(false);
+ }
- return await GetDynamicSegment(streamingRequest, segmentId)
- .ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets a video stream using HTTP live streaming.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="segmentId">The segment id.</param>
+ /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+ /// <param name="runtimeTicks">The position of the requested segment in ticks.</param>
+ /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</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 desired 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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <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">Video stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> GetHlsVideoSegment(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] int segmentId,
+ [FromRoute, Required] string container,
+ [FromQuery, Required] long runtimeTicks,
+ [FromQuery, Required] long actualSegmentLengthTicks,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ var streamingRequest = new VideoRequestDto
+ {
+ Id = itemId,
+ CurrentRuntimeTicks = runtimeTicks,
+ ActualSegmentLengthTicks = actualSegmentLengthTicks,
+ Container = container,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions
+ };
+
+ return await GetDynamicSegment(streamingRequest, segmentId)
+ .ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets a video stream using HTTP live streaming.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="playlistId">The playlist id.</param>
- /// <param name="segmentId">The segment id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
- /// <param name="runtimeTicks">The position of the requested segment in ticks.</param>
- /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</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="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="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, vpx, 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">Video stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesAudioFile]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> GetHlsAudioSegment(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string playlistId,
- [FromRoute, Required] int segmentId,
- [FromRoute, Required] string container,
- [FromQuery, Required] long runtimeTicks,
- [FromQuery, Required] long actualSegmentLengthTicks,
- [FromQuery] bool? @static,
- [FromQuery] string? @params,
- [FromQuery] string? tag,
- [FromQuery] string? deviceProfileId,
- [FromQuery] string? playSessionId,
- [FromQuery] string? segmentContainer,
- [FromQuery] int? segmentLength,
- [FromQuery] int? minSegments,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? deviceId,
- [FromQuery] string? audioCodec,
- [FromQuery] bool? enableAutoStreamCopy,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
- [FromQuery] bool? breakOnNonKeyFrames,
- [FromQuery] int? audioSampleRate,
- [FromQuery] int? maxAudioBitDepth,
- [FromQuery] int? maxStreamingBitrate,
- [FromQuery] int? audioBitRate,
- [FromQuery] int? audioChannels,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? profile,
- [FromQuery] string? level,
- [FromQuery] float? framerate,
- [FromQuery] float? maxFramerate,
- [FromQuery] bool? copyTimestamps,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ /// <summary>
+ /// Gets a video stream using HTTP live streaming.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="segmentId">The segment id.</param>
+ /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+ /// <param name="runtimeTicks">The position of the requested segment in ticks.</param>
+ /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</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="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="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, vpx, 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">Video stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> GetHlsAudioSegment(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] int segmentId,
+ [FromRoute, Required] string container,
+ [FromQuery, Required] long runtimeTicks,
+ [FromQuery, Required] long actualSegmentLengthTicks,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ var streamingRequest = new StreamingRequestDto
{
- var streamingRequest = new StreamingRequestDto
- {
- Id = itemId,
- Container = container,
- CurrentRuntimeTicks = runtimeTicks,
- ActualSegmentLengthTicks = actualSegmentLengthTicks,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate ?? maxStreamingBitrate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
- };
+ Id = itemId,
+ Container = container,
+ CurrentRuntimeTicks = runtimeTicks,
+ ActualSegmentLengthTicks = actualSegmentLengthTicks,
+ Static = @static ?? false,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? false,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions
+ };
+
+ return await GetDynamicSegment(streamingRequest, segmentId)
+ .ConfigureAwait(false);
+ }
- return await GetDynamicSegment(streamingRequest, segmentId)
- .ConfigureAwait(false);
- }
+ private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, CancellationTokenSource cancellationTokenSource)
+ {
+ using var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ TranscodingJobType,
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
+
+ var request = new CreateMainPlaylistRequest(
+ state.MediaPath,
+ state.SegmentLength * 1000,
+ state.RunTimeTicks ?? 0,
+ state.Request.SegmentContainer ?? string.Empty,
+ "hls1/main/",
+ Request.QueryString.ToString(),
+ EncodingHelper.IsCopyCodec(state.OutputVideoCodec));
+ var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
+
+ return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
- private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, CancellationTokenSource cancellationTokenSource)
+ private async Task<ActionResult> GetDynamicSegment(StreamingRequestDto streamingRequest, int segmentId)
+ {
+ if ((streamingRequest.StartTimeTicks ?? 0) > 0)
{
- using var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- TranscodingJobType,
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
-
- var request = new CreateMainPlaylistRequest(
- state.MediaPath,
- state.SegmentLength * 1000,
- state.RunTimeTicks ?? 0,
- state.Request.SegmentContainer ?? string.Empty,
- "hls1/main/",
- Request.QueryString.ToString(),
- EncodingHelper.IsCopyCodec(state.OutputVideoCodec));
- var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
-
- return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
+ throw new ArgumentException("StartTimeTicks is not allowed.");
}
- private async Task<ActionResult> GetDynamicSegment(StreamingRequestDto streamingRequest, int segmentId)
- {
- if ((streamingRequest.StartTimeTicks ?? 0) > 0)
- {
- throw new ArgumentException("StartTimeTicks is not allowed.");
- }
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
+ var cancellationToken = cancellationTokenSource.Token;
+
+ var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ TranscodingJobType,
+ cancellationToken)
+ .ConfigureAwait(false);
- // CTS lifecycle is managed internally.
- var cancellationTokenSource = new CancellationTokenSource();
- var cancellationToken = cancellationTokenSource.Token;
+ var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
- var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- TranscodingJobType,
- cancellationToken)
- .ConfigureAwait(false);
+ var segmentPath = GetSegmentPath(state, playlistPath, segmentId);
- var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
+ var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
- var segmentPath = GetSegmentPath(state, playlistPath, segmentId);
+ TranscodingJobDto? job;
- var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
+ if (System.IO.File.Exists(segmentPath))
+ {
+ job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
+ return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
+ }
- TranscodingJobDto? job;
+ var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
+ await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ var released = false;
+ var startTranscoding = false;
+ try
+ {
if (System.IO.File.Exists(segmentPath))
{
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
+ transcodingLock.Release();
+ released = true;
+ _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
-
- var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
- await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- var released = false;
- var startTranscoding = false;
-
- try
+ else
{
- if (System.IO.File.Exists(segmentPath))
+ var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
+ var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
+
+ if (segmentId == -1)
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- transcodingLock.Release();
- released = true;
- _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
+ _logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
+ startTranscoding = true;
+ segmentId = 0;
}
- else
+ else if (currentTranscodingIndex is null)
{
- var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
- var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
-
- if (segmentId == -1)
- {
- _logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
- startTranscoding = true;
- segmentId = 0;
- }
- else if (currentTranscodingIndex is null)
- {
- _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
- startTranscoding = true;
- }
- else if (segmentId < currentTranscodingIndex.Value)
- {
- _logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex);
- startTranscoding = true;
- }
- else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
- {
- _logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId);
- startTranscoding = true;
- }
+ _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
+ startTranscoding = true;
+ }
+ else if (segmentId < currentTranscodingIndex.Value)
+ {
+ _logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex);
+ startTranscoding = true;
+ }
+ else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
+ {
+ _logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId);
+ startTranscoding = true;
+ }
- if (startTranscoding)
+ if (startTranscoding)
+ {
+ // If the playlist doesn't already exist, startup ffmpeg
+ try
{
- // If the playlist doesn't already exist, startup ffmpeg
- try
- {
- await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
- .ConfigureAwait(false);
-
- if (currentTranscodingIndex.HasValue)
- {
- DeleteLastFile(playlistPath, segmentExtension, 0);
- }
-
- streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
+ await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
+ .ConfigureAwait(false);
- state.WaitForPath = segmentPath;
- job = await _transcodingJobHelper.StartFfMpeg(
- state,
- playlistPath,
- GetCommandLineArguments(playlistPath, state, false, segmentId),
- Request,
- TranscodingJobType,
- cancellationTokenSource).ConfigureAwait(false);
- }
- catch
+ if (currentTranscodingIndex.HasValue)
{
- state.Dispose();
- throw;
+ DeleteLastFile(playlistPath, segmentExtension, 0);
}
- // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
+ streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
+
+ state.WaitForPath = segmentPath;
+ job = await _transcodingJobHelper.StartFfMpeg(
+ state,
+ playlistPath,
+ GetCommandLineArguments(playlistPath, state, false, segmentId),
+ Request,
+ TranscodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
}
- else
+ catch
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- if (job?.TranscodingThrottler is not null)
- {
- await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
- }
+ state.Dispose();
+ throw;
}
+
+ // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
}
- }
- finally
- {
- if (!released)
+ else
{
- transcodingLock.Release();
+ job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ if (job?.TranscodingThrottler is not null)
+ {
+ await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
+ }
}
}
-
- _logger.LogDebug("returning {0} [general case]", segmentPath);
- job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
-
- private static double[] GetSegmentLengths(StreamState state)
- => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength);
-
- internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength)
+ finally
{
- var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks;
- var wholeSegments = runtimeTicks / segmentLengthTicks;
- var remainingTicks = runtimeTicks % segmentLengthTicks;
-
- var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1);
- var segments = new double[segmentsLen];
- for (int i = 0; i < wholeSegments; i++)
+ if (!released)
{
- segments[i] = segmentlength;
+ transcodingLock.Release();
}
+ }
- if (remainingTicks != 0)
- {
- segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds;
- }
+ _logger.LogDebug("returning {0} [general case]", segmentPath);
+ job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
+ }
+
+ private static double[] GetSegmentLengths(StreamState state)
+ => GetSegmentLengthsInternal(state.RunTimeTicks ?? 0, state.SegmentLength);
- return segments;
+ internal static double[] GetSegmentLengthsInternal(long runtimeTicks, int segmentlength)
+ {
+ var segmentLengthTicks = TimeSpan.FromSeconds(segmentlength).Ticks;
+ var wholeSegments = runtimeTicks / segmentLengthTicks;
+ var remainingTicks = runtimeTicks % segmentLengthTicks;
+
+ var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1);
+ var segments = new double[segmentsLen];
+ for (int i = 0; i < wholeSegments; i++)
+ {
+ segments[i] = segmentlength;
}
- private string GetCommandLineArguments(string outputPath, StreamState state, bool isEventPlaylist, int startNumber)
+ if (remainingTicks != 0)
{
- var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
- var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
+ segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds;
+ }
- if (state.BaseRequest.BreakOnNonKeyFrames)
- {
- // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
- // breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
- // to produce a missing part of video stream before first keyframe is encountered, which may lead to
- // awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
- _logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
- state.BaseRequest.BreakOnNonKeyFrames = false;
- }
+ return segments;
+ }
- var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
+ private string GetCommandLineArguments(string outputPath, StreamState state, bool isEventPlaylist, int startNumber)
+ {
+ var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+ var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
- var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
- var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
- var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
- var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
- var outputTsArg = outputPrefix + "%d" + outputExtension;
+ if (state.BaseRequest.BreakOnNonKeyFrames)
+ {
+ // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
+ // breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
+ // to produce a missing part of video stream before first keyframe is encountered, which may lead to
+ // awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
+ _logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
+ state.BaseRequest.BreakOnNonKeyFrames = false;
+ }
- var segmentFormat = string.Empty;
- var segmentContainer = outputExtension.TrimStart('.');
- var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer);
+ var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
- if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
- {
- segmentFormat = "mpegts";
- }
- else if (string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
- {
- var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch
- {
- // on Windows, the path of fmp4 header file needs to be configured
- true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"",
- // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
- false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""
- };
+ var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
+ var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
+ var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
+ var outputTsArg = outputPrefix + "%d" + outputExtension;
- segmentFormat = "fmp4" + outputFmp4HeaderArg;
- }
- else
+ var segmentFormat = string.Empty;
+ var segmentContainer = outputExtension.TrimStart('.');
+ var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer);
+
+ if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
+ {
+ segmentFormat = "mpegts";
+ }
+ else if (string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch
{
- _logger.LogError("Invalid HLS segment container: {SegmentContainer}, default to mpegts", segmentContainer);
- segmentFormat = "mpegts";
- }
+ // on Windows, the path of fmp4 header file needs to be configured
+ true => " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"",
+ // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
+ false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""
+ };
- var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
- ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
- : "128";
+ segmentFormat = "fmp4" + outputFmp4HeaderArg;
+ }
+ else
+ {
+ _logger.LogError("Invalid HLS segment container: {SegmentContainer}, default to mpegts", segmentContainer);
+ segmentFormat = "mpegts";
+ }
- var baseUrlParam = string.Empty;
- if (isEventPlaylist)
- {
- baseUrlParam = string.Format(
- CultureInfo.InvariantCulture,
- " -hls_base_url \"hls/{0}/\"",
- Path.GetFileNameWithoutExtension(outputPath));
- }
+ var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
+ ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+ : "128";
- return string.Format(
+ var baseUrlParam = string.Empty;
+ if (isEventPlaylist)
+ {
+ baseUrlParam = string.Format(
CultureInfo.InvariantCulture,
- "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{12}\" -hls_playlist_type {11} -hls_list_size 0 -y \"{13}\"",
- inputModifier,
- _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer),
- threads,
- mapArgs,
- GetVideoArguments(state, startNumber, isEventPlaylist),
- GetAudioArguments(state),
- maxMuxingQueueSize,
- state.SegmentLength.ToString(CultureInfo.InvariantCulture),
- segmentFormat,
- startNumber.ToString(CultureInfo.InvariantCulture),
- baseUrlParam,
- isEventPlaylist ? "event" : "vod",
- outputTsArg,
- outputPath).Trim();
+ " -hls_base_url \"hls/{0}/\"",
+ Path.GetFileNameWithoutExtension(outputPath));
}
- /// <summary>
- /// Gets the audio arguments for transcoding.
- /// </summary>
- /// <param name="state">The <see cref="StreamState"/>.</param>
- /// <returns>The command line arguments for audio transcoding.</returns>
- private string GetAudioArguments(StreamState state)
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{12}\" -hls_playlist_type {11} -hls_list_size 0 -y \"{13}\"",
+ inputModifier,
+ _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer),
+ threads,
+ mapArgs,
+ GetVideoArguments(state, startNumber, isEventPlaylist),
+ GetAudioArguments(state),
+ maxMuxingQueueSize,
+ state.SegmentLength.ToString(CultureInfo.InvariantCulture),
+ segmentFormat,
+ startNumber.ToString(CultureInfo.InvariantCulture),
+ baseUrlParam,
+ isEventPlaylist ? "event" : "vod",
+ outputTsArg,
+ outputPath).Trim();
+ }
+
+ /// <summary>
+ /// Gets the audio arguments for transcoding.
+ /// </summary>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <returns>The command line arguments for audio transcoding.</returns>
+ private string GetAudioArguments(StreamState state)
+ {
+ if (state.AudioStream is null)
{
- if (state.AudioStream is null)
- {
- return string.Empty;
- }
+ return string.Empty;
+ }
- var audioCodec = _encodingHelper.GetAudioEncoder(state);
+ var audioCodec = _encodingHelper.GetAudioEncoder(state);
- if (!state.IsOutputVideo)
+ if (!state.IsOutputVideo)
+ {
+ if (EncodingHelper.IsCopyCodec(audioCodec))
{
- if (EncodingHelper.IsCopyCodec(audioCodec))
- {
- var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
-
- return "-acodec copy -strict -2" + bitStreamArgs;
- }
-
- var audioTranscodeParams = string.Empty;
-
- audioTranscodeParams += "-acodec " + audioCodec;
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
- if (state.OutputAudioBitrate.HasValue)
- {
- audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
- }
+ return "-acodec copy -strict -2" + bitStreamArgs;
+ }
- if (state.OutputAudioChannels.HasValue)
- {
- audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
- }
+ var audioTranscodeParams = string.Empty;
- if (state.OutputAudioSampleRate.HasValue)
- {
- audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
- }
+ audioTranscodeParams += "-acodec " + audioCodec;
- audioTranscodeParams += " -vn";
- return audioTranscodeParams;
+ if (state.OutputAudioBitrate.HasValue)
+ {
+ audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
}
- // dts, flac, opus and truehd are experimental in mp4 muxer
- var strictArgs = string.Empty;
-
- if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
+ if (state.OutputAudioChannels.HasValue)
{
- strictArgs = " -strict -2";
+ audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
}
- if (EncodingHelper.IsCopyCodec(audioCodec))
+ if (state.OutputAudioSampleRate.HasValue)
{
- var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
- var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
- var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
+ audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
+ }
- if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
- {
- return copyArgs + " -copypriorss:a:0 0";
- }
+ audioTranscodeParams += " -vn";
+ return audioTranscodeParams;
+ }
- return copyArgs;
- }
+ // dts, flac, opus and truehd are experimental in mp4 muxer
+ var strictArgs = string.Empty;
- var args = "-codec:a:0 " + audioCodec + strictArgs;
+ if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
+ {
+ strictArgs = " -strict -2";
+ }
- var channels = state.OutputAudioChannels;
+ if (EncodingHelper.IsCopyCodec(audioCodec))
+ {
+ var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+ var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
- if (channels.HasValue
- && (channels.Value != 2
- || (state.AudioStream is not null
- && state.AudioStream.Channels.HasValue
- && state.AudioStream.Channels.Value > 5
- && _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)))
+ if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
{
- args += " -ac " + channels.Value;
+ return copyArgs + " -copypriorss:a:0 0";
}
- var bitrate = state.OutputAudioBitrate;
+ return copyArgs;
+ }
- if (bitrate.HasValue)
- {
- args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
- }
+ var args = "-codec:a:0 " + audioCodec + strictArgs;
- if (state.OutputAudioSampleRate.HasValue)
- {
- args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
- }
+ var channels = state.OutputAudioChannels;
- args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions);
+ if (channels.HasValue
+ && (channels.Value != 2
+ || (state.AudioStream is not null
+ && state.AudioStream.Channels.HasValue
+ && state.AudioStream.Channels.Value > 5
+ && _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)))
+ {
+ args += " -ac " + channels.Value;
+ }
- return args;
+ var bitrate = state.OutputAudioBitrate;
+
+ if (bitrate.HasValue)
+ {
+ args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
}
- /// <summary>
- /// Gets the video arguments for transcoding.
- /// </summary>
- /// <param name="state">The <see cref="StreamState"/>.</param>
- /// <param name="startNumber">The first number in the hls sequence.</param>
- /// <param name="isEventPlaylist">Whether the playlist is EVENT or VOD.</param>
- /// <returns>The command line arguments for video transcoding.</returns>
- private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist)
+ if (state.OutputAudioSampleRate.HasValue)
{
- if (state.VideoStream is null)
- {
- return string.Empty;
- }
+ args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
+ }
- if (!state.IsOutputVideo)
- {
- return string.Empty;
- }
+ args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions);
- var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+ return args;
+ }
- var args = "-codec:v:0 " + codec;
+ /// <summary>
+ /// Gets the video arguments for transcoding.
+ /// </summary>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <param name="startNumber">The first number in the hls sequence.</param>
+ /// <param name="isEventPlaylist">Whether the playlist is EVENT or VOD.</param>
+ /// <returns>The command line arguments for video transcoding.</returns>
+ private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist)
+ {
+ if (state.VideoStream is null)
+ {
+ return string.Empty;
+ }
- if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- if (EncodingHelper.IsCopyCodec(codec)
- && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
- {
- // Prefer dvh1 to dvhe
- args += " -tag:v:0 dvh1 -strict -2";
- }
- else
- {
- // Prefer hvc1 to hev1
- args += " -tag:v:0 hvc1";
- }
- }
+ if (!state.IsOutputVideo)
+ {
+ return string.Empty;
+ }
- // if (state.EnableMpegtsM2TsMode)
- // {
- // args += " -mpegts_m2ts_mode 1";
- // }
+ var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
- // See if we can save come cpu cycles by avoiding encoding.
- if (EncodingHelper.IsCopyCodec(codec))
- {
- // If h264_mp4toannexb is ever added, do not use it for live tv.
- if (state.VideoStream is not null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
- {
- string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
- if (!string.IsNullOrEmpty(bitStreamArgs))
- {
- args += " " + bitStreamArgs;
- }
- }
+ var args = "-codec:v:0 " + codec;
- args += " -start_at_zero";
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ if (EncodingHelper.IsCopyCodec(codec)
+ && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
+ {
+ // Prefer dvh1 to dvhe
+ args += " -tag:v:0 dvh1 -strict -2";
}
else
{
- args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, isEventPlaylist ? DefaultEventEncoderPreset : DefaultVodEncoderPreset);
+ // Prefer hvc1 to hev1
+ args += " -tag:v:0 hvc1";
+ }
+ }
- // Set the key frame params for video encoding to match the hls segment time.
- args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber);
+ // if (state.EnableMpegtsM2TsMode)
+ // {
+ // args += " -mpegts_m2ts_mode 1";
+ // }
- // Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
- if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
+ // See if we can save come cpu cycles by avoiding encoding.
+ if (EncodingHelper.IsCopyCodec(codec))
+ {
+ // If h264_mp4toannexb is ever added, do not use it for live tv.
+ if (state.VideoStream is not null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+ {
+ string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
+ if (!string.IsNullOrEmpty(bitStreamArgs))
{
- args += " -bf 0";
+ args += " " + bitStreamArgs;
}
+ }
- // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
-
- // video processing filters.
- args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
+ args += " -start_at_zero";
+ }
+ else
+ {
+ args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, isEventPlaylist ? DefaultEventEncoderPreset : DefaultVodEncoderPreset);
- // -start_at_zero is necessary to use with -ss when seeking,
- // otherwise the target position cannot be determined.
- if (state.SubtitleStream is not null)
- {
- // Disable start_at_zero for external graphical subs
- if (!(state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream))
- {
- args += " -start_at_zero";
- }
- }
- }
+ // Set the key frame params for video encoding to match the hls segment time.
+ args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber);
- // TODO why was this not enabled for VOD?
- if (isEventPlaylist)
+ // Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
+ if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{
- args += " -flags -global_header";
+ args += " -bf 0";
}
- if (!string.IsNullOrEmpty(state.OutputVideoSync))
- {
- args += " -vsync " + state.OutputVideoSync;
- }
+ // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
- args += _encodingHelper.GetOutputFFlags(state);
+ // video processing filters.
+ args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
- return args;
+ // -start_at_zero is necessary to use with -ss when seeking,
+ // otherwise the target position cannot be determined.
+ if (state.SubtitleStream is not null)
+ {
+ // Disable start_at_zero for external graphical subs
+ if (!(state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream))
+ {
+ args += " -start_at_zero";
+ }
+ }
}
- private string GetSegmentPath(StreamState state, string playlist, int index)
+ // TODO why was this not enabled for VOD?
+ if (isEventPlaylist)
{
- var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist));
- var filename = Path.GetFileNameWithoutExtension(playlist);
+ args += " -flags -global_header";
+ }
- return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer));
+ if (!string.IsNullOrEmpty(state.OutputVideoSync))
+ {
+ args += " -vsync " + state.OutputVideoSync;
}
- private async Task<ActionResult> GetSegmentResult(
- StreamState state,
- string playlistPath,
- string segmentPath,
- string segmentExtension,
- int segmentIndex,
- TranscodingJobDto? transcodingJob,
- CancellationToken cancellationToken)
+ args += _encodingHelper.GetOutputFFlags(state);
+
+ return args;
+ }
+
+ private string GetSegmentPath(StreamState state, string playlist, int index)
+ {
+ var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist));
+ var filename = Path.GetFileNameWithoutExtension(playlist);
+
+ return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer));
+ }
+
+ private async Task<ActionResult> GetSegmentResult(
+ StreamState state,
+ string playlistPath,
+ string segmentPath,
+ string segmentExtension,
+ int segmentIndex,
+ TranscodingJobDto? transcodingJob,
+ CancellationToken cancellationToken)
+ {
+ var segmentExists = System.IO.File.Exists(segmentPath);
+ if (segmentExists)
{
- var segmentExists = System.IO.File.Exists(segmentPath);
- if (segmentExists)
+ if (transcodingJob is not null && transcodingJob.HasExited)
{
- if (transcodingJob is not null && transcodingJob.HasExited)
- {
- // Transcoding job is over, so assume all existing files are ready
- _logger.LogDebug("serving up {0} as transcode is over", segmentPath);
- return GetSegmentResult(state, segmentPath, transcodingJob);
- }
+ // Transcoding job is over, so assume all existing files are ready
+ _logger.LogDebug("serving up {0} as transcode is over", segmentPath);
+ return GetSegmentResult(state, segmentPath, transcodingJob);
+ }
- var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
+ var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
- // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready
- if (segmentIndex < currentTranscodingIndex)
- {
- _logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex);
- return GetSegmentResult(state, segmentPath, transcodingJob);
- }
+ // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready
+ if (segmentIndex < currentTranscodingIndex)
+ {
+ _logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex);
+ return GetSegmentResult(state, segmentPath, transcodingJob);
}
+ }
- var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1);
- if (transcodingJob is not null)
+ var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1);
+ if (transcodingJob is not null)
+ {
+ while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited)
{
- while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited)
+ // To be considered ready, the segment file has to exist AND
+ // either the transcoding job should be done or next segment should also exist
+ if (segmentExists)
{
- // To be considered ready, the segment file has to exist AND
- // either the transcoding job should be done or next segment should also exist
- if (segmentExists)
- {
- if (transcodingJob.HasExited || System.IO.File.Exists(nextSegmentPath))
- {
- _logger.LogDebug("Serving up {SegmentPath} as it deemed ready", segmentPath);
- return GetSegmentResult(state, segmentPath, transcodingJob);
- }
- }
- else
+ if (transcodingJob.HasExited || System.IO.File.Exists(nextSegmentPath))
{
- segmentExists = System.IO.File.Exists(segmentPath);
- if (segmentExists)
- {
- continue; // avoid unnecessary waiting if segment just became available
- }
+ _logger.LogDebug("Serving up {SegmentPath} as it deemed ready", segmentPath);
+ return GetSegmentResult(state, segmentPath, transcodingJob);
}
-
- await Task.Delay(100, cancellationToken).ConfigureAwait(false);
- }
-
- if (!System.IO.File.Exists(segmentPath))
- {
- _logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath);
}
else
{
- _logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath);
+ segmentExists = System.IO.File.Exists(segmentPath);
+ if (segmentExists)
+ {
+ continue; // avoid unnecessary waiting if segment just became available
+ }
}
- cancellationToken.ThrowIfCancellationRequested();
+ await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (!System.IO.File.Exists(segmentPath))
+ {
+ _logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath);
}
else
{
- _logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
+ _logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath);
}
- return GetSegmentResult(state, segmentPath, transcodingJob);
+ cancellationToken.ThrowIfCancellationRequested();
}
-
- private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJobDto? transcodingJob)
+ else
{
- var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks;
-
- Response.OnCompleted(() =>
- {
- _logger.LogDebug("Finished serving {SegmentPath}", segmentPath);
- if (transcodingJob is not null)
- {
- transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
- _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
- }
+ _logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
+ }
- return Task.CompletedTask;
- });
+ return GetSegmentResult(state, segmentPath, transcodingJob);
+ }
- return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath));
- }
+ private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJobDto? transcodingJob)
+ {
+ var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks;
- private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
+ Response.OnCompleted(() =>
{
- var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
-
- if (job is null || job.HasExited)
+ _logger.LogDebug("Finished serving {SegmentPath}", segmentPath);
+ if (transcodingJob is not null)
{
- return null;
+ transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
+ _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
}
- var file = GetLastTranscodingFile(playlist, segmentExtension, _fileSystem);
+ return Task.CompletedTask;
+ });
- if (file is null)
- {
- return null;
- }
-
- var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
+ return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath));
+ }
- var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
+ private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
+ {
+ var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
- return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ if (job is null || job.HasExited)
+ {
+ return null;
}
- private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
+ var file = GetLastTranscodingFile(playlist, segmentExtension, _fileSystem);
+
+ if (file is null)
{
- var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist));
+ return null;
+ }
- var filePrefix = Path.GetFileNameWithoutExtension(playlist);
+ var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
- try
- {
- return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false)
- .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
- .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
- .FirstOrDefault();
- }
- catch (IOException)
- {
- return null;
- }
- }
+ var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
+
+ return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
+ }
+
+ private static FileSystemMetadata? GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
+ {
+ var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException("Path can't be a root directory.", nameof(playlist));
- private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
+ var filePrefix = Path.GetFileNameWithoutExtension(playlist);
+
+ try
+ {
+ return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false)
+ .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
+ .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
+ .FirstOrDefault();
+ }
+ catch (IOException)
{
- var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
+ return null;
+ }
+ }
- if (file is not null)
- {
- DeleteFile(file.FullName, retryCount);
- }
+ private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
+ {
+ var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
+
+ if (file is not null)
+ {
+ DeleteFile(file.FullName, retryCount);
}
+ }
- private void DeleteFile(string path, int retryCount)
+ private void DeleteFile(string path, int retryCount)
+ {
+ if (retryCount >= 5)
{
- if (retryCount >= 5)
- {
- return;
- }
+ return;
+ }
- _logger.LogDebug("Deleting partial HLS file {Path}", path);
+ _logger.LogDebug("Deleting partial HLS file {Path}", path);
- try
- {
- _fileSystem.DeleteFile(path);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
+ try
+ {
+ _fileSystem.DeleteFile(path);
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
- var task = Task.Delay(100);
- task.Wait();
- DeleteFile(path, retryCount + 1);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
- }
+ var task = Task.Delay(100);
+ task.Wait();
+ DeleteFile(path, retryCount + 1);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
}
}
}
diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 6c78a7987..8c9ee1a19 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -12,186 +12,185 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Environment Controller.
+/// </summary>
+[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+public class EnvironmentController : BaseJellyfinApiController
{
+ private const char UncSeparator = '\\';
+ private const string UncStartPrefix = @"\\";
+
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger<EnvironmentController> _logger;
+
/// <summary>
- /// Environment Controller.
+ /// Initializes a new instance of the <see cref="EnvironmentController"/> class.
/// </summary>
- [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
- public class EnvironmentController : BaseJellyfinApiController
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param>
+ public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger)
{
- private const char UncSeparator = '\\';
- private const string UncStartPrefix = @"\\";
-
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<EnvironmentController> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="EnvironmentController"/> class.
- /// </summary>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param>
- public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger)
- {
- _fileSystem = fileSystem;
- _logger = logger;
- }
+ _fileSystem = fileSystem;
+ _logger = logger;
+ }
- /// <summary>
- /// Gets the contents of a given directory in the file system.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param>
- /// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param>
- /// <response code="200">Directory contents returned.</response>
- /// <returns>Directory contents.</returns>
- [HttpGet("DirectoryContents")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
- [FromQuery, Required] string path,
- [FromQuery] bool includeFiles = false,
- [FromQuery] bool includeDirectories = false)
+ /// <summary>
+ /// Gets the contents of a given directory in the file system.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param>
+ /// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param>
+ /// <response code="200">Directory contents returned.</response>
+ /// <returns>Directory contents.</returns>
+ [HttpGet("DirectoryContents")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
+ [FromQuery, Required] string path,
+ [FromQuery] bool includeFiles = false,
+ [FromQuery] bool includeDirectories = false)
+ {
+ if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase)
+ && path.LastIndexOf(UncSeparator) == 1)
{
- if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase)
- && path.LastIndexOf(UncSeparator) == 1)
- {
- return Array.Empty<FileSystemEntryInfo>();
- }
+ return Array.Empty<FileSystemEntryInfo>();
+ }
- var entries =
- _fileSystem.GetFileSystemEntries(path)
- .Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles))
- .OrderBy(i => i.FullName);
+ 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));
- }
+ 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)
+ /// <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.HasValue)
+ if (validatePathDto.IsFile.Value)
{
- if (validatePathDto.IsFile.Value)
+ if (!System.IO.File.Exists(validatePathDto.Path))
{
- if (!System.IO.File.Exists(validatePathDto.Path))
- {
- return NotFound();
- }
- }
- else
- {
- if (!Directory.Exists(validatePathDto.Path))
- {
- return NotFound();
- }
+ return NotFound();
}
}
else
{
- if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
+ if (!Directory.Exists(validatePathDto.Path))
{
return NotFound();
}
+ }
+ }
+ else
+ {
+ if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
+ {
+ return NotFound();
+ }
- if (validatePathDto.ValidateWritable)
+ if (validatePathDto.ValidateWritable)
+ {
+ if (validatePathDto.Path is null)
{
- if (validatePathDto.Path is null)
- {
- throw new ResourceNotFoundException(nameof(validatePathDto.Path));
- }
+ throw new ResourceNotFoundException(nameof(validatePathDto.Path));
+ }
- var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
- try
- {
- System.IO.File.WriteAllText(file, string.Empty);
- }
- finally
+ var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
+ try
+ {
+ System.IO.File.WriteAllText(file, string.Empty);
+ }
+ finally
+ {
+ if (System.IO.File.Exists(file))
{
- if (System.IO.File.Exists(file))
- {
- System.IO.File.Delete(file);
- }
+ System.IO.File.Delete(file);
}
}
}
-
- return NoContent();
}
- /// <summary>
- /// Gets network paths.
- /// </summary>
- /// <response code="200">Empty array returned.</response>
- /// <returns>List of entries.</returns>
- [Obsolete("This endpoint is obsolete.")]
- [HttpGet("NetworkShares")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares()
- {
- _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares");
- return Array.Empty<FileSystemEntryInfo>();
- }
+ return NoContent();
+ }
- /// <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 network paths.
+ /// </summary>
+ /// <response code="200">Empty array returned.</response>
+ /// <returns>List of entries.</returns>
+ [Obsolete("This endpoint is obsolete.")]
+ [HttpGet("NetworkShares")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares()
+ {
+ _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares");
+ return Array.Empty<FileSystemEntryInfo>();
+ }
- /// <summary>
- /// Gets 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)
+ /// <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))
{
- string? parent = Path.GetDirectoryName(path);
- if (string.IsNullOrEmpty(parent))
+ // Check if unc share
+ var index = path.LastIndexOf(UncSeparator);
+
+ if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0)
{
- // Check if unc share
- var index = path.LastIndexOf(UncSeparator);
+ parent = path.Substring(0, index);
- if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0)
+ if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
{
- parent = path.Substring(0, index);
-
- if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
- {
- parent = null;
- }
+ 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();
- }
+ 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();
}
}
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 17d136384..dd64ff903 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -1,6 +1,5 @@
using System;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
@@ -12,205 +11,204 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Filters controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class FilterController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// Filters controller.
+ /// Initializes a new instance of the <see cref="FilterController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class FilterController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public FilterController(ILibraryManager libraryManager, IUserManager userManager)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="FilterController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public FilterController(ILibraryManager libraryManager, IUserManager userManager)
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ }
+
+ /// <summary>
+ /// Gets legacy query filters.
+ /// </summary>
+ /// <param name="userId">Optional. User id.</param>
+ /// <param name="parentId">Optional. Parent id.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <response code="200">Legacy filters retrieved.</response>
+ /// <returns>Legacy query filters.</returns>
+ [HttpGet("Items/Filters")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
+ [FromQuery] Guid? userId,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ BaseItem? item = null;
+ if (includeItemTypes.Length != 1
+ || !(includeItemTypes[0] == BaseItemKind.BoxSet
+ || includeItemTypes[0] == BaseItemKind.Playlist
+ || includeItemTypes[0] == BaseItemKind.Trailer
+ || includeItemTypes[0] == BaseItemKind.Program))
{
- _libraryManager = libraryManager;
- _userManager = userManager;
+ item = _libraryManager.GetParentItem(parentId, user?.Id);
}
- /// <summary>
- /// Gets legacy query filters.
- /// </summary>
- /// <param name="userId">Optional. User id.</param>
- /// <param name="parentId">Optional. Parent id.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
- /// <response code="200">Legacy filters retrieved.</response>
- /// <returns>Legacy query filters.</returns>
- [HttpGet("Items/Filters")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
- [FromQuery] Guid? userId,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
+ var query = new InternalItemsQuery
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- BaseItem? item = null;
- if (includeItemTypes.Length != 1
- || !(includeItemTypes[0] == BaseItemKind.BoxSet
- || includeItemTypes[0] == BaseItemKind.Playlist
- || includeItemTypes[0] == BaseItemKind.Trailer
- || includeItemTypes[0] == BaseItemKind.Program))
- {
- item = _libraryManager.GetParentItem(parentId, user?.Id);
- }
-
- var query = new InternalItemsQuery
- {
- User = user,
- MediaTypes = mediaTypes,
- IncludeItemTypes = includeItemTypes,
- Recursive = true,
- EnableTotalRecordCount = false,
- DtoOptions = new DtoOptions
- {
- Fields = new[] { ItemFields.Genres, ItemFields.Tags },
- EnableImages = false,
- EnableUserData = false
- }
- };
-
- if (item is not Folder folder)
+ User = user,
+ MediaTypes = mediaTypes,
+ IncludeItemTypes = includeItemTypes,
+ Recursive = true,
+ EnableTotalRecordCount = false,
+ DtoOptions = new DtoOptions
{
- return new QueryFiltersLegacy();
+ Fields = new[] { ItemFields.Genres, ItemFields.Tags },
+ EnableImages = false,
+ EnableUserData = false
}
+ };
- var itemList = folder.GetItemList(query);
- return new QueryFiltersLegacy
- {
- Years = itemList.Select(i => i.ProductionYear ?? -1)
- .Where(i => i > 0)
- .Distinct()
- .Order()
- .ToArray(),
-
- Genres = itemList.SelectMany(i => i.Genres)
- .DistinctNames()
- .Order()
- .ToArray(),
-
- Tags = itemList
- .SelectMany(i => i.Tags)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .Order()
- .ToArray(),
-
- OfficialRatings = itemList
- .Select(i => i.OfficialRating)
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .Order()
- .ToArray()
- };
+ if (item is not Folder folder)
+ {
+ return new QueryFiltersLegacy();
}
- /// <summary>
- /// Gets query filters.
- /// </summary>
- /// <param name="userId">Optional. User id.</param>
- /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="isAiring">Optional. Is item airing.</param>
- /// <param name="isMovie">Optional. Is item movie.</param>
- /// <param name="isSports">Optional. Is item sports.</param>
- /// <param name="isKids">Optional. Is item kids.</param>
- /// <param name="isNews">Optional. Is item news.</param>
- /// <param name="isSeries">Optional. Is item series.</param>
- /// <param name="recursive">Optional. Search recursive.</param>
- /// <response code="200">Filters retrieved.</response>
- /// <returns>Query filters.</returns>
- [HttpGet("Items/Filters2")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryFilters> GetQueryFilters(
- [FromQuery] Guid? userId,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery] bool? isAiring,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSports,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? recursive)
+ var itemList = folder.GetItemList(query);
+ return new QueryFiltersLegacy
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- 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);
- }
+ 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()
+ };
+ }
- 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
+ /// <summary>
+ /// Gets query filters.
+ /// </summary>
+ /// <param name="userId">Optional. User id.</param>
+ /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="isAiring">Optional. Is item airing.</param>
+ /// <param name="isMovie">Optional. Is item movie.</param>
+ /// <param name="isSports">Optional. Is item sports.</param>
+ /// <param name="isKids">Optional. Is item kids.</param>
+ /// <param name="isNews">Optional. Is item news.</param>
+ /// <param name="isSeries">Optional. Is item series.</param>
+ /// <param name="recursive">Optional. Search recursive.</param>
+ /// <response code="200">Filters retrieved.</response>
+ /// <returns>Query filters.</returns>
+ [HttpGet("Items/Filters2")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryFilters> GetQueryFilters(
+ [FromQuery] Guid? userId,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery] bool? isAiring,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSports,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? recursive)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ 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
{
- genreQuery.Parent = parentItem;
- }
+ 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))
+ 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
{
- filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
- {
- Name = i.Item.Name,
- Id = i.Item.Id
- }).ToArray();
- }
- else
+ Name = i.Item.Name,
+ Id = i.Item.Id
+ }).ToArray();
+ }
+ else
+ {
+ filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
{
- filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
- {
- Name = i.Item.Name,
- Id = i.Item.Id
- }).ToArray();
- }
-
- return filters;
+ Name = i.Item.Name,
+ Id = i.Item.Id
+ }).ToArray();
}
+
+ return filters;
}
}
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 611643bd8..711fb4aef 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -18,194 +17,193 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Genre = MediaBrowser.Controller.Entities.Genre;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The genres controller.
+/// </summary>
+[Authorize]
+public class GenresController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+
/// <summary>
- /// The genres controller.
+ /// Initializes a new instance of the <see cref="GenresController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class GenresController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ public GenresController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IDtoService dtoService)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="GenresController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- public GenresController(
- IUserManager userManager,
- ILibraryManager libraryManager,
- IDtoService dtoService)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- }
-
- /// <summary>
- /// Gets all genres from a given item, folder, or the entire library.
- /// </summary>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="searchTerm">The search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="userId">User id.</param>
- /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
- /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
- /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
- /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
- /// <param name="enableImages">Optional, include image information in output.</param>
- /// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
- /// <response code="200">Genres returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the queryresult of genres.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetGenres(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery] bool? isFavorite,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] Guid? userId,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery] bool? enableImages = true,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ }
- User? user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ /// <summary>
+ /// Gets all genres from a given item, folder, or the entire library.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="searchTerm">The search term.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+ /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <param name="userId">User id.</param>
+ /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+ /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+ /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
+ /// <response code="200">Genres returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the queryresult of genres.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetGenres(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- var parentItem = _libraryManager.GetParentItem(parentId, userId);
+ User? user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- var query = new InternalItemsQuery(user)
- {
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
- };
-
- if (parentId.HasValue)
- {
- if (parentItem is Folder)
- {
- query.AncestorIds = new[] { parentId.Value };
- }
- else
- {
- query.ItemIds = new[] { parentId.Value };
- }
- }
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
- QueryResult<(BaseItem, ItemCounts)> result;
- if (parentItem is ICollectionFolder parentCollectionFolder
- && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
- || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
+ };
+
+ if (parentId.HasValue)
+ {
+ if (parentItem is Folder)
{
- result = _libraryManager.GetMusicGenres(query);
+ query.AncestorIds = new[] { parentId.Value };
}
else
{
- result = _libraryManager.GetGenres(query);
+ query.ItemIds = new[] { parentId.Value };
}
-
- var shouldIncludeItemTypes = includeItemTypes.Length != 0;
- return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
- /// <summary>
- /// Gets a genre, by name.
- /// </summary>
- /// <param name="genreName">The genre name.</param>
- /// <param name="userId">The user id.</param>
- /// <response code="200">Genres returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the genre.</returns>
- [HttpGet("{genreName}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
+ QueryResult<(BaseItem, ItemCounts)> result;
+ if (parentItem is ICollectionFolder parentCollectionFolder
+ && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
+ || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
{
- var dtoOptions = new DtoOptions()
- .AddClientFields(User);
+ result = _libraryManager.GetMusicGenres(query);
+ }
+ else
+ {
+ result = _libraryManager.GetGenres(query);
+ }
- Genre? item;
- if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase))
- {
- item = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre);
- }
- else
- {
- item = _libraryManager.GetGenre(genreName);
- }
+ var shouldIncludeItemTypes = includeItemTypes.Length != 0;
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
+ }
- item ??= new Genre();
+ /// <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);
- if (userId is null || userId.Value.Equals(default))
- {
- return _dtoService.GetBaseItemDto(item, dtoOptions);
- }
+ Genre? item;
+ if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase))
+ {
+ item = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre);
+ }
+ else
+ {
+ item = _libraryManager.GetGenre(genreName);
+ }
- var user = _userManager.GetUserById(userId.Value);
+ item ??= new Genre();
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ if (userId is null || userId.Value.Equals(default))
+ {
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
}
- private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
- where T : BaseItem, new()
+ 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
{
- var result = libraryManager.GetItemList(new InternalItemsQuery
- {
- Name = name.Replace(BaseItem.SlugChar, '&'),
- IncludeItemTypes = new[] { baseItemKind },
- DtoOptions = dtoOptions
- }).OfType<T>().FirstOrDefault();
+ 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();
- 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;
- }
+ return result;
}
}
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 50fee233a..d7cec865e 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
@@ -15,178 +14,177 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The hls segment controller.
+/// </summary>
+[Route("")]
+public class HlsSegmentController : BaseJellyfinApiController
{
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
+ /// </summary>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param>
+ public HlsSegmentController(
+ IFileSystem fileSystem,
+ IServerConfigurationManager serverConfigurationManager,
+ TranscodingJobHelper transcodingJobHelper)
+ {
+ _fileSystem = fileSystem;
+ _serverConfigurationManager = serverConfigurationManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ }
+
/// <summary>
- /// The hls segment controller.
+ /// Gets the specified audio segment for an audio item.
/// </summary>
- [Route("")]
- public class HlsSegmentController : BaseJellyfinApiController
+ /// <param name="itemId">The item id.</param>
+ /// <param name="segmentId">The segment id.</param>
+ /// <response code="200">Hls audio segment returned.</response>
+ /// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
+ // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
+ // [Authenticated]
+ [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
+ [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
+ public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
{
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
- /// </summary>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param>
- public HlsSegmentController(
- IFileSystem fileSystem,
- IServerConfigurationManager serverConfigurationManager,
- TranscodingJobHelper transcodingJobHelper)
+ // TODO: Deprecate with new iOS app
+ var file = segmentId + Path.GetExtension(Request.Path);
+ var transcodePath = _serverConfigurationManager.GetTranscodePath();
+ file = Path.GetFullPath(Path.Combine(transcodePath, file));
+ var fileDir = Path.GetDirectoryName(file);
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
{
- _fileSystem = fileSystem;
- _serverConfigurationManager = serverConfigurationManager;
- _transcodingJobHelper = transcodingJobHelper;
+ return BadRequest("Invalid segment.");
}
- /// <summary>
- /// Gets the specified audio segment for an audio item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="segmentId">The segment id.</param>
- /// <response code="200">Hls audio segment returned.</response>
- /// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
- // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
- // [Authenticated]
- [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
- [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesAudioFile]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
- public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
- {
- // TODO: Deprecate with new iOS app
- var file = segmentId + Path.GetExtension(Request.Path);
- var transcodePath = _serverConfigurationManager.GetTranscodePath();
- file = Path.GetFullPath(Path.Combine(transcodePath, file));
- var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
- {
- return BadRequest("Invalid segment.");
- }
+ return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file));
+ }
- return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file));
+ /// <summary>
+ /// Gets a hls video playlist.
+ /// </summary>
+ /// <param name="itemId">The video id.</param>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <response code="200">Hls video playlist returned.</response>
+ /// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
+ [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
+ [Authorize]
+ [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.");
}
- /// <summary>
- /// Gets a hls video playlist.
- /// </summary>
- /// <param name="itemId">The video id.</param>
- /// <param name="playlistId">The playlist id.</param>
- /// <response code="200">Hls video playlist returned.</response>
- /// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
- [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesPlaylistFile]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
- public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
- {
- var file = playlistId + Path.GetExtension(Request.Path);
- var transcodePath = _serverConfigurationManager.GetTranscodePath();
- file = Path.GetFullPath(Path.Combine(transcodePath, file));
- var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
- {
- return BadRequest("Invalid segment.");
- }
+ return GetFileResult(file, file);
+ }
- 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]
+ [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();
- /// <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)
+ file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
+ var fileDir = Path.GetDirectoryName(file);
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture))
{
- _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
- return NoContent();
+ return BadRequest("Invalid segment.");
}
- /// <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();
+ var normalizedPlaylistId = playlistId;
- file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
- var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture))
+ 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;
}
+ }
- 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);
+ }
- 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);
- private ActionResult GetFileResult(string path, string playlistPath)
+ Response.OnCompleted(() =>
{
- var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
-
- Response.OnCompleted(() =>
+ if (transcodingJob is not null)
{
- if (transcodingJob is not null)
- {
- _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
- }
+ _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
+ }
- return Task.CompletedTask;
- });
+ return Task.CompletedTask;
+ });
- return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path));
- }
+ return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path));
}
}
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index f866655c0..aecdf00dc 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -30,2116 +30,2080 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Image controller.
+/// </summary>
+[Route("")]
+public class ImageController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IProviderManager _providerManager;
+ private readonly IImageProcessor _imageProcessor;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger<ImageController> _logger;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IApplicationPaths _appPaths;
+
/// <summary>
- /// Image controller.
+ /// Initializes a new instance of the <see cref="ImageController"/> class.
/// </summary>
- [Route("")]
- public class ImageController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{ImageController}"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ public ImageController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IProviderManager providerManager,
+ IImageProcessor imageProcessor,
+ IFileSystem fileSystem,
+ ILogger<ImageController> logger,
+ IServerConfigurationManager serverConfigurationManager,
+ IApplicationPaths appPaths)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IProviderManager _providerManager;
- private readonly IImageProcessor _imageProcessor;
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<ImageController> _logger;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IApplicationPaths _appPaths;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ImageController"/> 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="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{ImageController}"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
- public ImageController(
- IUserManager userManager,
- ILibraryManager libraryManager,
- IProviderManager providerManager,
- IImageProcessor imageProcessor,
- IFileSystem fileSystem,
- ILogger<ImageController> logger,
- IServerConfigurationManager serverConfigurationManager,
- IApplicationPaths appPaths)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _providerManager = providerManager;
- _imageProcessor = imageProcessor;
- _fileSystem = fileSystem;
- _logger = logger;
- _serverConfigurationManager = serverConfigurationManager;
- _appPaths = appPaths;
- }
-
- /// <summary>
- /// Sets the user image.
- /// </summary>
- /// <param name="userId">User Id.</param>
- /// <param name="imageType">(Unused) Image type.</param>
- /// <param name="index">(Unused) Image index.</param>
- /// <response code="204">Image updated.</response>
- /// <response code="403">User does not have permission to delete the image.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Users/{userId}/Images/{imageType}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [AcceptsImageFile]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> PostUserImage(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] int? index = null)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
- }
-
- var user = _userManager.GetUserById(userId);
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
- {
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
- if (user.ProfileImage is not null)
- {
- await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
- }
-
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _providerManager = providerManager;
+ _imageProcessor = imageProcessor;
+ _fileSystem = fileSystem;
+ _logger = logger;
+ _serverConfigurationManager = serverConfigurationManager;
+ _appPaths = appPaths;
+ }
- await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
- .ConfigureAwait(false);
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ /// <summary>
+ /// Sets the user image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image updated.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Users/{userId}/Images/{imageType}")]
+ [Authorize]
+ [AcceptsImageFile]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> PostUserImage(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? index = null)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
- return NoContent();
- }
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
- /// <summary>
- /// Sets the user image.
- /// </summary>
- /// <param name="userId">User Id.</param>
- /// <param name="imageType">(Unused) Image type.</param>
- /// <param name="index">(Unused) Image index.</param>
- /// <response code="204">Image updated.</response>
- /// <response code="403">User does not have permission to delete the image.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [AcceptsImageFile]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> PostUserImageByIndex(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute] int index)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+ if (user.ProfileImage is not null)
{
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
- var user = _userManager.GetUserById(userId);
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
- {
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
- if (user.ProfileImage is not null)
- {
- await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
- }
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
- user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
+ await _providerManager
+ .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .ConfigureAwait(false);
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
- await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
- .ConfigureAwait(false);
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ return NoContent();
+ }
+ }
- return NoContent();
- }
+ /// <summary>
+ /// Sets the user image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image updated.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
+ [Authorize]
+ [AcceptsImageFile]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> PostUserImageByIndex(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int index)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Delete the user's image.
- /// </summary>
- /// <param name="userId">User Id.</param>
- /// <param name="imageType">(Unused) Image type.</param>
- /// <param name="index">(Unused) Image index.</param>
- /// <response code="204">Image deleted.</response>
- /// <response code="403">User does not have permission to delete the image.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Users/{userId}/Images/{imageType}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> DeleteUserImage(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] int? index = null)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
- }
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
+ }
- var user = _userManager.GetUserById(userId);
- if (user?.ProfileImage is null)
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+ if (user.ProfileImage is not null)
{
- return NoContent();
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
- try
- {
- System.IO.File.Delete(user.ProfileImage.Path);
- }
- catch (IOException e)
- {
- _logger.LogError(e, "Error deleting user profile image:");
- }
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty)));
+
+ await _providerManager
+ .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .ConfigureAwait(false);
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
- await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
return NoContent();
}
+ }
- /// <summary>
- /// Delete the user's image.
- /// </summary>
- /// <param name="userId">User Id.</param>
- /// <param name="imageType">(Unused) Image type.</param>
- /// <param name="index">(Unused) Image index.</param>
- /// <response code="204">Image deleted.</response>
- /// <response code="403">User does not have permission to delete the image.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> DeleteUserImageByIndex(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute] int index)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
- }
-
- var user = _userManager.GetUserById(userId);
- if (user?.ProfileImage is null)
- {
- return NoContent();
- }
-
- try
- {
- System.IO.File.Delete(user.ProfileImage.Path);
- }
- catch (IOException e)
- {
- _logger.LogError(e, "Error deleting user profile image:");
- }
+ /// <summary>
+ /// Delete the user's image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Users/{userId}/Images/{imageType}")]
+ [Authorize]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> DeleteUserImage(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? index = null)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
+ }
- await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+ var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage is null)
+ {
return NoContent();
}
- /// <summary>
- /// Delete an item's image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">The image index.</param>
- /// <response code="204">Image deleted.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpDelete("Items/{itemId}/Images/{imageType}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteItemImage(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ try
+ {
+ System.IO.File.Delete(user.ProfileImage.Path);
+ }
+ catch (IOException e)
+ {
+ _logger.LogError(e, "Error deleting user profile image:");
+ }
+
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+ return NoContent();
+ }
- await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false);
+ /// <summary>
+ /// Delete the user's image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
+ [Authorize]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> DeleteUserImageByIndex(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int index)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, HttpContext.User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
+ }
+
+ var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage is null)
+ {
return NoContent();
}
- /// <summary>
- /// Delete an item's image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">The image index.</param>
- /// <response code="204">Image deleted.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteItemImageByIndex(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute] int imageIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ try
+ {
+ System.IO.File.Delete(user.ProfileImage.Path);
+ }
+ catch (IOException e)
+ {
+ _logger.LogError(e, "Error deleting user profile image:");
+ }
- await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
- return NoContent();
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Delete an item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">The image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpDelete("Items/{itemId}/Images/{imageType}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteItemImage(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Set item image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <response code="204">Image saved.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpPost("Items/{itemId}/Images/{imageType}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [AcceptsImageFile]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> SetItemImage(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false);
+ return NoContent();
+ }
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
- {
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
- await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+ /// <summary>
+ /// Delete an item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">The image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteItemImageByIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
- return NoContent();
- }
+ await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Set item image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <response code="204">Image saved.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpPost("Items/{itemId}/Images/{imageType}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [AcceptsImageFile]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> SetItemImage(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Set item image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">(Unused) Image index.</param>
- /// <response code="204">Image saved.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [AcceptsImageFile]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> SetItemImageByIndex(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute] int imageIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
- {
- // Handle image/png; charset=utf-8
- var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
- await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
+ }
- return NoContent();
- }
+ /// <summary>
+ /// Set item image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">(Unused) Image index.</param>
+ /// <response code="204">Image saved.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [AcceptsImageFile]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> SetItemImageByIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Updates the index for an item image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Old image index.</param>
- /// <param name="newIndex">New image index.</param>
- /// <response code="204">Image index updated.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> UpdateItemImageIndex(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery, Required] int newIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
+ await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
- await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false);
return NoContent();
}
+ }
- /// <summary>
- /// Get item image infos.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Item images returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpGet("Items/{itemId}/Images")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Updates the index for an item image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Old image index.</param>
+ /// <param name="newIndex">New image index.</param>
+ /// <response code="204">Image index updated.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}/Index")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdateItemImageIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery, Required] int newIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
- var list = new List<ImageInfo>();
- var itemImages = item.ImageInfos;
+ await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false);
+ return NoContent();
+ }
- if (itemImages.Length == 0)
- {
- // short-circuit
- return list;
- }
+ /// <summary>
+ /// Get item image infos.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Item images returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>The list of image infos on success, or <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpGet("Items/{itemId}/Images")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
- await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes are correct
+ var list = new List<ImageInfo>();
+ var itemImages = item.ImageInfos;
- foreach (var image in itemImages)
+ if (itemImages.Length == 0)
+ {
+ // short-circuit
+ return list;
+ }
+
+ await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes are correct
+
+ foreach (var image in itemImages)
+ {
+ if (!item.AllowsMultipleImages(image.Type))
{
- if (!item.AllowsMultipleImages(image.Type))
- {
- var info = GetImageInfo(item, image, null);
+ var info = GetImageInfo(item, image, null);
- if (info is not null)
- {
- list.Add(info);
- }
+ if (info is not null)
+ {
+ list.Add(info);
}
}
+ }
- foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
- {
- var index = 0;
-
- // Prevent implicitly captured closure
- var currentImageType = imageType;
+ foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages))
+ {
+ var index = 0;
- foreach (var image in itemImages.Where(i => i.Type == currentImageType))
- {
- var info = GetImageInfo(item, image, index);
+ // Prevent implicitly captured closure
+ var currentImageType = imageType;
- if (info is not null)
- {
- list.Add(info);
- }
+ foreach (var image in itemImages.Where(i => i.Type == currentImageType))
+ {
+ var info = GetImageInfo(item, image, index);
- index++;
+ if (info is not null)
+ {
+ list.Add(info);
}
+
+ index++;
}
+ }
- return list;
+ return list;
+ }
+
+ /// <summary>
+ /// Gets the item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Items/{itemId}/Images/{imageType}")]
+ [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetItemImage(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery] string? tag,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Gets the item's image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Items/{itemId}/Images/{imageType}")]
- [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetItemImage(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery] string? tag,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] ImageFormat? format,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ itemId,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- itemId,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Gets the item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetItemImageByIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int imageIndex,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery] string? tag,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Gets the item's image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetItemImageByIndex(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute] int imageIndex,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery] string? tag,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] ImageFormat? format,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ itemId,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- itemId,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Gets the item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
+ [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetItemImage2(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int maxWidth,
+ [FromRoute, Required] int maxHeight,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromRoute, Required] string tag,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromRoute, Required] ImageFormat format,
+ [FromRoute, Required] double percentPlayed,
+ [FromRoute, Required] int unplayedCount,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute, Required] int imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Gets the item's image.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
- [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetItemImage2(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int maxWidth,
- [FromRoute, Required] int maxHeight,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromRoute, Required] string tag,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromRoute, Required] ImageFormat format,
- [FromQuery] bool? addPlayedIndicator,
- [FromRoute, Required] double percentPlayed,
- [FromRoute, Required] int unplayedCount,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromRoute, Required] int imageIndex)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ itemId,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- itemId,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get artist image by name.
+ /// </summary>
+ /// <param name="name">Artist name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetArtistImage(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromRoute, Required] int imageIndex)
+ {
+ var item = _libraryManager.GetArtist(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get artist image by name.
- /// </summary>
- /// <param name="name">Artist name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetArtistImage(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromRoute, Required] int imageIndex)
- {
- var item = _libraryManager.GetArtist(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get genre image by name.
+ /// </summary>
+ /// <param name="name">Genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Genres/{name}/Images/{imageType}")]
+ [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetGenreImage(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetGenre(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get genre image by name.
- /// </summary>
- /// <param name="name">Genre name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Genres/{name}/Images/{imageType}")]
- [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetGenreImage(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetGenre(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get genre image by name.
+ /// </summary>
+ /// <param name="name">Genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetGenreImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var item = _libraryManager.GetGenre(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get genre image by name.
- /// </summary>
- /// <param name="name">Genre name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetGenreImageByIndex(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var item = _libraryManager.GetGenre(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get music genre image by name.
+ /// </summary>
+ /// <param name="name">Music genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("MusicGenres/{name}/Images/{imageType}")]
+ [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetMusicGenreImage(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetMusicGenre(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get music genre image by name.
- /// </summary>
- /// <param name="name">Music genre name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("MusicGenres/{name}/Images/{imageType}")]
- [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetMusicGenreImage(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetMusicGenre(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get music genre image by name.
+ /// </summary>
+ /// <param name="name">Music genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetMusicGenreImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var item = _libraryManager.GetMusicGenre(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get music genre image by name.
- /// </summary>
- /// <param name="name">Music genre name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
- [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetMusicGenreImageByIndex(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var item = _libraryManager.GetMusicGenre(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get person image by name.
+ /// </summary>
+ /// <param name="name">Person name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Persons/{name}/Images/{imageType}")]
+ [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetPersonImage(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetPerson(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get person image by name.
- /// </summary>
- /// <param name="name">Person name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Persons/{name}/Images/{imageType}")]
- [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetPersonImage(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetPerson(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get person image by name.
+ /// </summary>
+ /// <param name="name">Person name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetPersonImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var item = _libraryManager.GetPerson(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get person image by name.
- /// </summary>
- /// <param name="name">Person name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetPersonImageByIndex(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var item = _libraryManager.GetPerson(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get studio image by name.
+ /// </summary>
+ /// <param name="name">Studio name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Studios/{name}/Images/{imageType}")]
+ [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetStudioImage(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetStudio(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get studio image by name.
- /// </summary>
- /// <param name="name">Studio name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Studios/{name}/Images/{imageType}")]
- [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetStudioImage(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var item = _libraryManager.GetStudio(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get studio image by name.
+ /// </summary>
+ /// <param name="name">Studio name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetStudioImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var item = _libraryManager.GetStudio(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get studio image by name.
- /// </summary>
- /// <param name="name">Studio name.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetStudioImageByIndex(
- [FromRoute, Required] string name,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var item = _libraryManager.GetStudio(name);
- if (item is null)
- {
- return NotFound();
- }
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- item.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- item)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get user profile image.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Users/{userId}/Images/{imageType}")]
+ [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetUserImage(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery] int? imageIndex)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get user profile image.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Users/{userId}/Images/{imageType}")]
- [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetUserImage(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
- {
- var user = _userManager.GetUserById(userId);
- if (user?.ProfileImage is null)
- {
- return NotFound();
- }
+ var info = new ItemImageInfo
+ {
+ Path = user.ProfileImage.Path,
+ Type = ImageType.Profile,
+ DateModified = user.ProfileImage.LastModified
+ };
- var info = new ItemImageInfo
- {
- Path = user.ProfileImage.Path,
- Type = ImageType.Profile,
- DateModified = user.ProfileImage.LastModified
- };
+ if (width.HasValue)
+ {
+ info.Width = width.Value;
+ }
- if (width.HasValue)
- {
- info.Width = width.Value;
- }
+ if (height.HasValue)
+ {
+ info.Height = height.Value;
+ }
- if (height.HasValue)
- {
- info.Height = height.Value;
- }
+ return await GetImageInternal(
+ user.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ null,
+ info)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- user.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- null,
- info)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get user profile image.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetUserImageByIndex(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery, ParameterObsolete] bool? cropWhitespace,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Get user profile image.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">Image index.</param>
- /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <response code="200">Image stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>
- /// A <see cref="FileStreamResult"/> containing the file stream on success,
- /// or a <see cref="NotFoundResult"/> if item not found.
- /// </returns>
- [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
- [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesImageFile]
- public async Task<ActionResult> GetUserImageByIndex(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] ImageType imageType,
- [FromRoute, Required] int imageIndex,
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery, ParameterObsolete] bool? cropWhitespace,
- [FromQuery] bool? addPlayedIndicator,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer)
- {
- var user = _userManager.GetUserById(userId);
- if (user?.ProfileImage is null)
- {
- return NotFound();
- }
+ var info = new ItemImageInfo
+ {
+ Path = user.ProfileImage.Path,
+ Type = ImageType.Profile,
+ DateModified = user.ProfileImage.LastModified
+ };
- var info = new ItemImageInfo
- {
- Path = user.ProfileImage.Path,
- Type = ImageType.Profile,
- DateModified = user.ProfileImage.LastModified
- };
+ if (width.HasValue)
+ {
+ info.Width = width.Value;
+ }
- if (width.HasValue)
- {
- info.Width = width.Value;
- }
+ if (height.HasValue)
+ {
+ info.Height = height.Value;
+ }
- if (height.HasValue)
- {
- info.Height = height.Value;
- }
+ return await GetImageInternal(
+ user.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ fillWidth,
+ fillHeight,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ null,
+ info)
+ .ConfigureAwait(false);
+ }
- return await GetImageInternal(
- user.Id,
- imageType,
- imageIndex,
- tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- addPlayedIndicator,
- blur,
- backgroundColor,
- foregroundLayer,
- null,
- info)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Generates or gets the splashscreen.
+ /// </summary>
+ /// <param name="tag">Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="fillWidth">Width of box to fill.</param>
+ /// <param name="fillHeight">Height of box to fill.</param>
+ /// <param name="blur">Blur image.</param>
+ /// <param name="backgroundColor">Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Apply a foreground layer on top of the image.</param>
+ /// <param name="quality">Quality setting, from 0-100.</param>
+ /// <response code="200">Splashscreen returned successfully.</response>
+ /// <returns>The splashscreen.</returns>
+ [HttpGet("Branding/Splashscreen")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetSplashscreen(
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? fillWidth,
+ [FromQuery] int? fillHeight,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer,
+ [FromQuery, Range(0, 100)] int quality = 90)
+ {
+ var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+ if (!brandingOptions.SplashscreenEnabled)
+ {
+ return NotFound();
}
- /// <summary>
- /// Generates or gets the splashscreen.
- /// </summary>
- /// <param name="tag">Supply the cache tag from the item object to receive strong caching headers.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="blur">Blur image.</param>
- /// <param name="backgroundColor">Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Apply a foreground layer on top of the image.</param>
- /// <param name="quality">Quality setting, from 0-100.</param>
- /// <response code="200">Splashscreen returned successfully.</response>
- /// <returns>The splashscreen.</returns>
- [HttpGet("Branding/Splashscreen")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesImageFile]
- public async Task<ActionResult> GetSplashscreen(
- [FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery, Range(0, 100)] int quality = 90)
+ string splashscreenPath;
+
+ if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
+ && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
{
- var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- if (!brandingOptions.SplashscreenEnabled)
+ splashscreenPath = brandingOptions.SplashscreenLocation;
+ }
+ else
+ {
+ splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
+ if (!System.IO.File.Exists(splashscreenPath))
{
return NotFound();
}
+ }
- string splashscreenPath;
+ var outputFormats = GetOutputFormats(format);
- if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
- && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
+ TimeSpan? cacheDuration = null;
+ if (!string.IsNullOrEmpty(tag))
+ {
+ cacheDuration = TimeSpan.FromDays(365);
+ }
+
+ var options = new ImageProcessingOptions
+ {
+ Image = new ItemImageInfo
+ {
+ Path = splashscreenPath
+ },
+ Height = height,
+ MaxHeight = maxHeight,
+ MaxWidth = maxWidth,
+ FillHeight = fillHeight,
+ FillWidth = fillWidth,
+ Quality = quality,
+ Width = width,
+ Blur = blur,
+ BackgroundColor = backgroundColor,
+ ForegroundLayer = foregroundLayer,
+ SupportedOutputFormats = outputFormats
+ };
+
+ return await GetImageResult(
+ options,
+ cacheDuration,
+ ImmutableDictionary<string, string>.Empty)
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Uploads a custom splashscreen.
+ /// The body is expected to the image contents base64 encoded.
+ /// </summary>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ /// <response code="204">Successfully uploaded new splashscreen.</response>
+ /// <response code="400">Error reading MimeType from uploaded image.</response>
+ /// <response code="403">User does not have permission to upload splashscreen..</response>
+ /// <exception cref="ArgumentException">Error reading the image format.</exception>
+ [HttpPost("Branding/Splashscreen")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [AcceptsImageFile]
+ public async Task<ActionResult> UploadCustomSplashscreen()
+ {
+ var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType;
+
+ if (!mimeType.HasValue)
{
- splashscreenPath = brandingOptions.SplashscreenLocation;
+ return BadRequest("Error reading mimetype from uploaded image");
}
- else
+
+ var extension = MimeTypes.ToExtension(mimeType.Value);
+ if (string.IsNullOrEmpty(extension))
{
- splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
- if (!System.IO.File.Exists(splashscreenPath))
- {
- return NotFound();
- }
+ return BadRequest("Error converting mimetype to an image extension");
}
- var outputFormats = GetOutputFormats(format);
+ var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
+ var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+ brandingOptions.SplashscreenLocation = filePath;
+ _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
- TimeSpan? cacheDuration = null;
- if (!string.IsNullOrEmpty(tag))
+ var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+ await using (fs.ConfigureAwait(false))
{
- cacheDuration = TimeSpan.FromDays(365);
+ await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
}
- var options = new ImageProcessingOptions
- {
- Image = new ItemImageInfo
- {
- Path = splashscreenPath
- },
- Height = height,
- MaxHeight = maxHeight,
- MaxWidth = maxWidth,
- FillHeight = fillHeight,
- FillWidth = fillWidth,
- Quality = quality,
- Width = width,
- Blur = blur,
- BackgroundColor = backgroundColor,
- ForegroundLayer = foregroundLayer,
- SupportedOutputFormats = outputFormats
- };
+ return NoContent();
+ }
+ }
- return await GetImageResult(
- options,
- cacheDuration,
- ImmutableDictionary<string, string>.Empty)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Delete a custom splashscreen.
+ /// </summary>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ /// <response code="204">Successfully deleted the custom splashscreen.</response>
+ /// <response code="403">User does not have permission to delete splashscreen..</response>
+ [HttpDelete("Branding/Splashscreen")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult DeleteCustomSplashscreen()
+ {
+ var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
+ if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation)
+ && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
+ {
+ System.IO.File.Delete(brandingOptions.SplashscreenLocation);
+ brandingOptions.SplashscreenLocation = null;
+ _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
}
- /// <summary>
- /// Uploads a custom splashscreen.
- /// The body is expected to the image contents base64 encoded.
- /// </summary>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- /// <response code="204">Successfully uploaded new splashscreen.</response>
- /// <response code="400">Error reading MimeType from uploaded image.</response>
- /// <response code="403">User does not have permission to upload splashscreen..</response>
- /// <exception cref="ArgumentException">Error reading the image format.</exception>
- [HttpPost("Branding/Splashscreen")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [AcceptsImageFile]
- public async Task<ActionResult> UploadCustomSplashscreen()
- {
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
- {
- var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType;
+ return NoContent();
+ }
- if (!mimeType.HasValue)
- {
- return BadRequest("Error reading mimetype from uploaded image");
- }
+ private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
+ {
+ using var reader = new StreamReader(inputStream);
+ var text = await reader.ReadToEndAsync().ConfigureAwait(false);
- var extension = MimeTypes.ToExtension(mimeType.Value);
- if (string.IsNullOrEmpty(extension))
- {
- return BadRequest("Error converting mimetype to an image extension");
- }
+ var bytes = Convert.FromBase64String(text);
+ return new MemoryStream(bytes, 0, bytes.Length, false, true);
+ }
- var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
- var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- brandingOptions.SplashscreenLocation = filePath;
- _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
+ private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
+ {
+ int? width = null;
+ int? height = null;
+ string? blurhash = null;
+ long length = 0;
+
+ try
+ {
+ if (info.IsLocalFile)
+ {
+ var fileInfo = _fileSystem.GetFileInfo(info.Path);
+ length = fileInfo.Length;
- var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
- await using (fs.ConfigureAwait(false))
+ blurhash = info.BlurHash;
+ width = info.Width;
+ height = info.Height;
+
+ if (width <= 0 || height <= 0)
{
- await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
+ width = null;
+ height = null;
}
-
- return NoContent();
}
}
-
- /// <summary>
- /// Delete a custom splashscreen.
- /// </summary>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- /// <response code="204">Successfully deleted the custom splashscreen.</response>
- /// <response code="403">User does not have permission to delete splashscreen..</response>
- [HttpDelete("Branding/Splashscreen")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult DeleteCustomSplashscreen()
+ catch (Exception ex)
{
- var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
- if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation)
- && System.IO.File.Exists(brandingOptions.SplashscreenLocation))
- {
- System.IO.File.Delete(brandingOptions.SplashscreenLocation);
- brandingOptions.SplashscreenLocation = null;
- _serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
- }
-
- return NoContent();
+ _logger.LogError(ex, "Error getting image information for {Item}", item.Name);
}
- private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
+ try
{
- using var reader = new StreamReader(inputStream);
- var text = await reader.ReadToEndAsync().ConfigureAwait(false);
-
- var bytes = Convert.FromBase64String(text);
- return new MemoryStream(bytes, 0, bytes.Length, false, true);
+ return new ImageInfo
+ {
+ Path = info.Path,
+ ImageIndex = imageIndex,
+ ImageType = info.Type,
+ ImageTag = _imageProcessor.GetImageCacheTag(item, info),
+ Size = length,
+ BlurHash = blurhash,
+ Width = width,
+ Height = height
+ };
}
-
- private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
+ catch (Exception ex)
{
- int? width = null;
- int? height = null;
- string? blurhash = null;
- long length = 0;
-
- try
- {
- if (info.IsLocalFile)
- {
- var fileInfo = _fileSystem.GetFileInfo(info.Path);
- length = fileInfo.Length;
-
- blurhash = info.BlurHash;
- width = info.Width;
- height = info.Height;
-
- if (width <= 0 || height <= 0)
- {
- width = null;
- height = null;
- }
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting image information for {Item}", item.Name);
- }
+ _logger.LogError(ex, "Error getting image information for {Path}", info.Path);
+ return null;
+ }
+ }
- try
+ private async Task<ActionResult> GetImageInternal(
+ Guid itemId,
+ ImageType imageType,
+ int? imageIndex,
+ string? tag,
+ ImageFormat? format,
+ int? maxWidth,
+ int? maxHeight,
+ double? percentPlayed,
+ int? unplayedCount,
+ int? width,
+ int? height,
+ int? quality,
+ int? fillWidth,
+ int? fillHeight,
+ int? blur,
+ string? backgroundColor,
+ string? foregroundLayer,
+ BaseItem? item,
+ ItemImageInfo? imageInfo = null)
+ {
+ if (percentPlayed.HasValue)
+ {
+ if (percentPlayed.Value <= 0)
{
- return new ImageInfo
- {
- Path = info.Path,
- ImageIndex = imageIndex,
- ImageType = info.Type,
- ImageTag = _imageProcessor.GetImageCacheTag(item, info),
- Size = length,
- BlurHash = blurhash,
- Width = width,
- Height = height
- };
+ percentPlayed = null;
}
- catch (Exception ex)
+ else if (percentPlayed.Value >= 100)
{
- _logger.LogError(ex, "Error getting image information for {Path}", info.Path);
- return null;
+ percentPlayed = null;
}
}
- private async Task<ActionResult> GetImageInternal(
- Guid itemId,
- ImageType imageType,
- int? imageIndex,
- string? tag,
- ImageFormat? format,
- int? maxWidth,
- int? maxHeight,
- double? percentPlayed,
- int? unplayedCount,
- int? width,
- int? height,
- int? quality,
- int? fillWidth,
- int? fillHeight,
- bool? addPlayedIndicator,
- int? blur,
- string? backgroundColor,
- string? foregroundLayer,
- BaseItem? item,
- ItemImageInfo? imageInfo = null)
- {
- if (percentPlayed.HasValue)
- {
- if (percentPlayed.Value <= 0)
- {
- percentPlayed = null;
- }
- else if (percentPlayed.Value >= 100)
- {
- percentPlayed = null;
- addPlayedIndicator = true;
- }
- }
-
- if (percentPlayed.HasValue)
- {
- unplayedCount = null;
- }
+ if (percentPlayed.HasValue)
+ {
+ unplayedCount = null;
+ }
- if (unplayedCount.HasValue
- && unplayedCount.Value <= 0)
- {
- unplayedCount = null;
- }
+ if (unplayedCount.HasValue
+ && unplayedCount.Value <= 0)
+ {
+ unplayedCount = null;
+ }
+ if (imageInfo is null)
+ {
+ imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0);
if (imageInfo is null)
{
- imageInfo = item?.GetImageInfo(imageType, imageIndex ?? 0);
- if (imageInfo is null)
- {
- return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item?.Name, imageType));
- }
+ return NotFound(string.Format(NumberFormatInfo.InvariantInfo, "{0} does not have an image of type {1}", item?.Name, imageType));
}
+ }
- var outputFormats = GetOutputFormats(format);
-
- TimeSpan? cacheDuration = null;
+ var outputFormats = GetOutputFormats(format);
- if (!string.IsNullOrEmpty(tag))
- {
- cacheDuration = TimeSpan.FromDays(365);
- }
-
- var responseHeaders = new Dictionary<string, string>
- {
- { "transferMode.dlna.org", "Interactive" },
- { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
- };
+ TimeSpan? cacheDuration = null;
- if (!imageInfo.IsLocalFile && item is not null)
- {
- imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false);
- }
+ if (!string.IsNullOrEmpty(tag))
+ {
+ cacheDuration = TimeSpan.FromDays(365);
+ }
- var options = new ImageProcessingOptions
- {
- Height = height,
- ImageIndex = imageIndex ?? 0,
- Image = imageInfo,
- Item = item,
- ItemId = itemId,
- MaxHeight = maxHeight,
- MaxWidth = maxWidth,
- FillHeight = fillHeight,
- FillWidth = fillWidth,
- Quality = quality ?? 100,
- Width = width,
- AddPlayedIndicator = addPlayedIndicator ?? false,
- PercentPlayed = percentPlayed ?? 0,
- UnplayedCount = unplayedCount,
- Blur = blur,
- BackgroundColor = backgroundColor,
- ForegroundLayer = foregroundLayer,
- SupportedOutputFormats = outputFormats
- };
+ var responseHeaders = new Dictionary<string, string>
+ {
+ { "transferMode.dlna.org", "Interactive" },
+ { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" }
+ };
- return await GetImageResult(
- options,
- cacheDuration,
- responseHeaders).ConfigureAwait(false);
+ if (!imageInfo.IsLocalFile && item is not null)
+ {
+ imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false);
}
- private ImageFormat[] GetOutputFormats(ImageFormat? format)
+ var options = new ImageProcessingOptions
{
- if (format.HasValue)
- {
- return new[] { format.Value };
- }
+ Height = height,
+ ImageIndex = imageIndex ?? 0,
+ Image = imageInfo,
+ Item = item,
+ ItemId = itemId,
+ MaxHeight = maxHeight,
+ MaxWidth = maxWidth,
+ FillHeight = fillHeight,
+ FillWidth = fillWidth,
+ Quality = quality ?? 100,
+ Width = width,
+ PercentPlayed = percentPlayed ?? 0,
+ UnplayedCount = unplayedCount,
+ Blur = blur,
+ BackgroundColor = backgroundColor,
+ ForegroundLayer = foregroundLayer,
+ SupportedOutputFormats = outputFormats
+ };
+
+ return await GetImageResult(
+ options,
+ cacheDuration,
+ responseHeaders).ConfigureAwait(false);
+ }
- return GetClientSupportedFormats();
+ private ImageFormat[] GetOutputFormats(ImageFormat? format)
+ {
+ if (format.HasValue)
+ {
+ return new[] { format.Value };
}
- private ImageFormat[] GetClientSupportedFormats()
+ return GetClientSupportedFormats();
+ }
+
+ private ImageFormat[] GetClientSupportedFormats()
+ {
+ var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
+ for (var i = 0; i < supportedFormats.Length; i++)
{
- var supportedFormats = Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
- for (var i = 0; i < supportedFormats.Length; i++)
+ // Remove charsets etc. (anything after semi-colon)
+ var type = supportedFormats[i];
+ int index = type.IndexOf(';', StringComparison.Ordinal);
+ if (index != -1)
{
- // Remove charsets etc. (anything after semi-colon)
- var type = supportedFormats[i];
- int index = type.IndexOf(';', StringComparison.Ordinal);
- if (index != -1)
- {
- supportedFormats[i] = type.Substring(0, index);
- }
+ supportedFormats[i] = type.Substring(0, index);
}
+ }
- var acceptParam = Request.Query[HeaderNames.Accept];
+ var acceptParam = Request.Query[HeaderNames.Accept];
- var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
+ var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
- if (!supportsWebP)
+ if (!supportsWebP)
+ {
+ var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
+ if (userAgent.Contains("crosswalk", StringComparison.OrdinalIgnoreCase)
+ && userAgent.Contains("android", StringComparison.OrdinalIgnoreCase))
{
- var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
- if (userAgent.Contains("crosswalk", StringComparison.OrdinalIgnoreCase)
- && userAgent.Contains("android", StringComparison.OrdinalIgnoreCase))
- {
- supportsWebP = true;
- }
+ supportsWebP = true;
}
+ }
- var formats = new List<ImageFormat>(4);
+ var formats = new List<ImageFormat>(4);
- if (supportsWebP)
- {
- formats.Add(ImageFormat.Webp);
- }
+ if (supportsWebP)
+ {
+ formats.Add(ImageFormat.Webp);
+ }
- formats.Add(ImageFormat.Jpg);
- formats.Add(ImageFormat.Png);
+ formats.Add(ImageFormat.Jpg);
+ formats.Add(ImageFormat.Png);
- if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
- {
- formats.Add(ImageFormat.Gif);
- }
+ if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
+ {
+ formats.Add(ImageFormat.Gif);
+ }
+
+ return formats.ToArray();
+ }
- return formats.ToArray();
+ private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string? acceptParam, ImageFormat format, bool acceptAll)
+ {
+ if (requestAcceptTypes.Contains(format.GetMimeType()))
+ {
+ return true;
}
- private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string? acceptParam, ImageFormat format, bool acceptAll)
+ if (acceptAll && requestAcceptTypes.Contains("*/*"))
{
- if (requestAcceptTypes.Contains(format.GetMimeType()))
- {
- return true;
- }
+ return true;
+ }
- if (acceptAll && requestAcceptTypes.Contains("*/*"))
- {
- return true;
- }
+ // Review if this should be jpeg, jpg or both for ImageFormat.Jpg
+ var normalized = format.ToString().ToLowerInvariant();
+ return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
+ }
- // Review if this should be jpeg, jpg or both for ImageFormat.Jpg
- var normalized = format.ToString().ToLowerInvariant();
- return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
+ private async Task<ActionResult> GetImageResult(
+ ImageProcessingOptions imageProcessingOptions,
+ TimeSpan? cacheDuration,
+ IDictionary<string, string> headers)
+ {
+ var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions).ConfigureAwait(false);
+
+ var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache");
+ var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader);
+
+ // if the parsing of the IfModifiedSince header was not successful, disable caching
+ if (!parsingSuccessful)
+ {
+ // disableCaching = true;
}
- private async Task<ActionResult> GetImageResult(
- ImageProcessingOptions imageProcessingOptions,
- TimeSpan? cacheDuration,
- IDictionary<string, string> headers)
+ foreach (var (key, value) in headers)
{
- var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions).ConfigureAwait(false);
+ Response.Headers.Add(key, value);
+ }
- var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache");
- var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader);
+ Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
+ Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
+ Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
- // if the parsing of the IfModifiedSince header was not successful, disable caching
- if (!parsingSuccessful)
+ if (disableCaching)
+ {
+ Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
+ Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
+ }
+ else
+ {
+ if (cacheDuration.HasValue)
{
- // disableCaching = true;
+ Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
}
-
- foreach (var (key, value) in headers)
+ else
{
- Response.Headers.Add(key, value);
+ Response.Headers.Add(HeaderNames.CacheControl, "public");
}
- Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
- Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
- Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
+ Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture));
- if (disableCaching)
+ // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
+ if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
{
- Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
- Response.Headers.Add(HeaderNames.Pragma, "no-cache, no-store, must-revalidate");
- }
- else
- {
- if (cacheDuration.HasValue)
- {
- Response.Headers.Add(HeaderNames.CacheControl, "public, max-age=" + cacheDuration.Value.TotalSeconds);
- }
- else
+ if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow)
{
- Response.Headers.Add(HeaderNames.CacheControl, "public");
- }
-
- Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture));
-
- // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
- if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
- {
- if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow)
- {
- Response.StatusCode = StatusCodes.Status304NotModified;
- return new ContentResult();
- }
+ Response.StatusCode = StatusCodes.Status304NotModified;
+ return new ContentResult();
}
}
-
- return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
}
+
+ return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
}
}
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 2e0d3cb99..43f09b49a 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
@@ -16,346 +15,345 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The instant mix controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class InstantMixController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IMusicManager _musicManager;
+
/// <summary>
- /// The instant mix controller.
+ /// Initializes a new instance of the <see cref="InstantMixController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class InstantMixController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public InstantMixController(
+ IUserManager userManager,
+ IDtoService dtoService,
+ IMusicManager musicManager,
+ ILibraryManager libraryManager)
{
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
- private readonly ILibraryManager _libraryManager;
- private readonly IMusicManager _musicManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="InstantMixController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public InstantMixController(
- IUserManager userManager,
- IDtoService dtoService,
- IMusicManager musicManager,
- ILibraryManager libraryManager)
- {
- _userManager = userManager;
- _dtoService = dtoService;
- _musicManager = musicManager;
- _libraryManager = libraryManager;
- }
+ _userManager = userManager;
+ _dtoService = dtoService;
+ _musicManager = musicManager;
+ _libraryManager = libraryManager;
+ }
- /// <summary>
- /// Creates an instant playlist based on a given song.
- /// </summary>
- /// <param name="id">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableImages">Optional. Include image information in output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <response code="200">Instant playlist returned.</response>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
- [HttpGet("Songs/{id}/InstantMix")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
- [FromRoute, Required] Guid id,
- [FromQuery] Guid? userId,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableImages,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
- {
- var item = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
- return GetResult(items, user, limit, dtoOptions);
- }
+ /// <summary>
+ /// Creates an instant playlist based on a given song.
+ /// </summary>
+ /// <param name="id">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <response code="200">Instant playlist returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+ [HttpGet("Songs/{id}/InstantMix")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
+ [FromRoute, Required] Guid id,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ {
+ var item = _libraryManager.GetItemById(id);
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
+ return GetResult(items, user, limit, dtoOptions);
+ }
- /// <summary>
- /// Creates an instant playlist based on a given album.
- /// </summary>
- /// <param name="id">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableImages">Optional. Include image information in output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <response code="200">Instant playlist returned.</response>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
- [HttpGet("Albums/{id}/InstantMix")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
- [FromRoute, Required] Guid id,
- [FromQuery] Guid? userId,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableImages,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
- {
- var album = _libraryManager.GetItemById(id);
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
- return GetResult(items, user, limit, dtoOptions);
- }
+ /// <summary>
+ /// Creates an instant playlist based on a given 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 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 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 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 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 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);
- }
+ /// <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;
+ private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
+ {
+ var list = items;
- var totalCount = list.Count;
+ var totalCount = list.Count;
- if (limit.HasValue && limit < list.Count)
- {
- list = list.GetRange(0, limit.Value);
- }
+ if (limit.HasValue && limit < list.Count)
+ {
+ list = list.GetRange(0, limit.Value);
+ }
- var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
+ var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
- var result = new QueryResult<BaseItemDto>(
- 0,
- totalCount,
- returnList);
+ var result = new QueryResult<BaseItemDto>(
+ 0,
+ totalCount,
+ returnList);
- return result;
- }
+ return result;
}
}
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index b6c5504db..b030e74dd 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
@@ -18,257 +17,256 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Item lookup controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class ItemLookupController : BaseJellyfinApiController
{
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<ItemLookupController> _logger;
+
/// <summary>
- /// Item lookup controller.
+ /// Initializes a new instance of the <see cref="ItemLookupController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ItemLookupController : BaseJellyfinApiController
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param>
+ public ItemLookupController(
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ ILogger<ItemLookupController> logger)
{
- private readonly IProviderManager _providerManager;
- private readonly IFileSystem _fileSystem;
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<ItemLookupController> _logger;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ItemLookupController"/> class.
- /// </summary>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param>
- public ItemLookupController(
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILibraryManager libraryManager,
- ILogger<ItemLookupController> logger)
+ /// <summary>
+ /// Get the item's external id info.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">External id info retrieved.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>List of external id info.</returns>
+ [HttpGet("Items/{itemId}/ExternalIdInfos")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- _providerManager = providerManager;
- _fileSystem = fileSystem;
- _libraryManager = libraryManager;
- _logger = logger;
+ return NotFound();
}
- /// <summary>
- /// Get the item's external id info.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">External id info retrieved.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>List of external id info.</returns>
- [HttpGet("Items/{itemId}/ExternalIdInfos")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
-
- return Ok(_providerManager.GetExternalIdInfos(item));
- }
+ return Ok(_providerManager.GetExternalIdInfos(item));
+ }
- /// <summary>
- /// Get movie remote search.
- /// </summary>
- /// <param name="query">Remote search query.</param>
- /// <response code="200">Movie remote search executed.</response>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
- /// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
- /// </returns>
- [HttpPost("Items/RemoteSearch/Movie")]
- public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MovieInfo> query)
- {
- var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None)
- .ConfigureAwait(false);
- return Ok(results);
- }
+ /// <summary>
+ /// Get 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 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 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 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 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 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 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 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>
+ /// 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);
+ /// <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);
+ // Since the refresh process won't erase provider Ids, we need to set this explicitly now.
+ item.ProviderIds = searchResult.ProviderIds;
+ await _providerManager.RefreshFullItem(
+ item,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllMetadata = true,
+ ReplaceAllImages = replaceAllImages,
+ SearchResult = searchResult,
+ RemoveOldMetadata = true
+ },
+ CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index 0dc3fbd05..b8f6e91ad 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -9,78 +9,77 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Item Refresh Controller.
+/// </summary>
+[Route("Items")]
+[Authorize(Policy = Policies.RequiresElevation)]
+public class ItemRefreshController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+
/// <summary>
- /// Item Refresh Controller.
+ /// Initializes a new instance of the <see cref="ItemRefreshController"/> class.
/// </summary>
- [Route("Items")]
- [Authorize(Policy = Policies.RequiresElevation)]
- public class ItemRefreshController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
+ public ItemRefreshController(
+ ILibraryManager libraryManager,
+ IProviderManager providerManager,
+ IFileSystem fileSystem)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IProviderManager _providerManager;
- private readonly IFileSystem _fileSystem;
+ _libraryManager = libraryManager;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ItemRefreshController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
- /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
- public ItemRefreshController(
- ILibraryManager libraryManager,
- IProviderManager providerManager,
- IFileSystem fileSystem)
+ /// <summary>
+ /// Refreshes metadata for an item.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param>
+ /// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
+ /// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
+ /// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
+ /// <response code="204">Item metadata refresh queued.</response>
+ /// <response code="404">Item to refresh not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+ [HttpPost("{itemId}/Refresh")]
+ [Description("Refreshes metadata for an item.")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult RefreshItem(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
+ [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
+ [FromQuery] bool replaceAllMetadata = false,
+ [FromQuery] bool replaceAllImages = false)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- _libraryManager = libraryManager;
- _providerManager = providerManager;
- _fileSystem = fileSystem;
+ return NotFound();
}
- /// <summary>
- /// Refreshes metadata for an item.
- /// </summary>
- /// <param name="itemId">Item id.</param>
- /// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param>
- /// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
- /// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
- /// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
- /// <response code="204">Item metadata refresh queued.</response>
- /// <response code="404">Item to refresh not found.</response>
- /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
- [HttpPost("{itemId}/Refresh")]
- [Description("Refreshes metadata for an item.")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult RefreshItem(
- [FromRoute, Required] Guid itemId,
- [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
- [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
- [FromQuery] bool replaceAllMetadata = false,
- [FromQuery] bool replaceAllImages = false)
+ var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ MetadataRefreshMode = metadataRefreshMode,
+ ImageRefreshMode = imageRefreshMode,
+ ReplaceAllImages = replaceAllImages,
+ ReplaceAllMetadata = replaceAllMetadata,
+ ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh
+ || imageRefreshMode == MetadataRefreshMode.FullRefresh
+ || replaceAllImages
+ || replaceAllMetadata,
+ IsAutomated = false
+ };
- var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- MetadataRefreshMode = metadataRefreshMode,
- ImageRefreshMode = imageRefreshMode,
- ReplaceAllImages = replaceAllImages,
- ReplaceAllMetadata = replaceAllMetadata,
- ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh
- || imageRefreshMode == MetadataRefreshMode.FullRefresh
- || replaceAllImages
- || replaceAllMetadata,
- IsAutomated = false
- };
-
- _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
- return NoContent();
- }
+ _providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index af3d779f5..230fbfb2c 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -20,332 +20,332 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <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>
+ /// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
+ /// </summary>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public ItemUpdateController(
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager,
+ IProviderManager providerManager,
+ ILocalizationManager localizationManager,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _libraryManager = libraryManager;
+ _providerManager = providerManager;
+ _localizationManager = localizationManager;
+ _fileSystem = fileSystem;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
/// <summary>
- /// Item update controller.
+ /// Updates an item.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.RequiresElevation)]
- public class ItemUpdateController : BaseJellyfinApiController
+ /// <param name="itemId">The item id.</param>
+ /// <param name="request">The new item properties.</param>
+ /// <response code="204">Item updated.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
+ [HttpPost("Items/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IProviderManager _providerManager;
- private readonly ILocalizationManager _localizationManager;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
- /// </summary>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public ItemUpdateController(
- IFileSystem fileSystem,
- ILibraryManager libraryManager,
- IProviderManager providerManager,
- ILocalizationManager localizationManager,
- IServerConfigurationManager serverConfigurationManager)
- {
- _libraryManager = libraryManager;
- _providerManager = providerManager;
- _localizationManager = localizationManager;
- _fileSystem = fileSystem;
- _serverConfigurationManager = serverConfigurationManager;
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Updates an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="request">The new item properties.</param>
- /// <response code="204">Item updated.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
- [HttpPost("Items/{itemId}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ var newLockData = request.LockData ?? false;
+ var isLockedChanged = item.IsLocked != newLockData;
- 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);
- 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());
+ }
- // 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);
- UpdateItem(request, item);
+ item.OnMetadataChanged();
- item.OnMetadataChanged();
+ await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ if (isLockedChanged && item.IsFolder)
+ {
+ var folder = (Folder)item;
- if (isLockedChanged && item.IsFolder)
+ foreach (var child in folder.GetRecursiveChildren())
{
- var folder = (Folder)item;
+ child.IsLocked = newLockData;
+ await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ }
+ }
- foreach (var child in folder.GetRecursiveChildren())
+ if (displayOrderChanged)
+ {
+ _providerManager.QueueRefresh(
+ series!.Id,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
- child.IsLocked = newLockData;
- await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- }
- }
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ReplaceAllMetadata = true
+ },
+ RefreshPriority.High);
+ }
- if (displayOrderChanged)
- {
- _providerManager.QueueRefresh(
- series!.Id,
- new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
- ImageRefreshMode = MetadataRefreshMode.FullRefresh,
- ReplaceAllMetadata = true
- },
- RefreshPriority.High);
- }
+ return NoContent();
+ }
- 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);
- /// <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
- {
- ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
- ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
- Countries = _localizationManager.GetCountries().ToArray(),
- Cultures = _localizationManager.GetCultures().ToArray()
- };
-
- if (!item.IsVirtualItem
- && item is not ICollectionFolder
- && item is not UserView
- && item is not AggregateFolder
- && item is not LiveTvChannel
- && item is not IItemByName
- && item.SourceType == SourceType.Library)
+ var info = new MetadataEditorInfo
+ {
+ ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
+ ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
+ Countries = _localizationManager.GetCountries().ToArray(),
+ Cultures = _localizationManager.GetCultures().ToArray()
+ };
+
+ if (!item.IsVirtualItem
+ && item is not ICollectionFolder
+ && item is not UserView
+ && item is not AggregateFolder
+ && item is not LiveTvChannel
+ && item is not IItemByName
+ && item.SourceType == SourceType.Library)
+ {
+ var inheritedContentType = _libraryManager.GetInheritedContentType(item);
+ var configuredContentType = _libraryManager.GetConfiguredContentType(item);
+
+ if (string.IsNullOrWhiteSpace(inheritedContentType) ||
+ !string.IsNullOrWhiteSpace(configuredContentType))
{
- var inheritedContentType = _libraryManager.GetInheritedContentType(item);
- var configuredContentType = _libraryManager.GetConfiguredContentType(item);
+ info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
+ info.ContentType = configuredContentType;
- if (string.IsNullOrWhiteSpace(inheritedContentType) ||
- !string.IsNullOrWhiteSpace(configuredContentType))
+ if (string.IsNullOrWhiteSpace(inheritedContentType)
+ || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
- 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();
- }
+ info.ContentTypeOptions = info.ContentTypeOptions
+ .Where(i => string.IsNullOrWhiteSpace(i.Value)
+ || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
}
}
-
- 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();
- }
+ return info;
+ }
- var path = item.ContainingFolderPath;
+ /// <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 types = _serverConfigurationManager.Configuration.ContentTypes
- .Where(i => !string.IsNullOrWhiteSpace(i.Name))
- .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
- .ToList();
+ var path = item.ContainingFolderPath;
- if (!string.IsNullOrWhiteSpace(contentType))
- {
- types.Add(new NameValuePair
- {
- Name = path,
- Value = contentType
- });
- }
+ var types = _serverConfigurationManager.Configuration.ContentTypes
+ .Where(i => !string.IsNullOrWhiteSpace(i.Name))
+ .Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
+ .ToList();
- _serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
- _serverConfigurationManager.SaveConfiguration();
- return NoContent();
+ if (!string.IsNullOrWhiteSpace(contentType))
+ {
+ types.Add(new NameValuePair
+ {
+ Name = path,
+ Value = contentType
+ });
}
- private void UpdateItem(BaseItemDto request, BaseItem item)
- {
- item.Name = request.Name;
- item.ForcedSortName = request.ForcedSortName;
+ _serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
+ _serverConfigurationManager.SaveConfiguration();
+ return NoContent();
+ }
- item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle;
+ private void UpdateItem(BaseItemDto request, BaseItem item)
+ {
+ item.Name = request.Name;
+ item.ForcedSortName = request.ForcedSortName;
- item.CriticRating = request.CriticRating;
+ item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle;
- item.CommunityRating = request.CommunityRating;
- item.IndexNumber = request.IndexNumber;
- item.ParentIndexNumber = request.ParentIndexNumber;
- item.Overview = request.Overview;
- item.Genres = request.Genres;
+ item.CriticRating = request.CriticRating;
- if (item is Episode episode)
- {
- episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber;
- episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber;
- episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
- }
+ item.CommunityRating = request.CommunityRating;
+ item.IndexNumber = request.IndexNumber;
+ item.ParentIndexNumber = request.ParentIndexNumber;
+ item.Overview = request.Overview;
+ item.Genres = request.Genres;
- item.Tags = request.Tags;
+ if (item is Episode episode)
+ {
+ episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber;
+ episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber;
+ episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
+ }
- if (request.Taglines is not null)
- {
- item.Tagline = request.Taglines.FirstOrDefault();
- }
+ item.Tags = request.Tags;
- if (request.Studios is not null)
- {
- item.Studios = request.Studios.Select(x => x.Name).ToArray();
- }
+ if (request.Taglines is not null)
+ {
+ item.Tagline = request.Taglines.FirstOrDefault();
+ }
- if (request.DateCreated.HasValue)
- {
- item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
- }
+ if (request.Studios is not null)
+ {
+ item.Studios = request.Studios.Select(x => x.Name).ToArray();
+ }
- 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.DateCreated.HasValue)
+ {
+ item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
+ }
- if (request.ProductionLocations is not null)
- {
- item.ProductionLocations = request.ProductionLocations;
- }
+ 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;
- item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
- item.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
+ if (request.ProductionLocations is not null)
+ {
+ item.ProductionLocations = request.ProductionLocations;
+ }
- if (item is IHasDisplayOrder hasDisplayOrder)
- {
- hasDisplayOrder.DisplayOrder = request.DisplayOrder;
- }
+ item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
+ item.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
- if (item is IHasAspectRatio hasAspectRatio)
- {
- hasAspectRatio.AspectRatio = request.AspectRatio;
- }
+ if (item is IHasDisplayOrder hasDisplayOrder)
+ {
+ hasDisplayOrder.DisplayOrder = request.DisplayOrder;
+ }
+
+ if (item is IHasAspectRatio hasAspectRatio)
+ {
+ hasAspectRatio.AspectRatio = request.AspectRatio;
+ }
- item.IsLocked = request.LockData ?? false;
+ item.IsLocked = request.LockData ?? false;
- if (request.LockedFields is not null)
- {
- item.LockedFields = request.LockedFields;
- }
+ 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;
- }
+ // 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())
+ foreach (var pair in request.ProviderIds.ToList())
+ {
+ if (string.IsNullOrEmpty(pair.Value))
{
- if (string.IsNullOrEmpty(pair.Value))
- {
- request.ProviderIds.Remove(pair.Key);
- }
+ request.ProviderIds.Remove(pair.Key);
}
+ }
- item.ProviderIds = request.ProviderIds;
+ item.ProviderIds = request.ProviderIds;
- if (item is Video video)
- {
- video.Video3DFormat = request.Video3DFormat;
- }
+ if (item is Video video)
+ {
+ video.Video3DFormat = request.Video3DFormat;
+ }
- if (request.AlbumArtists is not null)
+ if (request.AlbumArtists is not null)
+ {
+ if (item is IHasAlbumArtist hasAlbumArtists)
{
- if (item is IHasAlbumArtist hasAlbumArtists)
- {
- hasAlbumArtists.AlbumArtists = request
- .AlbumArtists
- .Select(i => i.Name)
- .ToArray();
- }
+ hasAlbumArtists.AlbumArtists = request
+ .AlbumArtists
+ .Select(i => i.Name)
+ .ToArray();
}
+ }
- if (request.ArtistItems is not null)
+ if (request.ArtistItems is not null)
+ {
+ if (item is IHasArtist hasArtists)
{
- if (item is IHasArtist hasArtists)
- {
- hasArtists.Artists = request
- .ArtistItems
- .Select(i => i.Name)
- .ToArray();
- }
+ 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:
+ switch (item)
+ {
+ case Audio song:
+ song.Album = request.Album;
+ break;
+ case MusicVideo musicVideo:
+ musicVideo.Album = request.Album;
+ break;
+ case Series series:
{
series.Status = GetSeriesStatus(request);
@@ -357,93 +357,92 @@ namespace Jellyfin.Api.Controllers
break;
}
- }
}
+ }
- private SeriesStatus? GetSeriesStatus(BaseItemDto item)
+ private SeriesStatus? GetSeriesStatus(BaseItemDto item)
+ {
+ if (string.IsNullOrEmpty(item.Status))
{
- if (string.IsNullOrEmpty(item.Status))
- {
- return null;
- }
-
- return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
+ return null;
}
- private DateTime NormalizeDateTime(DateTime val)
- {
- return DateTime.SpecifyKind(val, DateTimeKind.Utc);
- }
+ return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
+ }
- private List<NameValuePair> GetContentTypeOptions(bool isForItem)
- {
- var list = new List<NameValuePair>();
+ private DateTime NormalizeDateTime(DateTime val)
+ {
+ return DateTime.SpecifyKind(val, DateTimeKind.Utc);
+ }
- if (isForItem)
- {
- list.Add(new NameValuePair
- {
- Name = "Inherit",
- Value = string.Empty
- });
- }
+ private List<NameValuePair> GetContentTypeOptions(bool isForItem)
+ {
+ var list = new List<NameValuePair>();
+ if (isForItem)
+ {
list.Add(new NameValuePair
{
- Name = "Movies",
- Value = "movies"
- });
- list.Add(new NameValuePair
- {
- Name = "Music",
- Value = "music"
- });
- list.Add(new NameValuePair
- {
- Name = "Shows",
- Value = "tvshows"
+ Name = "Inherit",
+ Value = string.Empty
});
+ }
- if (!isForItem)
- {
- list.Add(new NameValuePair
- {
- Name = "Books",
- Value = "books"
- });
- }
+ 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 = "HomeVideos",
- Value = "homevideos"
- });
- list.Add(new NameValuePair
- {
- Name = "MusicVideos",
- Value = "musicvideos"
- });
- list.Add(new NameValuePair
- {
- Name = "Photos",
- Value = "photos"
+ Name = "Books",
+ Value = "books"
});
+ }
- if (!isForItem)
- {
- list.Add(new NameValuePair
- {
- Name = "MixedContent",
- Value = string.Empty
- });
- }
+ list.Add(new NameValuePair
+ {
+ Name = "HomeVideos",
+ Value = "homevideos"
+ });
+ list.Add(new NameValuePair
+ {
+ Name = "MusicVideos",
+ Value = "musicvideos"
+ });
+ list.Add(new NameValuePair
+ {
+ Name = "Photos",
+ Value = "photos"
+ });
- foreach (var val in list)
+ if (!isForItem)
+ {
+ list.Add(new NameValuePair
{
- val.Name = _localizationManager.GetLocalizedString(val.Name);
- }
+ Name = "MixedContent",
+ Value = string.Empty
+ });
+ }
- return list;
+ foreach (var val in list)
+ {
+ val.Name = _localizationManager.GetLocalizedString(val.Name);
}
+
+ return list;
}
}
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 717ddc32b..99366e80c 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -1,12 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -20,854 +19,858 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The items controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class ItemsController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+ private readonly IDtoService _dtoService;
+ private readonly ILogger<ItemsController> _logger;
+ private readonly ISessionManager _sessionManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemsController"/> class.
+ /// </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="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ public ItemsController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ ILocalizationManager localization,
+ IDtoService dtoService,
+ ILogger<ItemsController> logger,
+ ISessionManager sessionManager)
+ {
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _localization = localization;
+ _dtoService = dtoService;
+ _logger = logger;
+ _sessionManager = sessionManager;
+ }
+
/// <summary>
- /// The items controller.
+ /// Gets items based on a query.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ItemsController : BaseJellyfinApiController
+ /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
+ /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
+ /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
+ /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
+ /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
+ /// <param name="hasTrailer">Optional filter by items with trailers.</param>
+ /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
+ /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
+ /// <param name="isHd">Optional filter by items that are HD or not.</param>
+ /// <param name="is4K">Optional filter by items that are 4K or not.</param>
+ /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
+ /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
+ /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+ /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
+ /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
+ /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
+ /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
+ /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
+ /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
+ /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
+ /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
+ /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
+ /// <param name="isMovie">Optional filter for live tv movies.</param>
+ /// <param name="isSeries">Optional filter for live tv series.</param>
+ /// <param name="isNews">Optional filter for live tv news.</param>
+ /// <param name="isKids">Optional filter for live tv kids.</param>
+ /// <param name="isSports">Optional filter for live tv sports.</param>
+ /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
+ /// <param name="searchTerm">Optional. Filter based on a search term.</param>
+ /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the 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 items.</returns>
+ [HttpGet("Items")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetItems(
+ [FromQuery] Guid? userId,
+ [FromQuery] string? maxOfficialRating,
+ [FromQuery] bool? hasThemeSong,
+ [FromQuery] bool? hasThemeVideo,
+ [FromQuery] bool? hasSubtitles,
+ [FromQuery] bool? hasSpecialFeature,
+ [FromQuery] bool? hasTrailer,
+ [FromQuery] Guid? adjacentTo,
+ [FromQuery] int? parentIndexNumber,
+ [FromQuery] bool? hasParentalRating,
+ [FromQuery] bool? isHd,
+ [FromQuery] bool? is4K,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
+ [FromQuery] bool? isMissing,
+ [FromQuery] bool? isUnaired,
+ [FromQuery] double? minCommunityRating,
+ [FromQuery] double? minCriticRating,
+ [FromQuery] DateTime? minPremiereDate,
+ [FromQuery] DateTime? minDateLastSaved,
+ [FromQuery] DateTime? minDateLastSavedForUser,
+ [FromQuery] DateTime? maxPremiereDate,
+ [FromQuery] bool? hasOverview,
+ [FromQuery] bool? hasImdbId,
+ [FromQuery] bool? hasTmdbId,
+ [FromQuery] bool? hasTvdbId,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? recursive,
+ [FromQuery] string? searchTerm,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery] bool? isPlayed,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
+ [FromQuery] string? minOfficialRating,
+ [FromQuery] bool? isLocked,
+ [FromQuery] bool? isPlaceHolder,
+ [FromQuery] bool? hasOfficialRating,
+ [FromQuery] bool? collapseBoxSetItems,
+ [FromQuery] int? minWidth,
+ [FromQuery] int? minHeight,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] bool? is3D,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localization;
- private readonly IDtoService _dtoService;
- private readonly ILogger<ItemsController> _logger;
- private readonly ISessionManager _sessionManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ItemsController"/> class.
- /// </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="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- public ItemsController(
- IUserManager userManager,
- ILibraryManager libraryManager,
- ILocalizationManager localization,
- IDtoService dtoService,
- ILogger<ItemsController> logger,
- ISessionManager sessionManager)
+ var isApiKey = User.GetIsApiKey();
+ // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
+ var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default)
+ ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
+ : null;
+
+ // beyond this point, we're either using an api key or we have a valid user
+ if (!isApiKey && user is null)
{
- _userManager = userManager;
- _libraryManager = libraryManager;
- _localization = localization;
- _dtoService = dtoService;
- _logger = logger;
- _sessionManager = sessionManager;
+ return BadRequest("userId is required");
}
- /// <summary>
- /// Gets items based on a query.
- /// </summary>
- /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
- /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
- /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
- /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
- /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
- /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
- /// <param name="hasTrailer">Optional filter by items with trailers.</param>
- /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
- /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
- /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
- /// <param name="isHd">Optional filter by items that are HD or not.</param>
- /// <param name="is4K">Optional filter by items that are 4K or not.</param>
- /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
- /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
- /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
- /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
- /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
- /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
- /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
- /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
- /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
- /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
- /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
- /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
- /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
- /// <param name="isMovie">Optional filter for live tv movies.</param>
- /// <param name="isSeries">Optional filter for live tv series.</param>
- /// <param name="isNews">Optional filter for live tv news.</param>
- /// <param name="isKids">Optional filter for live tv kids.</param>
- /// <param name="isSports">Optional filter for live tv sports.</param>
- /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
- /// <param name="searchTerm">Optional. Filter based on a search term.</param>
- /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the 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 items.</returns>
- [HttpGet("Items")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetItems(
- [FromQuery] Guid? userId,
- [FromQuery] string? maxOfficialRating,
- [FromQuery] bool? hasThemeSong,
- [FromQuery] bool? hasThemeVideo,
- [FromQuery] bool? hasSubtitles,
- [FromQuery] bool? hasSpecialFeature,
- [FromQuery] bool? hasTrailer,
- [FromQuery] Guid? adjacentTo,
- [FromQuery] int? parentIndexNumber,
- [FromQuery] bool? hasParentalRating,
- [FromQuery] bool? isHd,
- [FromQuery] bool? is4K,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
- [FromQuery] bool? isMissing,
- [FromQuery] bool? isUnaired,
- [FromQuery] double? minCommunityRating,
- [FromQuery] double? minCriticRating,
- [FromQuery] DateTime? minPremiereDate,
- [FromQuery] DateTime? minDateLastSaved,
- [FromQuery] DateTime? minDateLastSavedForUser,
- [FromQuery] DateTime? maxPremiereDate,
- [FromQuery] bool? hasOverview,
- [FromQuery] bool? hasImdbId,
- [FromQuery] bool? hasTmdbId,
- [FromQuery] bool? hasTvdbId,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool? recursive,
- [FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery] bool? isPlayed,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
- [FromQuery] string? minOfficialRating,
- [FromQuery] bool? isLocked,
- [FromQuery] bool? isPlaceHolder,
- [FromQuery] bool? hasOfficialRating,
- [FromQuery] bool? collapseBoxSetItems,
- [FromQuery] int? minWidth,
- [FromQuery] int? minHeight,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] bool? is3D,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool? enableImages = true)
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ if (includeItemTypes.Length == 1
+ && (includeItemTypes[0] == BaseItemKind.Playlist
+ || includeItemTypes[0] == BaseItemKind.BoxSet))
{
- var isApiKey = User.GetIsApiKey();
- // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
- var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default)
- ? _userManager.GetUserById(userId.Value)
- : null;
-
- // beyond this point, we're either using an api key or we have a valid user
- if (!isApiKey && user is null)
- {
- return BadRequest("userId is required");
- }
+ parentId = null;
+ }
+
+ var item = _libraryManager.GetParentItem(parentId, userId);
+ QueryResult<BaseItem> result;
+
+ if (item is not Folder folder)
+ {
+ folder = _libraryManager.GetUserRootFolder();
+ }
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ string? collectionType = null;
+ if (folder is IHasCollectionType hasCollectionType)
+ {
+ collectionType = hasCollectionType.CollectionType;
+ }
+
+ if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ {
+ recursive = true;
+ includeItemTypes = new[] { BaseItemKind.Playlist };
+ }
+
+ if (item is not UserRootFolder
+ // api keys can always access all folders
+ && !isApiKey
+ // check the item is visible for the user
+ && !item.IsVisible(user))
+ {
+ _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user!.Username, item.Name);
+ return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
+ }
+
+ if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder)
+ {
+ var query = new InternalItemsQuery(user)
+ {
+ IsPlayed = isPlayed,
+ MediaTypes = mediaTypes,
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
+ Recursive = recursive ?? false,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
+ IsFavorite = isFavorite,
+ Limit = limit,
+ StartIndex = startIndex,
+ IsMissing = isMissing,
+ IsUnaired = isUnaired,
+ CollapseBoxSetItems = collapseBoxSetItems,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ HasImdbId = hasImdbId,
+ IsPlaceHolder = isPlaceHolder,
+ IsLocked = isLocked,
+ MinWidth = minWidth,
+ MinHeight = minHeight,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
+ Is3D = is3D,
+ HasTvdbId = hasTvdbId,
+ HasTmdbId = hasTmdbId,
+ IsMovie = isMovie,
+ IsSeries = isSeries,
+ IsNews = isNews,
+ IsKids = isKids,
+ IsSports = isSports,
+ HasOverview = hasOverview,
+ HasOfficialRating = hasOfficialRating,
+ HasParentalRating = hasParentalRating,
+ HasSpecialFeature = hasSpecialFeature,
+ HasSubtitles = hasSubtitles,
+ HasThemeSong = hasThemeSong,
+ HasThemeVideo = hasThemeVideo,
+ HasTrailer = hasTrailer,
+ IsHD = isHd,
+ Is4K = is4K,
+ Tags = tags,
+ OfficialRatings = officialRatings,
+ Genres = genres,
+ ArtistIds = artistIds,
+ AlbumArtistIds = albumArtistIds,
+ ContributingArtistIds = contributingArtistIds,
+ GenreIds = genreIds,
+ StudioIds = studioIds,
+ Person = person,
+ PersonIds = personIds,
+ PersonTypes = personTypes,
+ Years = years,
+ ImageTypes = imageTypes,
+ VideoTypes = videoTypes,
+ AdjacentTo = adjacentTo,
+ ItemIds = ids,
+ MinCommunityRating = minCommunityRating,
+ MinCriticRating = minCriticRating,
+ ParentId = parentId ?? Guid.Empty,
+ ParentIndexNumber = parentIndexNumber,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ ExcludeItemIds = excludeItemIds,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
+ MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
+ MinPremiereDate = minPremiereDate?.ToUniversalTime(),
+ MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
+ };
- if (includeItemTypes.Length == 1
- && (includeItemTypes[0] == BaseItemKind.Playlist
- || includeItemTypes[0] == BaseItemKind.BoxSet))
+ if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
{
- parentId = null;
+ query.CollapseBoxSetItems = false;
}
- var item = _libraryManager.GetParentItem(parentId, userId);
- QueryResult<BaseItem> result;
+ 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;
+ }
+ }
- if (item is not Folder folder)
+ // Filter by Series Status
+ if (seriesStatus.Length != 0)
{
- folder = _libraryManager.GetUserRootFolder();
+ query.SeriesStatuses = seriesStatus;
}
- string? collectionType = null;
- if (folder is IHasCollectionType hasCollectionType)
+ // ExcludeLocationTypes
+ if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
{
- collectionType = hasCollectionType.CollectionType;
+ query.IsVirtualItem = false;
}
- if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ if (locationTypes.Length > 0 && locationTypes.Length < 4)
{
- recursive = true;
- includeItemTypes = new[] { BaseItemKind.Playlist };
+ query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
}
- if (item is not UserRootFolder
- // api keys can always access all folders
- && !isApiKey
- // check the item is visible for the user
- && !item.IsVisible(user))
+ // Min official rating
+ if (!string.IsNullOrWhiteSpace(minOfficialRating))
{
- _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user!.Username, item.Name);
- return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
+ query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating);
}
- if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder)
+ // Max official rating
+ if (!string.IsNullOrWhiteSpace(maxOfficialRating))
{
- var query = new InternalItemsQuery(user)
- {
- IsPlayed = isPlayed,
- MediaTypes = mediaTypes,
- IncludeItemTypes = includeItemTypes,
- ExcludeItemTypes = excludeItemTypes,
- Recursive = recursive ?? false,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
- IsFavorite = isFavorite,
- Limit = limit,
- StartIndex = startIndex,
- IsMissing = isMissing,
- IsUnaired = isUnaired,
- CollapseBoxSetItems = collapseBoxSetItems,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- HasImdbId = hasImdbId,
- IsPlaceHolder = isPlaceHolder,
- IsLocked = isLocked,
- MinWidth = minWidth,
- MinHeight = minHeight,
- MaxWidth = maxWidth,
- MaxHeight = maxHeight,
- Is3D = is3D,
- HasTvdbId = hasTvdbId,
- HasTmdbId = hasTmdbId,
- IsMovie = isMovie,
- IsSeries = isSeries,
- IsNews = isNews,
- IsKids = isKids,
- IsSports = isSports,
- HasOverview = hasOverview,
- HasOfficialRating = hasOfficialRating,
- HasParentalRating = hasParentalRating,
- HasSpecialFeature = hasSpecialFeature,
- HasSubtitles = hasSubtitles,
- HasThemeSong = hasThemeSong,
- HasThemeVideo = hasThemeVideo,
- HasTrailer = hasTrailer,
- IsHD = isHd,
- Is4K = is4K,
- Tags = tags,
- OfficialRatings = officialRatings,
- Genres = genres,
- ArtistIds = artistIds,
- AlbumArtistIds = albumArtistIds,
- ContributingArtistIds = contributingArtistIds,
- GenreIds = genreIds,
- StudioIds = studioIds,
- Person = person,
- PersonIds = personIds,
- PersonTypes = personTypes,
- Years = years,
- ImageTypes = imageTypes,
- VideoTypes = videoTypes,
- AdjacentTo = adjacentTo,
- ItemIds = ids,
- MinCommunityRating = minCommunityRating,
- MinCriticRating = minCriticRating,
- ParentId = parentId ?? Guid.Empty,
- ParentIndexNumber = parentIndexNumber,
- EnableTotalRecordCount = enableTotalRecordCount,
- ExcludeItemIds = excludeItemIds,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
- MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
- MinPremiereDate = minPremiereDate?.ToUniversalTime(),
- MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
- };
-
- if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
- {
- query.CollapseBoxSetItems = false;
- }
+ query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating);
+ }
- foreach (var filter in filters)
+ // Artists
+ if (artists.Length != 0)
+ {
+ query.ArtistIds = artists.Select(i =>
{
- switch (filter)
+ try
{
- 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 _libraryManager.GetArtist(i, new DtoOptions(false));
}
- }
-
- // Filter by Series Status
- if (seriesStatus.Length != 0)
- {
- query.SeriesStatuses = seriesStatus;
- }
-
- // ExcludeLocationTypes
- if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
- {
- query.IsVirtualItem = false;
- }
-
- if (locationTypes.Length > 0 && locationTypes.Length < 4)
- {
- query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
- }
-
- // Min official rating
- if (!string.IsNullOrWhiteSpace(minOfficialRating))
- {
- query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating);
- }
-
- // Max official rating
- if (!string.IsNullOrWhiteSpace(maxOfficialRating))
- {
- query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating);
- }
-
- // Artists
- if (artists.Length != 0)
- {
- query.ArtistIds = artists.Select(i =>
+ catch
{
- try
- {
- return _libraryManager.GetArtist(i, new DtoOptions(false));
- }
- catch
- {
- return null;
- }
- }).Where(i => i is not null).Select(i => i!.Id).ToArray();
- }
+ return null;
+ }
+ }).Where(i => i is not null).Select(i => i!.Id).ToArray();
+ }
- // ExcludeArtistIds
- if (excludeArtistIds.Length != 0)
- {
- query.ExcludeArtistIds = excludeArtistIds;
- }
+ // ExcludeArtistIds
+ if (excludeArtistIds.Length != 0)
+ {
+ query.ExcludeArtistIds = excludeArtistIds;
+ }
- if (albumIds.Length != 0)
- {
- query.AlbumIds = albumIds;
- }
+ if (albumIds.Length != 0)
+ {
+ query.AlbumIds = albumIds;
+ }
- // Albums
- if (albums.Length != 0)
+ // Albums
+ if (albums.Length != 0)
+ {
+ query.AlbumIds = albums.SelectMany(i =>
{
- query.AlbumIds = albums.SelectMany(i =>
- {
- return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = i, Limit = 1 });
- }).ToArray();
- }
+ return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = i, Limit = 1 });
+ }).ToArray();
+ }
- // Studios
- if (studios.Length != 0)
+ // Studios
+ if (studios.Length != 0)
+ {
+ query.StudioIds = studios.Select(i =>
{
- query.StudioIds = studios.Select(i =>
+ try
{
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i is not null).Select(i => i!.Id).ToArray();
- }
-
- // Apply default sorting if none requested
- if (query.OrderBy.Count == 0)
- {
- // Albums by artist
- if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum)
+ return _libraryManager.GetStudio(i);
+ }
+ catch
{
- query.OrderBy = new[] { (ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Ascending) };
+ return null;
}
- }
-
- result = folder.GetItems(query);
+ }).Where(i => i is not null).Select(i => i!.Id).ToArray();
}
- else
+
+ // Apply default sorting if none requested
+ if (query.OrderBy.Count == 0)
{
- var itemsArray = folder.GetChildren(user, true);
- result = new QueryResult<BaseItem>(itemsArray);
+ // Albums by artist
+ if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum)
+ {
+ query.OrderBy = new[] { (ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Ascending) };
+ }
}
- return new QueryResult<BaseItemDto>(
- startIndex,
- result.TotalRecordCount,
- _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user));
+ result = folder.GetItems(query);
}
-
- /// <summary>
- /// Gets items based on a query.
- /// </summary>
- /// <param name="userId">The user id supplied as query parameter.</param>
- /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
- /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
- /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
- /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
- /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
- /// <param name="hasTrailer">Optional filter by items with trailers.</param>
- /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
- /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
- /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
- /// <param name="isHd">Optional filter by items that are HD or not.</param>
- /// <param name="is4K">Optional filter by items that are 4K or not.</param>
- /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
- /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
- /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
- /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
- /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
- /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
- /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
- /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
- /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
- /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
- /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
- /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
- /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
- /// <param name="isMovie">Optional filter for live tv movies.</param>
- /// <param name="isSeries">Optional filter for live tv series.</param>
- /// <param name="isNews">Optional filter for live tv news.</param>
- /// <param name="isKids">Optional filter for live tv kids.</param>
- /// <param name="isSports">Optional filter for live tv sports.</param>
- /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
- /// <param name="searchTerm">Optional. Filter based on a search term.</param>
- /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the 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 items.</returns>
- [HttpGet("Users/{userId}/Items")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId(
- [FromRoute] Guid userId,
- [FromQuery] string? maxOfficialRating,
- [FromQuery] bool? hasThemeSong,
- [FromQuery] bool? hasThemeVideo,
- [FromQuery] bool? hasSubtitles,
- [FromQuery] bool? hasSpecialFeature,
- [FromQuery] bool? hasTrailer,
- [FromQuery] Guid? adjacentTo,
- [FromQuery] int? parentIndexNumber,
- [FromQuery] bool? hasParentalRating,
- [FromQuery] bool? isHd,
- [FromQuery] bool? is4K,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
- [FromQuery] bool? isMissing,
- [FromQuery] bool? isUnaired,
- [FromQuery] double? minCommunityRating,
- [FromQuery] double? minCriticRating,
- [FromQuery] DateTime? minPremiereDate,
- [FromQuery] DateTime? minDateLastSaved,
- [FromQuery] DateTime? minDateLastSavedForUser,
- [FromQuery] DateTime? maxPremiereDate,
- [FromQuery] bool? hasOverview,
- [FromQuery] bool? hasImdbId,
- [FromQuery] bool? hasTmdbId,
- [FromQuery] bool? hasTvdbId,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool? recursive,
- [FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery] bool? isPlayed,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
- [FromQuery] string? minOfficialRating,
- [FromQuery] bool? isLocked,
- [FromQuery] bool? isPlaceHolder,
- [FromQuery] bool? hasOfficialRating,
- [FromQuery] bool? collapseBoxSetItems,
- [FromQuery] int? minWidth,
- [FromQuery] int? minHeight,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] bool? is3D,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool? enableImages = true)
+ else
{
- return GetItems(
- userId,
- maxOfficialRating,
- hasThemeSong,
- hasThemeVideo,
- hasSubtitles,
- hasSpecialFeature,
- hasTrailer,
- adjacentTo,
- parentIndexNumber,
- hasParentalRating,
- isHd,
- is4K,
- locationTypes,
- excludeLocationTypes,
- isMissing,
- isUnaired,
- minCommunityRating,
- minCriticRating,
- minPremiereDate,
- minDateLastSaved,
- minDateLastSavedForUser,
- maxPremiereDate,
- hasOverview,
- hasImdbId,
- hasTmdbId,
- hasTvdbId,
- isMovie,
- isSeries,
- isNews,
- isKids,
- isSports,
- excludeItemIds,
- startIndex,
- limit,
- recursive,
- searchTerm,
- sortOrder,
- parentId,
- fields,
- excludeItemTypes,
- includeItemTypes,
- filters,
- isFavorite,
- mediaTypes,
- imageTypes,
- sortBy,
- isPlayed,
- genres,
- officialRatings,
- tags,
- years,
- enableUserData,
- imageTypeLimit,
- enableImageTypes,
- person,
- personIds,
- personTypes,
- studios,
- artists,
- excludeArtistIds,
- artistIds,
- albumArtistIds,
- contributingArtistIds,
- albums,
- albumIds,
- ids,
- videoTypes,
- minOfficialRating,
- isLocked,
- isPlaceHolder,
- hasOfficialRating,
- collapseBoxSetItems,
- minWidth,
- minHeight,
- maxWidth,
- maxHeight,
- is3D,
- seriesStatus,
- nameStartsWithOrGreater,
- nameStartsWith,
- nameLessThan,
- studioIds,
- genreIds,
- enableTotalRecordCount,
- enableImages);
+ var itemsArray = folder.GetChildren(user, true);
+ result = new QueryResult<BaseItem>(itemsArray);
}
- /// <summary>
- /// Gets items based on a query.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="startIndex">The start index.</param>
- /// <param name="limit">The item limit.</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. 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="mediaTypes">Optional. Filter by MediaType. 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="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
- /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
- /// <param name="enableImages">Optional. Include image information in output.</param>
- /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
- /// <response code="200">Items returned.</response>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
- [HttpGet("Users/{userId}/Items/Resume")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
- [FromRoute, Required] Guid userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool? enableImages = true,
- [FromQuery] bool excludeActiveSessions = false)
- {
- var user = _userManager.GetUserById(userId);
- var parentIdGuid = parentId ?? Guid.Empty;
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ result.TotalRecordCount,
+ _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user));
+ }
- var ancestorIds = Array.Empty<Guid>();
+ /// <summary>
+ /// Gets items based on a query.
+ /// </summary>
+ /// <param name="userId">The user id supplied as query parameter.</param>
+ /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
+ /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
+ /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
+ /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
+ /// <param name="hasTrailer">Optional filter by items with trailers.</param>
+ /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
+ /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
+ /// <param name="isHd">Optional filter by items that are HD or not.</param>
+ /// <param name="is4K">Optional filter by items that are 4K or not.</param>
+ /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
+ /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
+ /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+ /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
+ /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
+ /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
+ /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
+ /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
+ /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
+ /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
+ /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
+ /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
+ /// <param name="isMovie">Optional filter for live tv movies.</param>
+ /// <param name="isSeries">Optional filter for live tv series.</param>
+ /// <param name="isNews">Optional filter for live tv news.</param>
+ /// <param name="isKids">Optional filter for live tv kids.</param>
+ /// <param name="isSports">Optional filter for live tv sports.</param>
+ /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
+ /// <param name="searchTerm">Optional. Filter based on a search term.</param>
+ /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the 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 items.</returns>
+ [HttpGet("Users/{userId}/Items")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId(
+ [FromRoute] Guid userId,
+ [FromQuery] string? maxOfficialRating,
+ [FromQuery] bool? hasThemeSong,
+ [FromQuery] bool? hasThemeVideo,
+ [FromQuery] bool? hasSubtitles,
+ [FromQuery] bool? hasSpecialFeature,
+ [FromQuery] bool? hasTrailer,
+ [FromQuery] Guid? adjacentTo,
+ [FromQuery] int? parentIndexNumber,
+ [FromQuery] bool? hasParentalRating,
+ [FromQuery] bool? isHd,
+ [FromQuery] bool? is4K,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
+ [FromQuery] bool? isMissing,
+ [FromQuery] bool? isUnaired,
+ [FromQuery] double? minCommunityRating,
+ [FromQuery] double? minCriticRating,
+ [FromQuery] DateTime? minPremiereDate,
+ [FromQuery] DateTime? minDateLastSaved,
+ [FromQuery] DateTime? minDateLastSavedForUser,
+ [FromQuery] DateTime? maxPremiereDate,
+ [FromQuery] bool? hasOverview,
+ [FromQuery] bool? hasImdbId,
+ [FromQuery] bool? hasTmdbId,
+ [FromQuery] bool? hasTvdbId,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? recursive,
+ [FromQuery] string? searchTerm,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery] bool? isPlayed,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
+ [FromQuery] string? minOfficialRating,
+ [FromQuery] bool? isLocked,
+ [FromQuery] bool? isPlaceHolder,
+ [FromQuery] bool? hasOfficialRating,
+ [FromQuery] bool? collapseBoxSetItems,
+ [FromQuery] int? minWidth,
+ [FromQuery] int? minHeight,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] bool? is3D,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true)
+ {
+ return GetItems(
+ userId,
+ maxOfficialRating,
+ hasThemeSong,
+ hasThemeVideo,
+ hasSubtitles,
+ hasSpecialFeature,
+ hasTrailer,
+ adjacentTo,
+ parentIndexNumber,
+ hasParentalRating,
+ isHd,
+ is4K,
+ locationTypes,
+ excludeLocationTypes,
+ isMissing,
+ isUnaired,
+ minCommunityRating,
+ minCriticRating,
+ minPremiereDate,
+ minDateLastSaved,
+ minDateLastSavedForUser,
+ maxPremiereDate,
+ hasOverview,
+ hasImdbId,
+ hasTmdbId,
+ hasTvdbId,
+ isMovie,
+ isSeries,
+ isNews,
+ isKids,
+ isSports,
+ excludeItemIds,
+ startIndex,
+ limit,
+ recursive,
+ searchTerm,
+ sortOrder,
+ parentId,
+ fields,
+ excludeItemTypes,
+ includeItemTypes,
+ filters,
+ isFavorite,
+ mediaTypes,
+ imageTypes,
+ sortBy,
+ isPlayed,
+ genres,
+ officialRatings,
+ tags,
+ years,
+ enableUserData,
+ imageTypeLimit,
+ enableImageTypes,
+ person,
+ personIds,
+ personTypes,
+ studios,
+ artists,
+ excludeArtistIds,
+ artistIds,
+ albumArtistIds,
+ contributingArtistIds,
+ albums,
+ albumIds,
+ ids,
+ videoTypes,
+ minOfficialRating,
+ isLocked,
+ isPlaceHolder,
+ hasOfficialRating,
+ collapseBoxSetItems,
+ minWidth,
+ minHeight,
+ maxWidth,
+ maxHeight,
+ is3D,
+ seriesStatus,
+ nameStartsWithOrGreater,
+ nameStartsWith,
+ nameLessThan,
+ studioIds,
+ genreIds,
+ enableTotalRecordCount,
+ enableImages);
+ }
- var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
- if (parentIdGuid.Equals(default) && excludeFolderIds.Length > 0)
- {
- ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
- .Where(i => i is Folder)
- .Where(i => !excludeFolderIds.Contains(i.Id))
- .Select(i => i.Id)
- .ToArray();
- }
+ /// <summary>
+ /// Gets items based on a query.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="startIndex">The start index.</param>
+ /// <param name="limit">The item limit.</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. 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="mediaTypes">Optional. Filter by MediaType. 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="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
+ /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</param>
+ /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param>
+ /// <response code="200">Items returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
+ [HttpGet("Users/{userId}/Items/Resume")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
+ [FromRoute, Required] Guid userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool excludeActiveSessions = false)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
- var excludeItemIds = Array.Empty<Guid>();
- if (excludeActiveSessions)
- {
- excludeItemIds = _sessionManager.Sessions
- .Where(s => s.UserId.Equals(userId) && s.NowPlayingItem is not null)
- .Select(s => s.NowPlayingItem.Id)
- .ToArray();
- }
+ var parentIdGuid = parentId ?? Guid.Empty;
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
- {
- OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
- IsResumable = true,
- StartIndex = startIndex,
- Limit = limit,
- ParentId = parentIdGuid,
- Recursive = true,
- DtoOptions = dtoOptions,
- MediaTypes = mediaTypes,
- IsVirtualItem = false,
- CollapseBoxSetItems = false,
- EnableTotalRecordCount = enableTotalRecordCount,
- AncestorIds = ancestorIds,
- IncludeItemTypes = includeItemTypes,
- ExcludeItemTypes = excludeItemTypes,
- SearchTerm = searchTerm,
- ExcludeItemIds = excludeItemIds
- });
+ var ancestorIds = Array.Empty<Guid>();
- var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
+ var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
+ if (parentIdGuid.Equals(default) && excludeFolderIds.Length > 0)
+ {
+ ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
+ .Where(i => i is Folder)
+ .Where(i => !excludeFolderIds.Contains(i.Id))
+ .Select(i => i.Id)
+ .ToArray();
+ }
- return new QueryResult<BaseItemDto>(
- startIndex,
- itemsResult.TotalRecordCount,
- returnItems);
+ var excludeItemIds = Array.Empty<Guid>();
+ if (excludeActiveSessions)
+ {
+ excludeItemIds = _sessionManager.Sessions
+ .Where(s => s.UserId.Equals(userId) && s.NowPlayingItem is not null)
+ .Select(s => s.NowPlayingItem.Id)
+ .ToArray();
}
+
+ var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
+ {
+ OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
+ IsResumable = true,
+ StartIndex = startIndex,
+ Limit = limit,
+ ParentId = parentIdGuid,
+ Recursive = true,
+ DtoOptions = dtoOptions,
+ MediaTypes = mediaTypes,
+ IsVirtualItem = false,
+ CollapseBoxSetItems = false,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ AncestorIds = ancestorIds,
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
+ SearchTerm = searchTerm,
+ ExcludeItemIds = excludeItemIds
+ });
+
+ var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
+
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ itemsResult.TotalRecordCount,
+ returnItems);
}
}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 196d509fb..e8b68c7c3 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -4,8 +4,6 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Net;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -37,773 +35,787 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Library Controller.
+/// </summary>
+[Route("")]
+public class LibraryController : BaseJellyfinApiController
{
+ private readonly IProviderManager _providerManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+ private readonly IActivityManager _activityManager;
+ private readonly ILocalizationManager _localization;
+ private readonly ILibraryMonitor _libraryMonitor;
+ private readonly ILogger<LibraryController> _logger;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
/// <summary>
- /// Library Controller.
+ /// Initializes a new instance of the <see cref="LibraryController"/> class.
/// </summary>
- [Route("")]
- public class LibraryController : BaseJellyfinApiController
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="libraryMonitor">Instance of the <see cref="ILibraryMonitor"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{LibraryController}"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public LibraryController(
+ IProviderManager providerManager,
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService,
+ IActivityManager activityManager,
+ ILocalizationManager localization,
+ ILibraryMonitor libraryMonitor,
+ ILogger<LibraryController> logger,
+ IServerConfigurationManager serverConfigurationManager)
{
- private readonly IProviderManager _providerManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
- private readonly IActivityManager _activityManager;
- private readonly ILocalizationManager _localization;
- private readonly ILibraryMonitor _libraryMonitor;
- private readonly ILogger<LibraryController> _logger;
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LibraryController"/> class.
- /// </summary>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="libraryMonitor">Instance of the <see cref="ILibraryMonitor"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{LibraryController}"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public LibraryController(
- IProviderManager providerManager,
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService,
- IActivityManager activityManager,
- ILocalizationManager localization,
- ILibraryMonitor libraryMonitor,
- ILogger<LibraryController> logger,
- IServerConfigurationManager serverConfigurationManager)
- {
- _providerManager = providerManager;
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- _activityManager = activityManager;
- _localization = localization;
- _libraryMonitor = libraryMonitor;
- _logger = logger;
- _serverConfigurationManager = serverConfigurationManager;
- }
-
- /// <summary>
- /// Get the original file of an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <response code="200">File stream returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="FileStreamResult"/> with the original file.</returns>
- [HttpGet("Items/{itemId}/File")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesFile("video/*", "audio/*")]
- public ActionResult GetFile([FromRoute, Required] Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
-
- return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
- }
+ _providerManager = providerManager;
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ _activityManager = activityManager;
+ _localization = localization;
+ _libraryMonitor = libraryMonitor;
+ _logger = logger;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- /// <summary>
- /// Gets critic review for an item.
- /// </summary>
- /// <response code="200">Critic reviews returned.</response>
- /// <returns>The list of critic reviews.</returns>
- [HttpGet("Items/{itemId}/CriticReviews")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [Obsolete("This endpoint is obsolete.")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews()
+ /// <summary>
+ /// Get the original file of an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="200">File stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="FileStreamResult"/> with the original file.</returns>
+ [HttpGet("Items/{itemId}/File")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesFile("video/*", "audio/*")]
+ public ActionResult GetFile([FromRoute, Required] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- return new QueryResult<BaseItemDto>();
+ return NotFound();
}
- /// <summary>
- /// Get theme songs for an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
- /// <response code="200">Theme songs returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>The item theme songs.</returns>
- [HttpGet("Items/{itemId}/ThemeSongs")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<ThemeMediaResult> GetThemeSongs(
- [FromRoute, Required] Guid itemId,
- [FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
- ? _libraryManager.RootFolder
- : _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
- if (item is null)
- {
- return NotFound("Item not found.");
- }
+ return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
+ }
- IEnumerable<BaseItem> themeItems;
+ /// <summary>
+ /// Gets critic review for an item.
+ /// </summary>
+ /// <response code="200">Critic reviews returned.</response>
+ /// <returns>The list of critic reviews.</returns>
+ [HttpGet("Items/{itemId}/CriticReviews")]
+ [Authorize]
+ [Obsolete("This endpoint is obsolete.")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetCriticReviews()
+ {
+ return new QueryResult<BaseItemDto>();
+ }
- while (true)
- {
- themeItems = item.GetThemeSongs();
+ /// <summary>
+ /// Get theme songs for an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+ /// <response code="200">Theme songs returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>The item theme songs.</returns>
+ [HttpGet("Items/{itemId}/ThemeSongs")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<ThemeMediaResult> GetThemeSongs(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool inheritFromParent = false)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- if (themeItems.Any() || !inheritFromParent)
- {
- break;
- }
+ var item = itemId.Equals(default)
+ ? (userId is null || userId.Value.Equals(default)
+ ? _libraryManager.RootFolder
+ : _libraryManager.GetUserRootFolder())
+ : _libraryManager.GetItemById(itemId);
- var parent = item.GetParent();
- if (parent is null)
- {
- break;
- }
+ if (item is null)
+ {
+ return NotFound("Item not found.");
+ }
- item = parent;
- }
+ IEnumerable<BaseItem> themeItems;
- var dtoOptions = new DtoOptions().AddClientFields(User);
- var items = themeItems
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
- .ToArray();
+ while (true)
+ {
+ themeItems = item.GetThemeSongs();
- return new ThemeMediaResult
+ if (themeItems.Any() || !inheritFromParent)
{
- Items = items,
- TotalRecordCount = items.Length,
- OwnerId = item.Id
- };
- }
-
- /// <summary>
- /// Get theme videos for an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
- /// <response code="200">Theme videos returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>The item theme videos.</returns>
- [HttpGet("Items/{itemId}/ThemeVideos")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<ThemeMediaResult> GetThemeVideos(
- [FromRoute, Required] Guid itemId,
- [FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
- ? _libraryManager.RootFolder
- : _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
- if (item is null)
+ break;
+ }
+
+ var parent = item.GetParent();
+ if (parent is null)
{
- return NotFound("Item not found.");
+ break;
}
- IEnumerable<BaseItem> themeItems;
+ item = parent;
+ }
- while (true)
- {
- themeItems = item.GetThemeVideos();
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ var items = themeItems
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
+ .ToArray();
- if (themeItems.Any() || !inheritFromParent)
- {
- break;
- }
+ return new ThemeMediaResult
+ {
+ Items = items,
+ TotalRecordCount = items.Length,
+ OwnerId = item.Id
+ };
+ }
- var parent = item.GetParent();
- if (parent is null)
- {
- break;
- }
+ /// <summary>
+ /// Get theme videos for an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+ /// <response code="200">Theme videos returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>The item theme videos.</returns>
+ [HttpGet("Items/{itemId}/ThemeVideos")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<ThemeMediaResult> GetThemeVideos(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool inheritFromParent = false)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- item = parent;
- }
+ var item = itemId.Equals(default)
+ ? (userId is null || userId.Value.Equals(default)
+ ? _libraryManager.RootFolder
+ : _libraryManager.GetUserRootFolder())
+ : _libraryManager.GetItemById(itemId);
- var dtoOptions = new DtoOptions().AddClientFields(User);
- var items = themeItems
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
- .ToArray();
+ if (item is null)
+ {
+ return NotFound("Item not found.");
+ }
- return new ThemeMediaResult
- {
- Items = items,
- TotalRecordCount = items.Length,
- OwnerId = item.Id
- };
- }
-
- /// <summary>
- /// Get theme songs and videos for an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
- /// <response code="200">Theme songs and videos returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>The item theme videos.</returns>
- [HttpGet("Items/{itemId}/ThemeMedia")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<AllThemeMediaResult> GetThemeMedia(
- [FromRoute, Required] Guid itemId,
- [FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
- {
- var themeSongs = GetThemeSongs(
- itemId,
- userId,
- inheritFromParent);
-
- var themeVideos = GetThemeVideos(
- itemId,
- userId,
- inheritFromParent);
-
- return new AllThemeMediaResult
- {
- ThemeSongsResult = themeSongs?.Value,
- ThemeVideosResult = themeVideos?.Value,
- SoundtrackSongsResult = new ThemeMediaResult()
- };
- }
-
- /// <summary>
- /// Starts a library scan.
- /// </summary>
- /// <response code="204">Library scan started.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Library/Refresh")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RefreshLibrary()
- {
- try
+ IEnumerable<BaseItem> themeItems;
+
+ while (true)
+ {
+ themeItems = item.GetThemeVideos();
+
+ if (themeItems.Any() || !inheritFromParent)
{
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
+ break;
}
- catch (Exception ex)
+
+ var parent = item.GetParent();
+ if (parent is null)
{
- _logger.LogError(ex, "Error refreshing library");
+ break;
}
- return NoContent();
+ item = parent;
}
- /// <summary>
- /// Deletes an item from the library and filesystem.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <response code="204">Item deleted.</response>
- /// <response code="401">Unauthorized access.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Items/{itemId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- public ActionResult DeleteItem(Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
- var user = _userManager.GetUserById(User.GetUserId());
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ var items = themeItems
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
+ .ToArray();
- if (!item.CanDelete(user))
- {
- return Unauthorized("Unauthorized access");
- }
+ return new ThemeMediaResult
+ {
+ Items = items,
+ TotalRecordCount = items.Length,
+ OwnerId = item.Id
+ };
+ }
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions { DeleteFileLocation = true },
- true);
+ /// <summary>
+ /// Get theme songs and videos for an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+ /// <response code="200">Theme songs and videos returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>The item theme videos.</returns>
+ [HttpGet("Items/{itemId}/ThemeMedia")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<AllThemeMediaResult> GetThemeMedia(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool inheritFromParent = false)
+ {
+ var themeSongs = GetThemeSongs(
+ itemId,
+ userId,
+ inheritFromParent);
- return NoContent();
- }
+ var themeVideos = GetThemeVideos(
+ itemId,
+ userId,
+ inheritFromParent);
- /// <summary>
- /// Deletes items from the library and filesystem.
- /// </summary>
- /// <param name="ids">The item ids.</param>
- /// <response code="204">Items deleted.</response>
- /// <response code="401">Unauthorized access.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Items")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
- {
- if (ids.Length == 0)
- {
- return NoContent();
- }
+ if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult)
+ {
+ return NotFound();
+ }
- foreach (var i in ids)
- {
- var item = _libraryManager.GetItemById(i);
- var user = _userManager.GetUserById(User.GetUserId());
+ return new AllThemeMediaResult
+ {
+ ThemeSongsResult = themeSongs?.Value,
+ ThemeVideosResult = themeVideos?.Value,
+ SoundtrackSongsResult = new ThemeMediaResult()
+ };
+ }
- if (!item.CanDelete(user))
- {
- if (ids.Length > 1)
- {
- return Unauthorized("Unauthorized access");
- }
+ /// <summary>
+ /// Starts a library scan.
+ /// </summary>
+ /// <response code="204">Library scan started.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Library/Refresh")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> RefreshLibrary()
+ {
+ try
+ {
+ await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error refreshing library");
+ }
- continue;
- }
+ return NoContent();
+ }
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions { DeleteFileLocation = true },
- true);
- }
+ /// <summary>
+ /// Deletes an item from the library and filesystem.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="204">Item deleted.</response>
+ /// <response code="401">Unauthorized access.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Items/{itemId}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ public ActionResult DeleteItem(Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ var user = _userManager.GetUserById(User.GetUserId());
- return NoContent();
+ if (!item.CanDelete(user))
+ {
+ return Unauthorized("Unauthorized access");
}
- /// <summary>
- /// Get item counts.
- /// </summary>
- /// <param name="userId">Optional. Get counts from a specific user's library.</param>
- /// <param name="isFavorite">Optional. Get counts of favorite items.</param>
- /// <response code="200">Item counts returned.</response>
- /// <returns>Item counts.</returns>
- [HttpGet("Items/Counts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<ItemCounts> GetItemCounts(
- [FromQuery] Guid? userId,
- [FromQuery] bool? isFavorite)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var counts = new ItemCounts
- {
- AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite),
- EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite),
- MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite),
- SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite),
- SongCount = GetCount(BaseItemKind.Audio, user, isFavorite),
- MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite),
- BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite),
- BookCount = GetCount(BaseItemKind.Book, user, isFavorite)
- };
-
- return counts;
- }
-
- /// <summary>
- /// Gets all parents of an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <response code="200">Item parents returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>Item parents.</returns>
- [HttpGet("Items/{itemId}/Ancestors")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
- {
- var item = _libraryManager.GetItemById(itemId);
-
- if (item is null)
- {
- return NotFound("Item not found");
- }
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions { DeleteFileLocation = true },
+ true);
- var baseItemDtos = new List<BaseItemDto>();
+ return NoContent();
+ }
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ /// <summary>
+ /// Deletes items from the library and filesystem.
+ /// </summary>
+ /// <param name="ids">The item ids.</param>
+ /// <response code="204">Items deleted.</response>
+ /// <response code="401">Unauthorized access.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Items")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ {
+ if (ids.Length == 0)
+ {
+ return NoContent();
+ }
- var dtoOptions = new DtoOptions().AddClientFields(User);
- BaseItem? parent = item.GetParent();
+ foreach (var i in ids)
+ {
+ var item = _libraryManager.GetItemById(i);
+ var user = _userManager.GetUserById(User.GetUserId());
- while (parent is not null)
+ if (!item.CanDelete(user))
{
- if (user is not null)
+ if (ids.Length > 1)
{
- parent = TranslateParentItem(parent, user);
+ return Unauthorized("Unauthorized access");
}
- baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
-
- parent = parent?.GetParent();
+ continue;
}
- return baseItemDtos;
+ _libraryManager.DeleteItem(
+ item,
+ new DeleteOptions { DeleteFileLocation = true },
+ true);
}
- /// <summary>
- /// Gets a list of physical paths from virtual folders.
- /// </summary>
- /// <response code="200">Physical paths returned.</response>
- /// <returns>List of physical paths.</returns>
- [HttpGet("Library/PhysicalPaths")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<string>> GetPhysicalPaths()
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Get item counts.
+ /// </summary>
+ /// <param name="userId">Optional. Get counts from a specific user's library.</param>
+ /// <param name="isFavorite">Optional. Get counts of favorite items.</param>
+ /// <response code="200">Item counts returned.</response>
+ /// <returns>Item counts.</returns>
+ [HttpGet("Items/Counts")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<ItemCounts> GetItemCounts(
+ [FromQuery] Guid? userId,
+ [FromQuery] bool? isFavorite)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ var counts = new ItemCounts
{
- return Ok(_libraryManager.RootFolder.Children
- .SelectMany(c => c.PhysicalLocations));
- }
+ AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite),
+ EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite),
+ MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite),
+ SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite),
+ SongCount = GetCount(BaseItemKind.Audio, user, isFavorite),
+ MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite),
+ BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite),
+ BookCount = GetCount(BaseItemKind.Book, user, isFavorite)
+ };
+
+ return counts;
+ }
+
+ /// <summary>
+ /// Gets all parents of an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <response code="200">Item parents returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>Item parents.</returns>
+ [HttpGet("Items/{itemId}/Ancestors")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
- /// <summary>
- /// Gets all user media folders.
- /// </summary>
- /// <param name="isHidden">Optional. Filter by folders that are marked hidden, or not.</param>
- /// <response code="200">Media folders returned.</response>
- /// <returns>List of user media folders.</returns>
- [HttpGet("Library/MediaFolders")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
+ if (item is null)
{
- var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
+ return NotFound("Item not found");
+ }
- if (isHidden.HasValue)
- {
- var val = isHidden.Value;
+ var baseItemDtos = new List<BaseItemDto>();
- items = items.Where(i => i.IsHidden == val).ToList();
- }
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions().AddClientFields(User);
- var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions);
- return new QueryResult<BaseItemDto>(resultArray);
- }
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ BaseItem? parent = item.GetParent();
- /// <summary>
- /// Reports that new episodes of a series have been added by an external source.
- /// </summary>
- /// <param name="tvdbId">The tvdbId.</param>
- /// <response code="204">Report success.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Library/Series/Added", Name = "PostAddedSeries")]
- [HttpPost("Library/Series/Updated")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId)
+ while (parent is not null)
{
- var series = _libraryManager.GetItemList(new InternalItemsQuery
+ if (user is not null)
{
- IncludeItemTypes = new[] { BaseItemKind.Series },
- DtoOptions = new DtoOptions(false)
+ parent = TranslateParentItem(parent, user);
+ if (parent is null)
{
- EnableImages = false
+ break;
}
- }).Where(i => string.Equals(tvdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray();
-
- foreach (var item in series)
- {
- _libraryMonitor.ReportFileSystemChanged(item.Path);
}
- return NoContent();
+ baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
+
+ parent = parent?.GetParent();
}
- /// <summary>
- /// Reports that new movies have been added by an external source.
- /// </summary>
- /// <param name="tmdbId">The tmdbId.</param>
- /// <param name="imdbId">The imdbId.</param>
- /// <response code="204">Report success.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")]
- [HttpPost("Library/Movies/Updated")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId)
- {
- var movies = _libraryManager.GetItemList(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { BaseItemKind.Movie },
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- }
- });
+ return baseItemDtos;
+ }
- if (!string.IsNullOrWhiteSpace(imdbId))
- {
- movies = movies.Where(i => string.Equals(imdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList();
- }
- else if (!string.IsNullOrWhiteSpace(tmdbId))
- {
- movies = movies.Where(i => string.Equals(tmdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList();
- }
- else
- {
- movies = new List<BaseItem>();
- }
+ /// <summary>
+ /// Gets a list of physical paths from virtual folders.
+ /// </summary>
+ /// <response code="200">Physical paths returned.</response>
+ /// <returns>List of physical paths.</returns>
+ [HttpGet("Library/PhysicalPaths")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<string>> GetPhysicalPaths()
+ {
+ return Ok(_libraryManager.RootFolder.Children
+ .SelectMany(c => c.PhysicalLocations));
+ }
- foreach (var item in movies)
- {
- _libraryMonitor.ReportFileSystemChanged(item.Path);
- }
+ /// <summary>
+ /// Gets all user media folders.
+ /// </summary>
+ /// <param name="isHidden">Optional. Filter by folders that are marked hidden, or not.</param>
+ /// <response code="200">Media folders returned.</response>
+ /// <returns>List of user media folders.</returns>
+ [HttpGet("Library/MediaFolders")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
+ {
+ var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList();
- return NoContent();
+ if (isHidden.HasValue)
+ {
+ var val = isHidden.Value;
+
+ items = items.Where(i => i.IsHidden == val).ToList();
}
- /// <summary>
- /// Reports that new movies have been added by an external source.
- /// </summary>
- /// <param name="dto">The update paths.</param>
- /// <response code="204">Report success.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Library/Media/Updated")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions);
+ return new QueryResult<BaseItemDto>(resultArray);
+ }
+
+ /// <summary>
+ /// Reports that new episodes of a series have been added by an external source.
+ /// </summary>
+ /// <param name="tvdbId">The tvdbId.</param>
+ /// <response code="204">Report success.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Library/Series/Added", Name = "PostAddedSeries")]
+ [HttpPost("Library/Series/Updated")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId)
+ {
+ var series = _libraryManager.GetItemList(new InternalItemsQuery
{
- foreach (var item in dto.Updates)
+ IncludeItemTypes = new[] { BaseItemKind.Series },
+ DtoOptions = new DtoOptions(false)
{
- _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null."));
+ EnableImages = false
}
+ }).Where(i => string.Equals(tvdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tvdb), StringComparison.OrdinalIgnoreCase)).ToArray();
- return NoContent();
+ foreach (var item in series)
+ {
+ _libraryMonitor.ReportFileSystemChanged(item.Path);
}
- /// <summary>
- /// Downloads item media.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <response code="200">Media downloaded.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="FileResult"/> containing the media stream.</returns>
- /// <exception cref="ArgumentException">User can't download or item can't be downloaded.</exception>
- [HttpGet("Items/{itemId}/Download")]
- [Authorize(Policy = Policies.Download)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesFile("video/*", "audio/*")]
- public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Reports that new movies have been added by an external source.
+ /// </summary>
+ /// <param name="tmdbId">The tmdbId.</param>
+ /// <param name="imdbId">The imdbId.</param>
+ /// <response code="204">Report success.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Library/Movies/Added", Name = "PostAddedMovies")]
+ [HttpPost("Library/Movies/Updated")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId)
+ {
+ var movies = _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { BaseItemKind.Movie },
+ DtoOptions = new DtoOptions(false)
{
- return NotFound();
+ EnableImages = false
}
+ });
- var user = _userManager.GetUserById(User.GetUserId());
+ if (!string.IsNullOrWhiteSpace(imdbId))
+ {
+ movies = movies.Where(i => string.Equals(imdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb), StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+ else if (!string.IsNullOrWhiteSpace(tmdbId))
+ {
+ movies = movies.Where(i => string.Equals(tmdbId, i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Tmdb), StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+ else
+ {
+ movies = new List<BaseItem>();
+ }
- if (user is not null)
- {
- if (!item.CanDownload(user))
- {
- throw new ArgumentException("Item does not support downloading");
- }
- }
- else
- {
- if (!item.CanDownload())
- {
- throw new ArgumentException("Item does not support downloading");
- }
- }
+ foreach (var item in movies)
+ {
+ _libraryMonitor.ReportFileSystemChanged(item.Path);
+ }
- if (user is not null)
- {
- await LogDownloadAsync(item, user).ConfigureAwait(false);
- }
+ return NoContent();
+ }
- // Quotes are valid in linux. They'll possibly cause issues here.
- var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal);
-
- return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true);
- }
-
- /// <summary>
- /// Gets similar items.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="excludeArtistIds">Exclude artist ids.</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. 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>
- /// <response code="200">Similar items returned.</response>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
- [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
- [HttpGet("Items/{itemId}/Similar")]
- [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")]
- [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")]
- [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")]
- [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
- [FromRoute, Required] Guid itemId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
- [FromQuery] Guid? userId,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
- {
- var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
- ? _libraryManager.RootFolder
- : _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
- if (item is Episode || (item is IItemByName && item is not MusicArtist))
- {
- return new QueryResult<BaseItemDto>();
- }
+ /// <summary>
+ /// Reports that new movies have been added by an external source.
+ /// </summary>
+ /// <param name="dto">The update paths.</param>
+ /// <response code="204">Report success.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Library/Media/Updated")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
+ {
+ foreach (var item in dto.Updates)
+ {
+ _libraryMonitor.ReportFileSystemChanged(item.Path ?? throw new ArgumentException("Item path can't be null."));
+ }
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Downloads item media.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="200">Media downloaded.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="FileResult"/> containing the media stream.</returns>
+ /// <exception cref="ArgumentException">User can't download or item can't be downloaded.</exception>
+ [HttpGet("Items/{itemId}/Download")]
+ [Authorize(Policy = Policies.Download)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesFile("video/*", "audio/*")]
+ public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
- var program = item as IHasProgramAttributes;
- bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer;
- bool? isSeries = item is Series || (program is not null && program.IsSeries);
+ var user = _userManager.GetUserById(User.GetUserId());
- var includeItemTypes = new List<BaseItemKind>();
- if (isMovie.Value)
- {
- includeItemTypes.Add(BaseItemKind.Movie);
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- includeItemTypes.Add(BaseItemKind.Trailer);
- includeItemTypes.Add(BaseItemKind.LiveTvProgram);
- }
- }
- else if (isSeries.Value)
+ if (user is not null)
+ {
+ if (!item.CanDownload(user))
{
- includeItemTypes.Add(BaseItemKind.Series);
+ throw new ArgumentException("Item does not support downloading");
}
- else
+ }
+ else
+ {
+ if (!item.CanDownload())
{
- // For non series and movie types these columns are typically null
- // isSeries = null;
- isMovie = null;
- includeItemTypes.Add(item.GetBaseItemKind());
+ throw new ArgumentException("Item does not support downloading");
}
+ }
- var query = new InternalItemsQuery(user)
- {
- Genres = item.Genres,
- Limit = limit,
- IncludeItemTypes = includeItemTypes.ToArray(),
- SimilarTo = item,
- DtoOptions = dtoOptions,
- EnableTotalRecordCount = !isMovie ?? true,
- EnableGroupByMetadataKey = isMovie ?? false,
- MinSimilarityScore = 2 // A remnant from album/artist scoring
- };
-
- // ExcludeArtistIds
- if (excludeArtistIds.Length != 0)
- {
- query.ExcludeArtistIds = excludeArtistIds;
- }
+ if (user is not null)
+ {
+ await LogDownloadAsync(item, user).ConfigureAwait(false);
+ }
- List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
+ // Quotes are valid in linux. They'll possibly cause issues here.
+ var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal);
- var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
+ return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true);
+ }
+
+ /// <summary>
+ /// Gets similar items.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="excludeArtistIds">Exclude artist ids.</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. 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>
+ /// <response code="200">Similar items returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
+ [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
+ [HttpGet("Items/{itemId}/Similar")]
+ [HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")]
+ [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")]
+ [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")]
+ [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
+ {
+ var item = itemId.Equals(default)
+ ? (userId is null || userId.Value.Equals(default)
+ ? _libraryManager.RootFolder
+ : _libraryManager.GetUserRootFolder())
+ : _libraryManager.GetItemById(itemId);
- return new QueryResult<BaseItemDto>(
- query.StartIndex,
- itemsResult.Count,
- returnList);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Gets the library options info.
- /// </summary>
- /// <param name="libraryContentType">Library content type.</param>
- /// <param name="isNewLibrary">Whether this is a new library.</param>
- /// <response code="200">Library options info returned.</response>
- /// <returns>Library options info.</returns>
- [HttpGet("Libraries/AvailableOptions")]
- [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
- [FromQuery] string? libraryContentType,
- [FromQuery] bool isNewLibrary = false)
+ if (item is Episode || (item is IItemByName && item is not MusicArtist))
{
- var result = new LibraryOptionsResultDto();
+ return new QueryResult<BaseItemDto>();
+ }
- var types = GetRepresentativeItemTypes(libraryContentType);
- var typesList = types.ToList();
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User);
- var plugins = _providerManager.GetAllMetadataPlugins()
- .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase))
- .OrderBy(i => typesList.IndexOf(i.ItemType))
- .ToList();
+ var program = item as IHasProgramAttributes;
+ bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer;
+ bool? isSeries = item is Series || (program is not null && program.IsSeries);
- result.MetadataSavers = plugins
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver))
- .Select(i => new LibraryOptionInfoDto
- {
- Name = i.Name,
- DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
- })
- .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .ToArray();
-
- result.MetadataReaders = plugins
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider))
- .Select(i => new LibraryOptionInfoDto
- {
- Name = i.Name,
- DefaultEnabled = true
- })
- .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .ToArray();
-
- result.SubtitleFetchers = plugins
- .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher))
- .Select(i => new LibraryOptionInfoDto
- {
- Name = i.Name,
- DefaultEnabled = true
- })
- .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
- .ToArray();
+ var includeItemTypes = new List<BaseItemKind>();
+ if (isMovie.Value)
+ {
+ includeItemTypes.Add(BaseItemKind.Movie);
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ includeItemTypes.Add(BaseItemKind.Trailer);
+ includeItemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
+ }
+ else if (isSeries.Value)
+ {
+ includeItemTypes.Add(BaseItemKind.Series);
+ }
+ else
+ {
+ // For non series and movie types these columns are typically null
+ // isSeries = null;
+ isMovie = null;
+ includeItemTypes.Add(item.GetBaseItemKind());
+ }
+
+ var query = new InternalItemsQuery(user)
+ {
+ Genres = item.Genres,
+ Limit = limit,
+ IncludeItemTypes = includeItemTypes.ToArray(),
+ SimilarTo = item,
+ DtoOptions = dtoOptions,
+ EnableTotalRecordCount = !isMovie ?? true,
+ EnableGroupByMetadataKey = isMovie ?? false,
+ MinSimilarityScore = 2 // A remnant from album/artist scoring
+ };
+
+ // ExcludeArtistIds
+ if (excludeArtistIds.Length != 0)
+ {
+ query.ExcludeArtistIds = excludeArtistIds;
+ }
+
+ List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
- var typeOptions = new List<LibraryTypeOptionsDto>();
+ var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
- foreach (var type in types)
+ return new QueryResult<BaseItemDto>(
+ query.StartIndex,
+ itemsResult.Count,
+ returnList);
+ }
+
+ /// <summary>
+ /// Gets the library options info.
+ /// </summary>
+ /// <param name="libraryContentType">Library content type.</param>
+ /// <param name="isNewLibrary">Whether this is a new library.</param>
+ /// <response code="200">Library options info returned.</response>
+ /// <returns>Library options info.</returns>
+ [HttpGet("Libraries/AvailableOptions")]
+ [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
+ [FromQuery] string? libraryContentType,
+ [FromQuery] bool isNewLibrary = false)
+ {
+ var result = new LibraryOptionsResultDto();
+
+ var types = GetRepresentativeItemTypes(libraryContentType);
+ var typesList = types.ToList();
+
+ var plugins = _providerManager.GetAllMetadataPlugins()
+ .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase))
+ .OrderBy(i => typesList.IndexOf(i.ItemType))
+ .ToList();
+
+ result.MetadataSavers = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataSaver))
+ .Select(i => new LibraryOptionInfoDto
{
- TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
+ Name = i.Name,
+ DefaultEnabled = IsSaverEnabledByDefault(i.Name, types, isNewLibrary)
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
- typeOptions.Add(new LibraryTypeOptionsDto
- {
- Type = type,
+ result.MetadataReaders = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalMetadataProvider))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = true
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
- MetadataFetchers = plugins
+ result.SubtitleFetchers = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.SubtitleFetcher))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = true
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ var typeOptions = new List<LibraryTypeOptionsDto>();
+
+ foreach (var type in types)
+ {
+ TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
+
+ typeOptions.Add(new LibraryTypeOptionsDto
+ {
+ Type = type,
+
+ MetadataFetchers = plugins
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MetadataFetcher))
.Select(i => new LibraryOptionInfoDto
@@ -814,7 +826,7 @@ namespace Jellyfin.Api.Controllers
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray(),
- ImageFetchers = plugins
+ ImageFetchers = plugins
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.ImageFetcher))
.Select(i => new LibraryOptionInfoDto
@@ -825,148 +837,147 @@ namespace Jellyfin.Api.Controllers
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray(),
- SupportedImageTypes = plugins
+ SupportedImageTypes = plugins
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
.Distinct()
.ToArray(),
- DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
- });
- }
+ DefaultImageOptions = defaultImageOptions ?? Array.Empty<ImageOption>()
+ });
+ }
- result.TypeOptions = typeOptions.ToArray();
+ result.TypeOptions = typeOptions.ToArray();
- return result;
- }
+ return result;
+ }
- private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite)
+ private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite)
+ {
+ var query = new InternalItemsQuery(user)
{
- var query = new InternalItemsQuery(user)
+ IncludeItemTypes = new[] { itemKind },
+ Limit = 0,
+ Recursive = true,
+ IsVirtualItem = false,
+ IsFavorite = isFavorite,
+ DtoOptions = new DtoOptions(false)
{
- IncludeItemTypes = new[] { itemKind },
- Limit = 0,
- Recursive = true,
- IsVirtualItem = false,
- IsFavorite = isFavorite,
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- }
- };
+ EnableImages = false
+ }
+ };
- return _libraryManager.GetItemsResult(query).TotalRecordCount;
- }
+ return _libraryManager.GetItemsResult(query).TotalRecordCount;
+ }
- private BaseItem? TranslateParentItem(BaseItem item, User user)
- {
- return item.GetParent() is AggregateFolder
- ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
- .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
- : item;
- }
+ private BaseItem? TranslateParentItem(BaseItem item, User user)
+ {
+ return item.GetParent() is AggregateFolder
+ ? _libraryManager.GetUserRootFolder().GetChildren(user, true)
+ .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
+ : item;
+ }
- private async Task LogDownloadAsync(BaseItem item, User user)
+ private async Task LogDownloadAsync(BaseItem item, User user)
+ {
+ try
{
- try
- {
- await _activityManager.CreateAsync(new ActivityLog(
- string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
- "UserDownloadingContent",
- User.GetUserId())
- {
- ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
- }).ConfigureAwait(false);
- }
- catch
+ await _activityManager.CreateAsync(new ActivityLog(
+ string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
+ "UserDownloadingContent",
+ User.GetUserId())
{
- // Logged at lower levels
- }
+ ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
+ }).ConfigureAwait(false);
}
-
- private static string[] GetRepresentativeItemTypes(string? contentType)
+ catch
{
- return contentType switch
- {
- CollectionType.BoxSets => new[] { "BoxSet" },
- CollectionType.Playlists => new[] { "Playlist" },
- CollectionType.Movies => new[] { "Movie" },
- CollectionType.TvShows => new[] { "Series", "Season", "Episode" },
- CollectionType.Books => new[] { "Book" },
- CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
- CollectionType.HomeVideos => new[] { "Video", "Photo" },
- CollectionType.Photos => new[] { "Video", "Photo" },
- CollectionType.MusicVideos => new[] { "MusicVideo" },
- _ => new[] { "Series", "Season", "Episode", "Movie" }
- };
- }
-
- private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
- {
- if (isNewLibrary)
- {
- return false;
- }
+ // Logged at lower levels
+ }
+ }
- var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
- .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
- .ToArray();
+ private static string[] GetRepresentativeItemTypes(string? contentType)
+ {
+ return contentType switch
+ {
+ CollectionType.BoxSets => new[] { "BoxSet" },
+ CollectionType.Playlists => new[] { "Playlist" },
+ CollectionType.Movies => new[] { "Movie" },
+ CollectionType.TvShows => new[] { "Series", "Season", "Episode" },
+ CollectionType.Books => new[] { "Book" },
+ CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
+ CollectionType.HomeVideos => new[] { "Video", "Photo" },
+ CollectionType.Photos => new[] { "Video", "Photo" },
+ CollectionType.MusicVideos => new[] { "MusicVideo" },
+ _ => new[] { "Series", "Season", "Episode", "Movie" }
+ };
+ }
- return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase));
+ private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
+ {
+ if (isNewLibrary)
+ {
+ return false;
}
- private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+ var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+ .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+
+ return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+ {
+ if (isNewLibrary)
{
- if (isNewLibrary)
+ if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
- {
- return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
+ return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
|| string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
|| string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
- }
+ }
- return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
+ return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
- }
+ }
- var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .ToArray();
+ var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
- return metadataOptions.Length == 0
+ return metadataOptions.Length == 0
|| metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
- }
+ }
- private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+ private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
+ {
+ if (isNewLibrary)
{
- if (isNewLibrary)
+ if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
- {
- return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
- }
-
- return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
- || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
+ return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
}
- var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
- .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
- .ToArray();
+ return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
+ }
- if (metadataOptions.Length == 0)
- {
- return true;
- }
+ var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
- return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
+ if (metadataOptions.Length == 0)
+ {
+ return true;
}
+
+ return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
}
}
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 1c2394055..b012ff42e 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -20,308 +20,307 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The library structure controller.
+/// </summary>
+[Route("Library/VirtualFolders")]
+[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+public class LibraryStructureController : BaseJellyfinApiController
{
+ private readonly IServerApplicationPaths _appPaths;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILibraryMonitor _libraryMonitor;
+
/// <summary>
- /// The library structure controller.
+ /// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
/// </summary>
- [Route("Library/VirtualFolders")]
- [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
- public class LibraryStructureController : BaseJellyfinApiController
+ /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
+ public LibraryStructureController(
+ IServerConfigurationManager serverConfigurationManager,
+ ILibraryManager libraryManager,
+ ILibraryMonitor libraryMonitor)
{
- private readonly IServerApplicationPaths _appPaths;
- private readonly ILibraryManager _libraryManager;
- private readonly ILibraryMonitor _libraryMonitor;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
- /// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
- public LibraryStructureController(
- IServerConfigurationManager serverConfigurationManager,
- ILibraryManager libraryManager,
- ILibraryMonitor libraryMonitor)
- {
- _appPaths = serverConfigurationManager.ApplicationPaths;
- _libraryManager = libraryManager;
- _libraryMonitor = libraryMonitor;
- }
+ _appPaths = serverConfigurationManager.ApplicationPaths;
+ _libraryManager = libraryManager;
+ _libraryMonitor = libraryMonitor;
+ }
+
+ /// <summary>
+ /// Gets all virtual folders.
+ /// </summary>
+ /// <response code="200">Virtual folders retrieved.</response>
+ /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders()
+ {
+ return _libraryManager.GetVirtualFolders(true);
+ }
+
+ /// <summary>
+ /// Adds a virtual folder.
+ /// </summary>
+ /// <param name="name">The name of the virtual folder.</param>
+ /// <param name="collectionType">The type of the collection.</param>
+ /// <param name="paths">The paths of the virtual folder.</param>
+ /// <param name="libraryOptionsDto">The library options.</param>
+ /// <param name="refreshLibrary">Whether to refresh the library.</param>
+ /// <response code="204">Folder added.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> AddVirtualFolder(
+ [FromQuery] string? name,
+ [FromQuery] CollectionTypeOptions? collectionType,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
+ [FromBody] AddVirtualFolderDto? libraryOptionsDto,
+ [FromQuery] bool refreshLibrary = false)
+ {
+ var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions();
- /// <summary>
- /// Gets all virtual folders.
- /// </summary>
- /// <response code="200">Virtual folders retrieved.</response>
- /// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders()
+ if (paths is not null && paths.Length > 0)
{
- return _libraryManager.GetVirtualFolders(true);
+ libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
}
- /// <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();
+ await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
- if (paths is not null && paths.Length > 0)
- {
- libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
- }
+ return NoContent();
+ }
- await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
+ /// <summary>
+ /// Removes a virtual folder.
+ /// </summary>
+ /// <param name="name">The name of the folder.</param>
+ /// <param name="refreshLibrary">Whether to refresh the library.</param>
+ /// <response code="204">Folder removed.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> RemoveVirtualFolder(
+ [FromQuery] string? name,
+ [FromQuery] bool refreshLibrary = false)
+ {
+ await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
+ return NoContent();
+ }
- 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))
+ {
+ throw new ArgumentNullException(nameof(name));
}
- /// <summary>
- /// Removes a virtual folder.
- /// </summary>
- /// <param name="name">The name of the folder.</param>
- /// <param name="refreshLibrary">Whether to refresh the library.</param>
- /// <response code="204">Folder removed.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveVirtualFolder(
- [FromQuery] string? name,
- [FromQuery] bool refreshLibrary = false)
+ if (string.IsNullOrWhiteSpace(newName))
{
- await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
- return NoContent();
+ throw new ArgumentNullException(nameof(newName));
}
- /// <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))
- {
- throw new ArgumentNullException(nameof(name));
- }
+ var rootFolderPath = _appPaths.DefaultUserViewsPath;
- if (string.IsNullOrWhiteSpace(newName))
- {
- throw new ArgumentNullException(nameof(newName));
- }
+ var currentPath = Path.Combine(rootFolderPath, name);
+ var newPath = Path.Combine(rootFolderPath, newName);
- var rootFolderPath = _appPaths.DefaultUserViewsPath;
+ if (!Directory.Exists(currentPath))
+ {
+ return NotFound("The media collection does not exist.");
+ }
- var currentPath = Path.Combine(rootFolderPath, name);
- var newPath = Path.Combine(rootFolderPath, newName);
+ if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
+ {
+ return Conflict($"The media library already exists at {newPath}.");
+ }
- if (!Directory.Exists(currentPath))
- {
- return NotFound("The media collection does not exist.");
- }
+ _libraryMonitor.Stop();
- if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
+ try
+ {
+ // Changing capitalization. Handle windows case insensitivity
+ if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
{
- return Conflict($"The media library already exists at {newPath}.");
+ var tempPath = Path.Combine(
+ rootFolderPath,
+ Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
+ Directory.Move(currentPath, tempPath);
+ currentPath = tempPath;
}
- _libraryMonitor.Stop();
+ Directory.Move(currentPath, newPath);
+ }
+ finally
+ {
+ CollectionFolder.OnCollectionFolderChange();
- try
+ Task.Run(async () =>
{
- // Changing capitalization. Handle windows case insensitivity
- if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
+ // No need to start if scanning the library because it will handle it
+ if (refreshLibrary)
{
- var tempPath = Path.Combine(
- rootFolderPath,
- Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
- Directory.Move(currentPath, tempPath);
- currentPath = tempPath;
+ await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
}
-
- Directory.Move(currentPath, newPath);
- }
- finally
- {
- CollectionFolder.OnCollectionFolderChange();
-
- Task.Run(async () =>
+ else
{
- // No need to start if scanning the library because it will handle it
- if (refreshLibrary)
- {
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
- }
- else
- {
- // Need to add a delay here or directory watchers may still pick up the changes
- // Have to block here to allow exceptions to bubble
- await Task.Delay(1000).ConfigureAwait(false);
- _libraryMonitor.Start();
- }
- });
- }
-
- return NoContent();
+ // Need to add a delay here or directory watchers may still pick up the changes
+ // Have to block here to allow exceptions to bubble
+ await Task.Delay(1000).ConfigureAwait(false);
+ _libraryMonitor.Start();
+ }
+ });
}
- /// <summary>
- /// Add a media path to a library.
- /// </summary>
- /// <param name="mediaPathDto">The media path dto.</param>
- /// <param name="refreshLibrary">Whether to refresh the library.</param>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- /// <response code="204">Media path added.</response>
- /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
- [HttpPost("Paths")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult AddMediaPath(
- [FromBody, Required] MediaPathDto mediaPathDto,
- [FromQuery] bool refreshLibrary = false)
- {
- _libraryMonitor.Stop();
+ return NoContent();
+ }
- try
- {
- var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null."));
+ /// <summary>
+ /// Add a media path to a library.
+ /// </summary>
+ /// <param name="mediaPathDto">The media path dto.</param>
+ /// <param name="refreshLibrary">Whether to refresh the library.</param>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ /// <response code="204">Media path added.</response>
+ /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
+ [HttpPost("Paths")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult AddMediaPath(
+ [FromBody, Required] MediaPathDto mediaPathDto,
+ [FromQuery] bool refreshLibrary = false)
+ {
+ _libraryMonitor.Stop();
- _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
- }
- 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();
- }
- });
- }
+ try
+ {
+ var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo(mediaPathDto.Path ?? throw new ArgumentException("PathInfo and Path can't both be null."));
- return NoContent();
+ _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
}
-
- /// <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)
+ finally
{
- if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name))
+ Task.Run(async () =>
{
- throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
- }
+ // 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();
+ }
- _libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo);
- 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");
}
- /// <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)
+ _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))
{
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
+ throw new ArgumentNullException(nameof(name));
+ }
- _libraryMonitor.Stop();
+ _libraryMonitor.Stop();
- try
- {
- _libraryManager.RemoveMediaPath(name, path);
- }
- finally
+ try
+ {
+ _libraryManager.RemoveMediaPath(name, path);
+ }
+ finally
+ {
+ Task.Run(async () =>
{
- Task.Run(async () =>
+ // No need to start if scanning the library because it will handle it
+ if (refreshLibrary)
{
- // No need to start if scanning the library because it will handle it
- if (refreshLibrary)
- {
- await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
- }
- else
- {
- // Need to add a delay here or directory watchers may still pick up the changes
- // Have to block here to allow exceptions to bubble
- await Task.Delay(1000).ConfigureAwait(false);
- _libraryMonitor.Start();
- }
- });
- }
-
- return NoContent();
+ 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();
+ }
+ });
}
- /// <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);
+ return NoContent();
+ }
- collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
- 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();
}
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 5228e0bab..318ed5c67 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -17,14 +17,12 @@ using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -35,1200 +33,1168 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Live tv controller.
+/// </summary>
+public class LiveTvController : BaseJellyfinApiController
{
+ private readonly ILiveTvManager _liveTvManager;
+ private readonly IUserManager _userManager;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IConfigurationManager _configurationManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly ISessionManager _sessionManager;
+
/// <summary>
- /// Live tv controller.
+ /// Initializes a new instance of the <see cref="LiveTvController"/> class.
/// </summary>
- public class LiveTvController : BaseJellyfinApiController
+ /// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> 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="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ public LiveTvController(
+ ILiveTvManager liveTvManager,
+ IUserManager userManager,
+ IHttpClientFactory httpClientFactory,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IMediaSourceManager mediaSourceManager,
+ IConfigurationManager configurationManager,
+ TranscodingJobHelper transcodingJobHelper,
+ ISessionManager sessionManager)
{
- private readonly ILiveTvManager _liveTvManager;
- private readonly IUserManager _userManager;
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IConfigurationManager _configurationManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly ISessionManager _sessionManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LiveTvController"/> class.
- /// </summary>
- /// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> 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="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
- /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- public LiveTvController(
- ILiveTvManager liveTvManager,
- IUserManager userManager,
- IHttpClientFactory httpClientFactory,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IMediaSourceManager mediaSourceManager,
- IConfigurationManager configurationManager,
- TranscodingJobHelper transcodingJobHelper,
- ISessionManager sessionManager)
- {
- _liveTvManager = liveTvManager;
- _userManager = userManager;
- _httpClientFactory = httpClientFactory;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _mediaSourceManager = mediaSourceManager;
- _configurationManager = configurationManager;
- _transcodingJobHelper = transcodingJobHelper;
- _sessionManager = sessionManager;
- }
+ _liveTvManager = liveTvManager;
+ _userManager = userManager;
+ _httpClientFactory = httpClientFactory;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _mediaSourceManager = mediaSourceManager;
+ _configurationManager = configurationManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _sessionManager = sessionManager;
+ }
- /// <summary>
- /// Gets available live tv services.
- /// </summary>
- /// <response code="200">Available live tv services returned.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the available live tv services.
- /// </returns>
- [HttpGet("Info")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<LiveTvInfo> GetLiveTvInfo()
- {
- return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
- }
+ /// <summary>
+ /// Gets available live tv services.
+ /// </summary>
+ /// <response code="200">Available live tv services returned.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the available live tv services.
+ /// </returns>
+ [HttpGet("Info")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public ActionResult<LiveTvInfo> GetLiveTvInfo()
+ {
+ return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
+ }
- /// <summary>
- /// Gets available live tv channels.
- /// </summary>
- /// <param name="type">Optional. Filter by channel type.</param>
- /// <param name="userId">Optional. Filter by user and attach user data.</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="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="limit">Optional. The maximum number of records to return.</param>
- /// <param name="isFavorite">Optional. Filter by channels that are favorites, or not.</param>
- /// <param name="isLiked">Optional. Filter by channels that are liked, or not.</param>
- /// <param name="isDisliked">Optional. Filter by channels that are disliked, 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="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="sortBy">Optional. Key to sort by.</param>
- /// <param name="sortOrder">Optional. Sort order.</param>
- /// <param name="enableFavoriteSorting">Optional. Incorporate favorite and like status into channel sorting.</param>
- /// <param name="addCurrentProgram">Optional. Adds current program info to each channel.</param>
- /// <response code="200">Available live tv channels returned.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the resulting available live tv channels.
- /// </returns>
- [HttpGet("Channels")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
- [FromQuery] ChannelType? type,
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery] int? limit,
- [FromQuery] bool? isFavorite,
- [FromQuery] bool? isLiked,
- [FromQuery] bool? isDisliked,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableUserData,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery] SortOrder? sortOrder,
- [FromQuery] bool enableFavoriteSorting = false,
- [FromQuery] bool addCurrentProgram = true)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
-
- var channelResult = _liveTvManager.GetInternalChannels(
- new LiveTvChannelQuery
- {
- ChannelType = type,
- UserId = userId ?? Guid.Empty,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- IsLiked = isLiked,
- IsDisliked = isDisliked,
- EnableFavoriteSorting = enableFavoriteSorting,
- IsMovie = isMovie,
- IsSeries = isSeries,
- IsNews = isNews,
- IsKids = isKids,
- IsSports = isSports,
- SortBy = sortBy,
- SortOrder = sortOrder ?? SortOrder.Ascending,
- AddCurrentProgram = addCurrentProgram
- },
- dtoOptions,
- CancellationToken.None);
-
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var fieldsList = dtoOptions.Fields.ToList();
- fieldsList.Remove(ItemFields.CanDelete);
- fieldsList.Remove(ItemFields.CanDownload);
- fieldsList.Remove(ItemFields.DisplayPreferencesId);
- fieldsList.Remove(ItemFields.Etag);
- dtoOptions.Fields = fieldsList.ToArray();
- dtoOptions.AddCurrentProgram = addCurrentProgram;
-
- var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, dtoOptions, user);
- return new QueryResult<BaseItemDto>(
- startIndex,
- channelResult.TotalRecordCount,
- returnArray);
- }
+ /// <summary>
+ /// Gets available live tv channels.
+ /// </summary>
+ /// <param name="type">Optional. Filter by channel type.</param>
+ /// <param name="userId">Optional. Filter by user and attach user data.</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="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="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="isFavorite">Optional. Filter by channels that are favorites, or not.</param>
+ /// <param name="isLiked">Optional. Filter by channels that are liked, or not.</param>
+ /// <param name="isDisliked">Optional. Filter by channels that are disliked, 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="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="sortBy">Optional. Key to sort by.</param>
+ /// <param name="sortOrder">Optional. Sort order.</param>
+ /// <param name="enableFavoriteSorting">Optional. Incorporate favorite and like status into channel sorting.</param>
+ /// <param name="addCurrentProgram">Optional. Adds current program info to each channel.</param>
+ /// <response code="200">Available live tv channels returned.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the resulting available live tv channels.
+ /// </returns>
+ [HttpGet("Channels")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
+ [FromQuery] ChannelType? type,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery] int? limit,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] bool? isLiked,
+ [FromQuery] bool? isDisliked,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableUserData,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery] SortOrder? sortOrder,
+ [FromQuery] bool enableFavoriteSorting = false,
+ [FromQuery] bool addCurrentProgram = true)
+ {
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- /// <summary>
- /// Gets a live tv channel.
- /// </summary>
- /// <param name="channelId">Channel id.</param>
- /// <param name="userId">Optional. Attach user data.</param>
- /// <response code="200">Live tv channel returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
- [HttpGet("Channels/{channelId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var item = channelId.Equals(default)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(channelId);
-
- var dtoOptions = new DtoOptions()
- .AddClientFields(User);
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ var channelResult = _liveTvManager.GetInternalChannels(
+ new LiveTvChannelQuery
+ {
+ ChannelType = type,
+ UserId = userId ?? Guid.Empty,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ IsLiked = isLiked,
+ IsDisliked = isDisliked,
+ EnableFavoriteSorting = enableFavoriteSorting,
+ IsMovie = isMovie,
+ IsSeries = isSeries,
+ IsNews = isNews,
+ IsKids = isKids,
+ IsSports = isSports,
+ SortBy = sortBy,
+ SortOrder = sortOrder ?? SortOrder.Ascending,
+ AddCurrentProgram = addCurrentProgram
+ },
+ dtoOptions,
+ CancellationToken.None);
+
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ var fieldsList = dtoOptions.Fields.ToList();
+ fieldsList.Remove(ItemFields.CanDelete);
+ fieldsList.Remove(ItemFields.CanDownload);
+ fieldsList.Remove(ItemFields.DisplayPreferencesId);
+ fieldsList.Remove(ItemFields.Etag);
+ dtoOptions.Fields = fieldsList.ToArray();
+ dtoOptions.AddCurrentProgram = addCurrentProgram;
+
+ var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, dtoOptions, user);
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ channelResult.TotalRecordCount,
+ returnArray);
+ }
- /// <summary>
- /// Gets live tv recordings.
- /// </summary>
- /// <param name="channelId">Optional. Filter by channel id.</param>
- /// <param name="userId">Optional. Filter by user and attach user data.</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="status">Optional. Filter by recording status.</param>
- /// <param name="isInProgress">Optional. Filter by recordings that are in progress, or not.</param>
- /// <param name="seriesTimerId">Optional. Filter by recordings belonging to a series timer.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="isMovie">Optional. Filter for movies.</param>
- /// <param name="isSeries">Optional. Filter for series.</param>
- /// <param name="isKids">Optional. Filter for kids.</param>
- /// <param name="isSports">Optional. Filter for sports.</param>
- /// <param name="isNews">Optional. Filter for news.</param>
- /// <param name="isLibraryItem">Optional. Filter for is library item.</param>
- /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
- /// <response code="200">Live tv recordings returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
- [HttpGet("Recordings")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
- [FromQuery] string? channelId,
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] RecordingStatus? status,
- [FromQuery] bool? isInProgress,
- [FromQuery] string? seriesTimerId,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableUserData,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isLibraryItem,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
-
- return _liveTvManager.GetRecordings(
- new RecordingQuery
- {
- ChannelId = channelId,
- UserId = userId ?? Guid.Empty,
- StartIndex = startIndex,
- Limit = limit,
- Status = status,
- SeriesTimerId = seriesTimerId,
- IsInProgress = isInProgress,
- EnableTotalRecordCount = enableTotalRecordCount,
- IsMovie = isMovie,
- IsNews = isNews,
- IsSeries = isSeries,
- IsKids = isKids,
- IsSports = isSports,
- IsLibraryItem = isLibraryItem,
- Fields = fields,
- ImageTypeLimit = imageTypeLimit,
- EnableImages = enableImages
- },
- dtoOptions);
- }
+ /// <summary>
+ /// Gets a live tv channel.
+ /// </summary>
+ /// <param name="channelId">Channel id.</param>
+ /// <param name="userId">Optional. Attach user data.</param>
+ /// <response code="200">Live tv channel returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
+ [HttpGet("Channels/{channelId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = channelId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(channelId);
+
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(User);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
- /// <summary>
- /// Gets live tv recording series.
- /// </summary>
- /// <param name="channelId">Optional. Filter by channel id.</param>
- /// <param name="userId">Optional. Filter by user and attach user data.</param>
- /// <param name="groupId">Optional. Filter by recording group.</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="status">Optional. Filter by recording status.</param>
- /// <param name="isInProgress">Optional. Filter by recordings that are in progress, or not.</param>
- /// <param name="seriesTimerId">Optional. Filter by recordings belonging to a series timer.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
- /// <response code="200">Live tv recordings returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
- [HttpGet("Recordings/Series")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [Obsolete("This endpoint is obsolete.")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "groupId", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "status", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isInProgress", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesTimerId", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "fields", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordingsSeries(
- [FromQuery] string? channelId,
- [FromQuery] Guid? userId,
- [FromQuery] string? groupId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] RecordingStatus? status,
- [FromQuery] bool? isInProgress,
- [FromQuery] string? seriesTimerId,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableUserData,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- return new QueryResult<BaseItemDto>();
- }
+ /// <summary>
+ /// Gets live tv recordings.
+ /// </summary>
+ /// <param name="channelId">Optional. Filter by channel id.</param>
+ /// <param name="userId">Optional. Filter by user and attach user data.</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="status">Optional. Filter by recording status.</param>
+ /// <param name="isInProgress">Optional. Filter by recordings that are in progress, or not.</param>
+ /// <param name="seriesTimerId">Optional. Filter by recordings belonging to a series timer.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="isMovie">Optional. Filter for movies.</param>
+ /// <param name="isSeries">Optional. Filter for series.</param>
+ /// <param name="isKids">Optional. Filter for kids.</param>
+ /// <param name="isSports">Optional. Filter for sports.</param>
+ /// <param name="isNews">Optional. Filter for news.</param>
+ /// <param name="isLibraryItem">Optional. Filter for is library item.</param>
+ /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
+ /// <response code="200">Live tv recordings returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
+ [HttpGet("Recordings")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
+ [FromQuery] string? channelId,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] RecordingStatus? status,
+ [FromQuery] bool? isInProgress,
+ [FromQuery] string? seriesTimerId,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isLibraryItem,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- /// <summary>
- /// Gets live tv recording groups.
- /// </summary>
- /// <param name="userId">Optional. Filter by user and attach user data.</param>
- /// <response code="200">Recording groups returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
- [HttpGet("Recordings/Groups")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [Obsolete("This endpoint is obsolete.")]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
- {
- return new QueryResult<BaseItemDto>();
- }
+ return _liveTvManager.GetRecordings(
+ new RecordingQuery
+ {
+ ChannelId = channelId,
+ UserId = userId ?? Guid.Empty,
+ StartIndex = startIndex,
+ Limit = limit,
+ Status = status,
+ SeriesTimerId = seriesTimerId,
+ IsInProgress = isInProgress,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ IsMovie = isMovie,
+ IsNews = isNews,
+ IsSeries = isSeries,
+ IsKids = isKids,
+ IsSports = isSports,
+ IsLibraryItem = isLibraryItem,
+ Fields = fields,
+ ImageTypeLimit = imageTypeLimit,
+ EnableImages = enableImages
+ },
+ dtoOptions);
+ }
- /// <summary>
- /// Gets recording folders.
- /// </summary>
- /// <param name="userId">Optional. Filter by user and attach user data.</param>
- /// <response code="200">Recording folders returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
- [HttpGet("Recordings/Folders")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var folders = _liveTvManager.GetRecordingFolders(user);
+ /// <summary>
+ /// Gets live tv recording series.
+ /// </summary>
+ /// <param name="channelId">Optional. Filter by channel id.</param>
+ /// <param name="userId">Optional. Filter by user and attach user data.</param>
+ /// <param name="groupId">Optional. Filter by recording group.</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="status">Optional. Filter by recording status.</param>
+ /// <param name="isInProgress">Optional. Filter by recordings that are in progress, or not.</param>
+ /// <param name="seriesTimerId">Optional. Filter by recordings belonging to a series timer.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
+ /// <response code="200">Live tv recordings returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
+ [HttpGet("Recordings/Series")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [Obsolete("This endpoint is obsolete.")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "groupId", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "status", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isInProgress", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "seriesTimerId", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "fields", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableTotalRecordCount", Justification = "Imported from ServiceStack")]
+ public ActionResult<QueryResult<BaseItemDto>> GetRecordingsSeries(
+ [FromQuery] string? channelId,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? groupId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] RecordingStatus? status,
+ [FromQuery] bool? isInProgress,
+ [FromQuery] string? seriesTimerId,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ return new QueryResult<BaseItemDto>();
+ }
- var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
+ /// <summary>
+ /// Gets live tv recording groups.
+ /// </summary>
+ /// <param name="userId">Optional. Filter by user and attach user data.</param>
+ /// <response code="200">Recording groups returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
+ [HttpGet("Recordings/Groups")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [Obsolete("This endpoint is obsolete.")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
+ public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
+ {
+ return new QueryResult<BaseItemDto>();
+ }
- return new QueryResult<BaseItemDto>(returnArray);
- }
+ /// <summary>
+ /// Gets recording folders.
+ /// </summary>
+ /// <param name="userId">Optional. Filter by user and attach user data.</param>
+ /// <response code="200">Recording folders returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
+ [HttpGet("Recordings/Folders")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var folders = _liveTvManager.GetRecordingFolders(user);
- /// <summary>
- /// Gets a live tv recording.
- /// </summary>
- /// <param name="recordingId">Recording id.</param>
- /// <param name="userId">Optional. Attach user data.</param>
- /// <response code="200">Recording returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
- [HttpGet("Recordings/{recordingId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
+ var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user);
- var dtoOptions = new DtoOptions()
- .AddClientFields(User);
+ return new QueryResult<BaseItemDto>(returnArray);
+ }
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ /// <summary>
+ /// Gets a live tv recording.
+ /// </summary>
+ /// <param name="recordingId">Recording id.</param>
+ /// <param name="userId">Optional. Attach user data.</param>
+ /// <response code="200">Recording returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
+ [HttpGet("Recordings/{recordingId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
- /// <summary>
- /// Resets a tv tuner.
- /// </summary>
- /// <param name="tunerId">Tuner id.</param>
- /// <response code="204">Tuner reset.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Tuners/{tunerId}/Reset")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
- {
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(User);
- /// <summary>
- /// Gets a timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <response code="200">Timer returned.</response>
- /// <returns>
- /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the timer.
- /// </returns>
- [HttpGet("Timers/{timerId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
- {
- return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
- }
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
- /// <summary>
- /// Gets the default values for a new timer.
- /// </summary>
- /// <param name="programId">Optional. To attach default values based on a program.</param>
- /// <response code="200">Default values returned.</response>
- /// <returns>
- /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the default values for a timer.
- /// </returns>
- [HttpGet("Timers/Defaults")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
- {
- return string.IsNullOrEmpty(programId)
- ? await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false)
- : await _liveTvManager.GetNewTimerDefaults(programId, CancellationToken.None).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Resets a tv tuner.
+ /// </summary>
+ /// <param name="tunerId">Tuner id.</param>
+ /// <response code="204">Tuner reset.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Tuners/{tunerId}/Reset")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
+ {
+ await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Gets the live tv timers.
- /// </summary>
- /// <param name="channelId">Optional. Filter by channel id.</param>
- /// <param name="seriesTimerId">Optional. Filter by timers belonging to a series timer.</param>
- /// <param name="isActive">Optional. Filter by timers that are active.</param>
- /// <param name="isScheduled">Optional. Filter by timers that are scheduled.</param>
- /// <returns>
- /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the live tv timers.
- /// </returns>
- [HttpGet("Timers")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
- [FromQuery] string? channelId,
- [FromQuery] string? seriesTimerId,
- [FromQuery] bool? isActive,
- [FromQuery] bool? isScheduled)
- {
- return await _liveTvManager.GetTimers(
- new TimerQuery
- {
- ChannelId = channelId,
- SeriesTimerId = seriesTimerId,
- IsActive = isActive,
- IsScheduled = isScheduled
- },
- CancellationToken.None).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets a timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <response code="200">Timer returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the timer.
+ /// </returns>
+ [HttpGet("Timers/{timerId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
+ {
+ return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets available live tv epgs.
- /// </summary>
- /// <param name="channelIds">The channels to return guide information for.</param>
- /// <param name="userId">Optional. Filter by user id.</param>
- /// <param name="minStartDate">Optional. The minimum premiere start date.</param>
- /// <param name="hasAired">Optional. Filter by programs that have completed airing, or not.</param>
- /// <param name="isAiring">Optional. Filter by programs that are currently airing, or not.</param>
- /// <param name="maxStartDate">Optional. The maximum premiere start date.</param>
- /// <param name="minEndDate">Optional. The minimum premiere end date.</param>
- /// <param name="maxEndDate">Optional. The maximum premiere end date.</param>
- /// <param name="isMovie">Optional. Filter for movies.</param>
- /// <param name="isSeries">Optional. Filter for series.</param>
- /// <param name="isNews">Optional. Filter for news.</param>
- /// <param name="isKids">Optional. Filter for kids.</param>
- /// <param name="isSports">Optional. Filter for sports.</param>
- /// <param name="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="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Name, StartDate.</param>
- /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
- /// <param name="genres">The genres to return guide information for.</param>
- /// <param name="genreIds">The genre ids to return guide information for.</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="seriesTimerId">Optional. Filter by series timer id.</param>
- /// <param name="librarySeriesId">Optional. Filter by library series id.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
- /// <response code="200">Live tv epgs returned.</response>
- /// <returns>
- /// A <see cref="Task"/> containing a <see cref="OkResult"/> which contains the live tv epgs.
- /// </returns>
- [HttpGet("Programs")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
- [FromQuery] Guid? userId,
- [FromQuery] DateTime? minStartDate,
- [FromQuery] bool? hasAired,
- [FromQuery] bool? isAiring,
- [FromQuery] DateTime? maxStartDate,
- [FromQuery] DateTime? minEndDate,
- [FromQuery] DateTime? maxEndDate,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] bool? enableUserData,
- [FromQuery] string? seriesTimerId,
- [FromQuery] Guid? librarySeriesId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ /// <summary>
+ /// Gets the default values for a new timer.
+ /// </summary>
+ /// <param name="programId">Optional. To attach default values based on a program.</param>
+ /// <response code="200">Default values returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the default values for a timer.
+ /// </returns>
+ [HttpGet("Timers/Defaults")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
+ {
+ return string.IsNullOrEmpty(programId)
+ ? await _liveTvManager.GetNewTimerDefaults(CancellationToken.None).ConfigureAwait(false)
+ : await _liveTvManager.GetNewTimerDefaults(programId, CancellationToken.None).ConfigureAwait(false);
+ }
- var query = new InternalItemsQuery(user)
+ /// <summary>
+ /// Gets the live tv timers.
+ /// </summary>
+ /// <param name="channelId">Optional. Filter by channel id.</param>
+ /// <param name="seriesTimerId">Optional. Filter by timers belonging to a series timer.</param>
+ /// <param name="isActive">Optional. Filter by timers that are active.</param>
+ /// <param name="isScheduled">Optional. Filter by timers that are scheduled.</param>
+ /// <returns>
+ /// A <see cref="Task"/> containing an <see cref="OkResult"/> which contains the live tv timers.
+ /// </returns>
+ [HttpGet("Timers")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
+ [FromQuery] string? channelId,
+ [FromQuery] string? seriesTimerId,
+ [FromQuery] bool? isActive,
+ [FromQuery] bool? isScheduled)
+ {
+ return await _liveTvManager.GetTimers(
+ new TimerQuery
{
- ChannelIds = channelIds,
- HasAired = hasAired,
- IsAiring = isAiring,
- EnableTotalRecordCount = enableTotalRecordCount,
- MinStartDate = minStartDate,
- MinEndDate = minEndDate,
- MaxStartDate = maxStartDate,
- MaxEndDate = maxEndDate,
- StartIndex = startIndex,
- Limit = limit,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
- IsNews = isNews,
- IsMovie = isMovie,
- IsSeries = isSeries,
- IsKids = isKids,
- IsSports = isSports,
+ ChannelId = channelId,
SeriesTimerId = seriesTimerId,
- Genres = genres,
- GenreIds = genreIds
- };
-
- if (librarySeriesId.HasValue && !librarySeriesId.Equals(default))
- {
- query.IsSeries = true;
-
- if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
- {
- query.Name = series.Name;
- }
- }
+ IsActive = isActive,
+ IsScheduled = isScheduled
+ },
+ CancellationToken.None).ConfigureAwait(false);
+ }
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets available live tv epgs.
+ /// </summary>
+ /// <param name="channelIds">The channels to return guide information for.</param>
+ /// <param name="userId">Optional. Filter by user id.</param>
+ /// <param name="minStartDate">Optional. The minimum premiere start date.</param>
+ /// <param name="hasAired">Optional. Filter by programs that have completed airing, or not.</param>
+ /// <param name="isAiring">Optional. Filter by programs that are currently airing, or not.</param>
+ /// <param name="maxStartDate">Optional. The maximum premiere start date.</param>
+ /// <param name="minEndDate">Optional. The minimum premiere end date.</param>
+ /// <param name="maxEndDate">Optional. The maximum premiere end date.</param>
+ /// <param name="isMovie">Optional. Filter for movies.</param>
+ /// <param name="isSeries">Optional. Filter for series.</param>
+ /// <param name="isNews">Optional. Filter for news.</param>
+ /// <param name="isKids">Optional. Filter for kids.</param>
+ /// <param name="isSports">Optional. Filter for sports.</param>
+ /// <param name="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="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Name, StartDate.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+ /// <param name="genres">The genres to return guide information for.</param>
+ /// <param name="genreIds">The genre ids to return guide information for.</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="seriesTimerId">Optional. Filter by series timer id.</param>
+ /// <param name="librarySeriesId">Optional. Filter by library series id.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
+ /// <response code="200">Live tv epgs returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> containing a <see cref="OkResult"/> which contains the live tv epgs.
+ /// </returns>
+ [HttpGet("Programs")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
+ [FromQuery] Guid? userId,
+ [FromQuery] DateTime? minStartDate,
+ [FromQuery] bool? hasAired,
+ [FromQuery] bool? isAiring,
+ [FromQuery] DateTime? maxStartDate,
+ [FromQuery] DateTime? minEndDate,
+ [FromQuery] DateTime? maxEndDate,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] string? seriesTimerId,
+ [FromQuery] Guid? librarySeriesId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- /// <summary>
- /// Gets available live tv epgs.
- /// </summary>
- /// <param name="body">Request body.</param>
- /// <response code="200">Live tv epgs returned.</response>
- /// <returns>
- /// A <see cref="Task"/> containing a <see cref="OkResult"/> which contains the live tv epgs.
- /// </returns>
- [HttpPost("Programs")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
+ var query = new InternalItemsQuery(user)
{
- var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
+ ChannelIds = channelIds,
+ HasAired = hasAired,
+ IsAiring = isAiring,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ MinStartDate = minStartDate,
+ MinEndDate = minEndDate,
+ MaxStartDate = maxStartDate,
+ MaxEndDate = maxEndDate,
+ StartIndex = startIndex,
+ Limit = limit,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
+ IsNews = isNews,
+ IsMovie = isMovie,
+ IsSeries = isSeries,
+ IsKids = isKids,
+ IsSports = isSports,
+ SeriesTimerId = seriesTimerId,
+ Genres = genres,
+ GenreIds = genreIds
+ };
+
+ if (librarySeriesId.HasValue && !librarySeriesId.Equals(default))
+ {
+ query.IsSeries = true;
- var query = new InternalItemsQuery(user)
- {
- ChannelIds = body.ChannelIds,
- HasAired = body.HasAired,
- IsAiring = body.IsAiring,
- EnableTotalRecordCount = body.EnableTotalRecordCount,
- MinStartDate = body.MinStartDate,
- MinEndDate = body.MinEndDate,
- MaxStartDate = body.MaxStartDate,
- MaxEndDate = body.MaxEndDate,
- StartIndex = body.StartIndex,
- Limit = body.Limit,
- OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder),
- IsNews = body.IsNews,
- IsMovie = body.IsMovie,
- IsSeries = body.IsSeries,
- IsKids = body.IsKids,
- IsSports = body.IsSports,
- SeriesTimerId = body.SeriesTimerId,
- Genres = body.Genres,
- GenreIds = body.GenreIds
- };
-
- if (!body.LibrarySeriesId.Equals(default))
+ if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
{
- query.IsSeries = true;
-
- if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series)
- {
- query.Name = series.Name;
- }
+ query.Name = series.Name;
}
-
- var dtoOptions = new DtoOptions { Fields = body.Fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
- return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
}
- /// <summary>
- /// Gets recommended live tv epgs.
- /// </summary>
- /// <param name="userId">Optional. filter by user id.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="isAiring">Optional. Filter by programs that are currently airing, or not.</param>
- /// <param name="hasAired">Optional. Filter by programs that have completed airing, or not.</param>
- /// <param name="isSeries">Optional. Filter for series.</param>
- /// <param name="isMovie">Optional. Filter for movies.</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="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="genreIds">The genres to return guide information for.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="enableUserData">Optional. include user data.</param>
- /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
- /// <response code="200">Recommended epgs returned.</response>
- /// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
- [HttpGet("Programs/Recommended")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
- [FromQuery] Guid? userId,
- [FromQuery] int? limit,
- [FromQuery] bool? isAiring,
- [FromQuery] bool? hasAired,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? enableUserData,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- var query = new InternalItemsQuery(user)
- {
- IsAiring = isAiring,
- Limit = limit,
- HasAired = hasAired,
- IsSeries = isSeries,
- IsMovie = isMovie,
- IsKids = isKids,
- IsNews = isNews,
- IsSports = isSports,
- EnableTotalRecordCount = enableTotalRecordCount,
- GenreIds = genreIds
- };
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
+ }
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets available live tv epgs.
+ /// </summary>
+ /// <param name="body">Request body.</param>
+ /// <response code="200">Live tv epgs returned.</response>
+ /// <returns>
+ /// A <see cref="Task"/> containing a <see cref="OkResult"/> which contains the live tv epgs.
+ /// </returns>
+ [HttpPost("Programs")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
+ {
+ var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
- /// <summary>
- /// Gets a live tv program.
- /// </summary>
- /// <param name="programId">Program id.</param>
- /// <param name="userId">Optional. Attach user data.</param>
- /// <response code="200">Program returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
- [HttpGet("Programs/{programId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<BaseItemDto>> GetProgram(
- [FromRoute, Required] string programId,
- [FromQuery] Guid? userId)
+ var query = new InternalItemsQuery(user)
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
-
- return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Deletes a live tv recording.
- /// </summary>
- /// <param name="recordingId">Recording id.</param>
- /// <response code="204">Recording deleted.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
- [HttpDelete("Recordings/{recordingId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteRecording([FromRoute, Required] Guid recordingId)
+ ChannelIds = body.ChannelIds,
+ HasAired = body.HasAired,
+ IsAiring = body.IsAiring,
+ EnableTotalRecordCount = body.EnableTotalRecordCount,
+ MinStartDate = body.MinStartDate,
+ MinEndDate = body.MinEndDate,
+ MaxStartDate = body.MaxStartDate,
+ MaxEndDate = body.MaxEndDate,
+ StartIndex = body.StartIndex,
+ Limit = body.Limit,
+ OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder),
+ IsNews = body.IsNews,
+ IsMovie = body.IsMovie,
+ IsSeries = body.IsSeries,
+ IsKids = body.IsKids,
+ IsSports = body.IsSports,
+ SeriesTimerId = body.SeriesTimerId,
+ Genres = body.Genres,
+ GenreIds = body.GenreIds
+ };
+
+ if (!body.LibrarySeriesId.Equals(default))
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
+ query.IsSeries = true;
- var item = _libraryManager.GetItemById(recordingId);
- if (item is null)
+ if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series)
{
- return NotFound();
+ query.Name = series.Name;
}
-
- _libraryManager.DeleteItem(item, new DeleteOptions
- {
- DeleteFileLocation = false
- });
-
- return NoContent();
}
- /// <summary>
- /// Cancels a live tv timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <response code="204">Timer deleted.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Timers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
- {
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
- return NoContent();
- }
+ var dtoOptions = new DtoOptions { Fields = body.Fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
+ return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
+ }
- /// <summary>
- /// Updates a live tv timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <param name="timerInfo">New timer info.</param>
- /// <response code="204">Timer updated.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Timers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
- {
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ /// <summary>
+ /// Gets recommended live tv epgs.
+ /// </summary>
+ /// <param name="userId">Optional. filter by user id.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="isAiring">Optional. Filter by programs that are currently airing, or not.</param>
+ /// <param name="hasAired">Optional. Filter by programs that have completed airing, or not.</param>
+ /// <param name="isSeries">Optional. Filter for series.</param>
+ /// <param name="isMovie">Optional. Filter for movies.</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="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="genreIds">The genres to return guide information for.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableUserData">Optional. include user data.</param>
+ /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
+ /// <response code="200">Recommended epgs returned.</response>
+ /// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
+ [HttpGet("Programs/Recommended")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
+ [FromQuery] Guid? userId,
+ [FromQuery] int? limit,
+ [FromQuery] bool? isAiring,
+ [FromQuery] bool? hasAired,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- /// <summary>
- /// Creates a live tv timer.
- /// </summary>
- /// <param name="timerInfo">New timer info.</param>
- /// <response code="204">Timer created.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Timers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
+ var query = new InternalItemsQuery(user)
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ IsAiring = isAiring,
+ Limit = limit,
+ HasAired = hasAired,
+ IsSeries = isSeries,
+ IsMovie = isMovie,
+ IsKids = isKids,
+ IsNews = isNews,
+ IsSports = isSports,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ GenreIds = genreIds
+ };
+
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets a live tv series timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <response code="200">Series timer returned.</response>
- /// <response code="404">Series timer not found.</response>
- /// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
- [HttpGet("SeriesTimers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
- {
- var timer = await _liveTvManager.GetSeriesTimer(timerId, CancellationToken.None).ConfigureAwait(false);
- if (timer is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Gets a live tv program.
+ /// </summary>
+ /// <param name="programId">Program id.</param>
+ /// <param name="userId">Optional. Attach user data.</param>
+ /// <response code="200">Program returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
+ [HttpGet("Programs/{programId}")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<BaseItemDto>> GetProgram(
+ [FromRoute, Required] string programId,
+ [FromQuery] Guid? userId)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- return timer;
- }
+ return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets live tv series timers.
- /// </summary>
- /// <param name="sortBy">Optional. Sort by SortName or Priority.</param>
- /// <param name="sortOrder">Optional. Sort in Ascending or Descending order.</param>
- /// <response code="200">Timers returned.</response>
- /// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
- [HttpGet("SeriesTimers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
+ /// <summary>
+ /// Deletes a live tv recording.
+ /// </summary>
+ /// <param name="recordingId">Recording id.</param>
+ /// <response code="204">Recording deleted.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpDelete("Recordings/{recordingId}")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
+ {
+ var item = _libraryManager.GetItemById(recordingId);
+ if (item is null)
{
- return await _liveTvManager.GetSeriesTimers(
- new SeriesTimerQuery
- {
- SortOrder = sortOrder ?? SortOrder.Ascending,
- SortBy = sortBy
- },
- CancellationToken.None).ConfigureAwait(false);
+ return NotFound();
}
- /// <summary>
- /// Cancels a live tv series timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <response code="204">Timer cancelled.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("SeriesTimers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
+ _libraryManager.DeleteItem(item, new DeleteOptions
{
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
- return NoContent();
- }
+ DeleteFileLocation = false
+ });
- /// <summary>
- /// Updates a live tv series timer.
- /// </summary>
- /// <param name="timerId">Timer id.</param>
- /// <param name="seriesTimerInfo">New series timer info.</param>
- /// <response code="204">Series timer updated.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("SeriesTimers/{timerId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
- {
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ return NoContent();
+ }
- /// <summary>
- /// Creates a live tv series timer.
- /// </summary>
- /// <param name="seriesTimerInfo">New series timer info.</param>
- /// <response code="204">Series timer info created.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("SeriesTimers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
- {
- await AssertUserCanManageLiveTv().ConfigureAwait(false);
- await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
- }
+ /// <summary>
+ /// Cancels a live tv timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <response code="204">Timer deleted.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Timers/{timerId}")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
+ {
+ await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Updates a live tv timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <param name="timerInfo">New timer info.</param>
+ /// <response code="204">Timer updated.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Timers/{timerId}")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
+ {
+ await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Creates a live tv timer.
+ /// </summary>
+ /// <param name="timerInfo">New timer info.</param>
+ /// <response code="204">Timer created.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Timers")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
+ {
+ await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Get recording group.
- /// </summary>
- /// <param name="groupId">Group id.</param>
- /// <returns>A <see cref="NotFoundResult"/>.</returns>
- [HttpGet("Recordings/Groups/{groupId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [Obsolete("This endpoint is obsolete.")]
- public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
+ /// <summary>
+ /// Gets a live tv series timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <response code="200">Series timer returned.</response>
+ /// <response code="404">Series timer not found.</response>
+ /// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
+ [HttpGet("SeriesTimers/{timerId}")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
+ {
+ var timer = await _liveTvManager.GetSeriesTimer(timerId, CancellationToken.None).ConfigureAwait(false);
+ if (timer is null)
{
return NotFound();
}
- /// <summary>
- /// Get guid info.
- /// </summary>
- /// <response code="200">Guid info returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
- [HttpGet("GuideInfo")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<GuideInfo> GetGuideInfo()
- {
- return _liveTvManager.GetGuideInfo();
- }
+ return timer;
+ }
- /// <summary>
- /// Adds a tuner host.
- /// </summary>
- /// <param name="tunerHostInfo">New tuner host.</param>
- /// <response code="200">Created tuner host returned.</response>
- /// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
- [HttpPost("TunerHosts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
- {
- return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets live tv series timers.
+ /// </summary>
+ /// <param name="sortBy">Optional. Sort by SortName or Priority.</param>
+ /// <param name="sortOrder">Optional. Sort in Ascending or Descending order.</param>
+ /// <response code="200">Timers returned.</response>
+ /// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
+ [HttpGet("SeriesTimers")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
+ {
+ return await _liveTvManager.GetSeriesTimers(
+ new SeriesTimerQuery
+ {
+ SortOrder = sortOrder ?? SortOrder.Ascending,
+ SortBy = sortBy
+ },
+ CancellationToken.None).ConfigureAwait(false);
+ }
- /// <summary>
- /// Deletes a tuner host.
- /// </summary>
- /// <param name="id">Tuner host id.</param>
- /// <response code="204">Tuner host deleted.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("TunerHosts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult DeleteTunerHost([FromQuery] string? id)
- {
- var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
- config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
- _configurationManager.SaveConfiguration("livetv", config);
- return NoContent();
- }
+ /// <summary>
+ /// Cancels a live tv series timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <response code="204">Timer cancelled.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("SeriesTimers/{timerId}")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
+ {
+ await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Gets default listings provider info.
- /// </summary>
- /// <response code="200">Default listings provider info returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
- [HttpGet("ListingProviders/Default")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
- {
- return new ListingsProviderInfo();
- }
+ /// <summary>
+ /// Updates a live tv series timer.
+ /// </summary>
+ /// <param name="timerId">Timer id.</param>
+ /// <param name="seriesTimerInfo">New series timer info.</param>
+ /// <response code="204">Series timer updated.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("SeriesTimers/{timerId}")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
+ {
+ await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
- /// <summary>
- /// Adds a listings provider.
- /// </summary>
- /// <param name="pw">Password.</param>
- /// <param name="listingsProviderInfo">New listings info.</param>
- /// <param name="validateListings">Validate listings.</param>
- /// <param name="validateLogin">Validate login.</param>
- /// <response code="200">Created listings provider returned.</response>
- /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
- [HttpPost("ListingProviders")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
- [FromQuery] string? pw,
- [FromBody] ListingsProviderInfo listingsProviderInfo,
- [FromQuery] bool validateListings = false,
- [FromQuery] bool validateLogin = false)
- {
- if (!string.IsNullOrEmpty(pw))
- {
- // TODO: remove ToLower when Convert.ToHexString supports lowercase
- // Schedules Direct requires the hex to be lowercase
- listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
- }
+ /// <summary>
+ /// Creates a live tv series timer.
+ /// </summary>
+ /// <param name="seriesTimerInfo">New series timer info.</param>
+ /// <response code="204">Series timer info created.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("SeriesTimers")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
+ {
+ await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
- return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
- }
+ /// <summary>
+ /// Get recording group.
+ /// </summary>
+ /// <param name="groupId">Group id.</param>
+ /// <returns>A <see cref="NotFoundResult"/>.</returns>
+ [HttpGet("Recordings/Groups/{groupId}")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("This endpoint is obsolete.")]
+ public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
+ {
+ return NotFound();
+ }
- /// <summary>
- /// Delete listing provider.
- /// </summary>
- /// <param name="id">Listing provider id.</param>
- /// <response code="204">Listing provider deleted.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("ListingProviders")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult DeleteListingProvider([FromQuery] string? id)
- {
- _liveTvManager.DeleteListingsProvider(id);
- return NoContent();
- }
+ /// <summary>
+ /// Get guid info.
+ /// </summary>
+ /// <response code="200">Guid info returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
+ [HttpGet("GuideInfo")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<GuideInfo> GetGuideInfo()
+ {
+ return _liveTvManager.GetGuideInfo();
+ }
+
+ /// <summary>
+ /// Adds a tuner host.
+ /// </summary>
+ /// <param name="tunerHostInfo">New tuner host.</param>
+ /// <response code="200">Created tuner host returned.</response>
+ /// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
+ [HttpPost("TunerHosts")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
+ {
+ return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Deletes a tuner host.
+ /// </summary>
+ /// <param name="id">Tuner host id.</param>
+ /// <response code="204">Tuner host deleted.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("TunerHosts")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult DeleteTunerHost([FromQuery] string? id)
+ {
+ var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
+ config.TunerHosts = config.TunerHosts.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
+ _configurationManager.SaveConfiguration("livetv", config);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets default listings provider info.
+ /// </summary>
+ /// <response code="200">Default listings provider info returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
+ [HttpGet("ListingProviders/Default")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
+ {
+ return new ListingsProviderInfo();
+ }
- /// <summary>
- /// Gets available lineups.
- /// </summary>
- /// <param name="id">Provider id.</param>
- /// <param name="type">Provider type.</param>
- /// <param name="location">Location.</param>
- /// <param name="country">Country.</param>
- /// <response code="200">Available lineups returned.</response>
- /// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
- [HttpGet("ListingProviders/Lineups")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
- [FromQuery] string? id,
- [FromQuery] string? type,
- [FromQuery] string? location,
- [FromQuery] string? country)
+ /// <summary>
+ /// Adds a listings provider.
+ /// </summary>
+ /// <param name="pw">Password.</param>
+ /// <param name="listingsProviderInfo">New listings info.</param>
+ /// <param name="validateListings">Validate listings.</param>
+ /// <param name="validateLogin">Validate login.</param>
+ /// <response code="200">Created listings provider returned.</response>
+ /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
+ [HttpPost("ListingProviders")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
+ [FromQuery] string? pw,
+ [FromBody] ListingsProviderInfo listingsProviderInfo,
+ [FromQuery] bool validateListings = false,
+ [FromQuery] bool validateLogin = false)
+ {
+ if (!string.IsNullOrEmpty(pw))
{
- return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false);
+ // TODO: remove ToLower when Convert.ToHexString supports lowercase
+ // Schedules Direct requires the hex to be lowercase
+ listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
}
- /// <summary>
- /// Gets available countries.
- /// </summary>
- /// <response code="200">Available countries returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
- [HttpGet("ListingProviders/SchedulesDirect/Countries")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesFile(MediaTypeNames.Application.Json)]
- public async Task<ActionResult> GetSchedulesDirectCountries()
- {
- var client = _httpClientFactory.CreateClient(NamedClient.Default);
- // https://json.schedulesdirect.org/20141201/available/countries
- // Can't dispose the response as it's required up the call chain.
- var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries"))
- .ConfigureAwait(false);
+ return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
+ }
- return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
- }
+ /// <summary>
+ /// Delete listing provider.
+ /// </summary>
+ /// <param name="id">Listing provider id.</param>
+ /// <response code="204">Listing provider deleted.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("ListingProviders")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult DeleteListingProvider([FromQuery] string? id)
+ {
+ _liveTvManager.DeleteListingsProvider(id);
+ return NoContent();
+ }
- /// <summary>
- /// Get channel mapping options.
- /// </summary>
- /// <param name="providerId">Provider id.</param>
- /// <response code="200">Channel mapping options returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
- [HttpGet("ChannelMappingOptions")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
- {
- var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
+ /// <summary>
+ /// Gets available lineups.
+ /// </summary>
+ /// <param name="id">Provider id.</param>
+ /// <param name="type">Provider type.</param>
+ /// <param name="location">Location.</param>
+ /// <param name="country">Country.</param>
+ /// <response code="200">Available lineups returned.</response>
+ /// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
+ [HttpGet("ListingProviders/Lineups")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
+ [FromQuery] string? id,
+ [FromQuery] string? type,
+ [FromQuery] string? location,
+ [FromQuery] string? country)
+ {
+ return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false);
+ }
- var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
+ /// <summary>
+ /// Gets available countries.
+ /// </summary>
+ /// <response code="200">Available countries returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
+ [HttpGet("ListingProviders/SchedulesDirect/Countries")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile(MediaTypeNames.Application.Json)]
+ public async Task<ActionResult> GetSchedulesDirectCountries()
+ {
+ var client = _httpClientFactory.CreateClient(NamedClient.Default);
+ // https://json.schedulesdirect.org/20141201/available/countries
+ // Can't dispose the response as it's required up the call chain.
+ var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries"))
+ .ConfigureAwait(false);
- var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
+ return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
+ }
- var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None)
- .ConfigureAwait(false);
+ /// <summary>
+ /// Get channel mapping options.
+ /// </summary>
+ /// <param name="providerId">Provider id.</param>
+ /// <response code="200">Channel mapping options returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
+ [HttpGet("ChannelMappingOptions")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
+ {
+ var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
- var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
- .ConfigureAwait(false);
+ var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
- var mappings = listingsProviderInfo.ChannelMappings;
+ var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
- return new ChannelMappingOptionsDto
- {
- TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
- ProviderChannels = providerChannels.Select(i => new NameIdPair
- {
- Name = i.Name,
- Id = i.Id
- }).ToList(),
- Mappings = mappings,
- ProviderName = listingsProviderName
- };
- }
+ var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None)
+ .ConfigureAwait(false);
- /// <summary>
- /// Set channel mappings.
- /// </summary>
- /// <param name="setChannelMappingDto">The set channel mapping dto.</param>
- /// <response code="200">Created channel mapping returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
- [HttpPost("ChannelMappings")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
- {
- return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
- }
+ var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
+ .ConfigureAwait(false);
- /// <summary>
- /// Get tuner host types.
- /// </summary>
- /// <response code="200">Tuner host types returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
- [HttpGet("TunerHosts/Types")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
- {
- return _liveTvManager.GetTunerHostTypes();
- }
+ var mappings = listingsProviderInfo.ChannelMappings;
- /// <summary>
- /// Discover tuners.
- /// </summary>
- /// <param name="newDevicesOnly">Only discover new tuners.</param>
- /// <response code="200">Tuners returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
- [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
- [HttpGet("Tuners/Discover")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
+ return new ChannelMappingOptionsDto
{
- return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
- }
+ TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
+ ProviderChannels = providerChannels.Select(i => new NameIdPair
+ {
+ Name = i.Name,
+ Id = i.Id
+ }).ToList(),
+ Mappings = mappings,
+ ProviderName = listingsProviderName
+ };
+ }
- /// <summary>
- /// Gets a live tv recording stream.
- /// </summary>
- /// <param name="recordingId">Recording id.</param>
- /// <response code="200">Recording stream returned.</response>
- /// <response code="404">Recording not found.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the recording stream on success,
- /// or a <see cref="NotFoundResult"/> if recording not found.
- /// </returns>
- [HttpGet("LiveRecordings/{recordingId}/stream")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesVideoFile]
- public ActionResult GetLiveRecordingFile([FromRoute, Required] string recordingId)
- {
- var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
+ /// <summary>
+ /// Set channel mappings.
+ /// </summary>
+ /// <param name="setChannelMappingDto">The set channel mapping dto.</param>
+ /// <response code="200">Created channel mapping returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
+ [HttpPost("ChannelMappings")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
+ {
+ return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
+ }
- if (string.IsNullOrWhiteSpace(path))
- {
- return NotFound();
- }
+ /// <summary>
+ /// Get tuner host types.
+ /// </summary>
+ /// <response code="200">Tuner host types returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
+ [HttpGet("TunerHosts/Types")]
+ [Authorize(Policy = Policies.LiveTvAccess)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
+ {
+ return _liveTvManager.GetTunerHostTypes();
+ }
- var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper);
- return new FileStreamResult(stream, MimeTypes.GetMimeType(path));
- }
+ /// <summary>
+ /// Discover tuners.
+ /// </summary>
+ /// <param name="newDevicesOnly">Only discover new tuners.</param>
+ /// <response code="200">Tuners returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
+ [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
+ [HttpGet("Tuners/Discover")]
+ [Authorize(Policy = Policies.LiveTvManagement)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
+ {
+ return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets a live tv channel stream.
- /// </summary>
- /// <param name="streamId">Stream id.</param>
- /// <param name="container">Container type.</param>
- /// <response code="200">Stream returned.</response>
- /// <response code="404">Stream not found.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the channel stream on success,
- /// or a <see cref="NotFoundResult"/> if stream not found.
- /// </returns>
- [HttpGet("LiveStreamFiles/{streamId}/stream.{container}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesVideoFile]
- public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
- {
- var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
- if (liveStreamInfo is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Gets a live tv recording stream.
+ /// </summary>
+ /// <param name="recordingId">Recording id.</param>
+ /// <response code="200">Recording stream returned.</response>
+ /// <response code="404">Recording not found.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the recording stream on success,
+ /// or a <see cref="NotFoundResult"/> if recording not found.
+ /// </returns>
+ [HttpGet("LiveRecordings/{recordingId}/stream")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesVideoFile]
+ public ActionResult GetLiveRecordingFile([FromRoute, Required] string recordingId)
+ {
+ var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
- var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
- return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return NotFound();
}
- private async Task AssertUserCanManageLiveTv()
- {
- var user = _userManager.GetUserById(User.GetUserId());
- var session = await _sessionManager.LogSessionActivity(
- User.GetClient(),
- User.GetVersion(),
- User.GetDeviceId(),
- User.GetDevice(),
- HttpContext.GetNormalizedRemoteIp().ToString(),
- user).ConfigureAwait(false);
-
- if (session.UserId.Equals(default))
- {
- throw new SecurityException("Anonymous live tv management is not allowed.");
- }
+ var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper);
+ return new FileStreamResult(stream, MimeTypes.GetMimeType(path));
+ }
- if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
- {
- throw new SecurityException("The current user does not have permission to manage live tv.");
- }
+ /// <summary>
+ /// Gets a live tv channel stream.
+ /// </summary>
+ /// <param name="streamId">Stream id.</param>
+ /// <param name="container">Container type.</param>
+ /// <response code="200">Stream returned.</response>
+ /// <response code="404">Stream not found.</response>
+ /// <returns>
+ /// An <see cref="OkResult"/> containing the channel stream on success,
+ /// or a <see cref="NotFoundResult"/> if stream not found.
+ /// </returns>
+ [HttpGet("LiveStreamFiles/{streamId}/stream.{container}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesVideoFile]
+ public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
+ {
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
+ if (liveStreamInfo is null)
+ {
+ return NotFound();
}
+
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
+ return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
}
}
diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs
index 3d8b9e0ca..b9772a069 100644
--- a/Jellyfin.Api/Controllers/LocalizationController.cs
+++ b/Jellyfin.Api/Controllers/LocalizationController.cs
@@ -6,71 +6,70 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Localization controller.
+/// </summary>
+[Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
+public class LocalizationController : BaseJellyfinApiController
{
+ private readonly ILocalizationManager _localization;
+
/// <summary>
- /// Localization controller.
+ /// Initializes a new instance of the <see cref="LocalizationController"/> class.
/// </summary>
- [Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
- public class LocalizationController : BaseJellyfinApiController
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public LocalizationController(ILocalizationManager localization)
{
- private readonly ILocalizationManager _localization;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="LocalizationController"/> class.
- /// </summary>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- public LocalizationController(ILocalizationManager localization)
- {
- _localization = localization;
- }
+ _localization = localization;
+ }
- /// <summary>
- /// Gets known cultures.
- /// </summary>
- /// <response code="200">Known cultures returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns>
- [HttpGet("Cultures")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<CultureDto>> GetCultures()
- {
- return Ok(_localization.GetCultures());
- }
+ /// <summary>
+ /// Gets known cultures.
+ /// </summary>
+ /// <response code="200">Known cultures returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns>
+ [HttpGet("Cultures")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<CultureDto>> GetCultures()
+ {
+ return Ok(_localization.GetCultures());
+ }
- /// <summary>
- /// Gets known countries.
- /// </summary>
- /// <response code="200">Known countries returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the list of countries.</returns>
- [HttpGet("Countries")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<CountryInfo>> GetCountries()
- {
- return Ok(_localization.GetCountries());
- }
+ /// <summary>
+ /// Gets known countries.
+ /// </summary>
+ /// <response code="200">Known countries returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of countries.</returns>
+ [HttpGet("Countries")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<CountryInfo>> GetCountries()
+ {
+ return Ok(_localization.GetCountries());
+ }
- /// <summary>
- /// Gets known parental ratings.
- /// </summary>
- /// <response code="200">Known parental ratings returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns>
- [HttpGet("ParentalRatings")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings()
- {
- return Ok(_localization.GetParentalRatings());
- }
+ /// <summary>
+ /// Gets known parental ratings.
+ /// </summary>
+ /// <response code="200">Known parental ratings returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns>
+ [HttpGet("ParentalRatings")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings()
+ {
+ return Ok(_localization.GetParentalRatings());
+ }
- /// <summary>
- /// Gets localization options.
- /// </summary>
- /// <response code="200">Localization options returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the list of localization options.</returns>
- [HttpGet("Options")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<LocalizationOption>> GetLocalizationOptions()
- {
- return Ok(_localization.GetLocalizationOptions());
- }
+ /// <summary>
+ /// Gets localization options.
+ /// </summary>
+ /// <response code="200">Localization options returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of localization options.</returns>
+ [HttpGet("Options")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<LocalizationOption>> GetLocalizationOptions()
+ {
+ return Ok(_localization.GetLocalizationOptions());
}
}
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 8115c3585..ea10dd771 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -5,7 +5,6 @@ using System.Linq;
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos;
@@ -19,295 +18,294 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The media info controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class MediaInfoController : BaseJellyfinApiController
{
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IDeviceManager _deviceManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<MediaInfoController> _logger;
+ private readonly MediaInfoHelper _mediaInfoHelper;
+
/// <summary>
- /// The media info controller.
+ /// Initializes a new instance of the <see cref="MediaInfoController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class MediaInfoController : BaseJellyfinApiController
+ /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
+ /// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
+ public MediaInfoController(
+ IMediaSourceManager mediaSourceManager,
+ IDeviceManager deviceManager,
+ ILibraryManager libraryManager,
+ ILogger<MediaInfoController> logger,
+ MediaInfoHelper mediaInfoHelper)
{
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IDeviceManager _deviceManager;
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<MediaInfoController> _logger;
- private readonly MediaInfoHelper _mediaInfoHelper;
+ _mediaSourceManager = mediaSourceManager;
+ _deviceManager = deviceManager;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _mediaInfoHelper = mediaInfoHelper;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="MediaInfoController"/> class.
- /// </summary>
- /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
- /// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
- public MediaInfoController(
- IMediaSourceManager mediaSourceManager,
- IDeviceManager deviceManager,
- ILibraryManager libraryManager,
- ILogger<MediaInfoController> logger,
- MediaInfoHelper mediaInfoHelper)
- {
- _mediaSourceManager = mediaSourceManager;
- _deviceManager = deviceManager;
- _libraryManager = libraryManager;
- _logger = logger;
- _mediaInfoHelper = mediaInfoHelper;
- }
+ /// <summary>
+ /// Gets live playback media info for an item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <response code="200">Playback info returned.</response>
+ /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
+ [HttpGet("Items/{itemId}/PlaybackInfo")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId)
+ {
+ return await _mediaInfoHelper.GetPlaybackInfo(
+ itemId,
+ userId)
+ .ConfigureAwait(false);
+ }
- /// <summary>
- /// Gets live playback media info for an item.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">The user id.</param>
- /// <response code="200">Playback info returned.</response>
- /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
- [HttpGet("Items/{itemId}/PlaybackInfo")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId)
- {
- return await _mediaInfoHelper.GetPlaybackInfo(
- itemId,
- userId)
- .ConfigureAwait(false);
- }
+ /// <summary>
+ /// Gets live playback media info for an item.
+ /// </summary>
+ /// <remarks>
+ /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
+ /// Query parameters are obsolete.
+ /// </remarks>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
+ /// <param name="startTimeTicks">The start time in ticks.</param>
+ /// <param name="audioStreamIndex">The audio stream index.</param>
+ /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
+ /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
+ /// <param name="mediaSourceId">The media source id.</param>
+ /// <param name="liveStreamId">The livestream id.</param>
+ /// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param>
+ /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
+ /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
+ /// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param>
+ /// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param>
+ /// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
+ /// <param name="playbackInfoDto">The playback info.</param>
+ /// <response code="200">Playback info returned.</response>
+ /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
+ [HttpPost("Items/{itemId}/PlaybackInfo")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery, ParameterObsolete] Guid? userId,
+ [FromQuery, ParameterObsolete] int? maxStreamingBitrate,
+ [FromQuery, ParameterObsolete] long? startTimeTicks,
+ [FromQuery, ParameterObsolete] int? audioStreamIndex,
+ [FromQuery, ParameterObsolete] int? subtitleStreamIndex,
+ [FromQuery, ParameterObsolete] int? maxAudioChannels,
+ [FromQuery, ParameterObsolete] string? mediaSourceId,
+ [FromQuery, ParameterObsolete] string? liveStreamId,
+ [FromQuery, ParameterObsolete] bool? autoOpenLiveStream,
+ [FromQuery, ParameterObsolete] bool? enableDirectPlay,
+ [FromQuery, ParameterObsolete] bool? enableDirectStream,
+ [FromQuery, ParameterObsolete] bool? enableTranscoding,
+ [FromQuery, ParameterObsolete] bool? allowVideoStreamCopy,
+ [FromQuery, ParameterObsolete] bool? allowAudioStreamCopy,
+ [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
+ {
+ var profile = playbackInfoDto?.DeviceProfile;
+ _logger.LogDebug("GetPostedPlaybackInfo profile: {@Profile}", profile);
- /// <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)
+ if (profile is null)
{
- 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)
{
- var caps = _deviceManager.GetCapabilities(User.GetDeviceId());
- if (caps is not null)
- {
- profile = caps.DeviceProfile;
- }
+ 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;
+ // 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);
+ var info = await _mediaInfoHelper.GetPlaybackInfo(
+ itemId,
+ userId,
+ mediaSourceId,
+ liveStreamId)
+ .ConfigureAwait(false);
- if (info.ErrorCode is not null)
- {
- return info;
- }
+ if (info.ErrorCode is not null)
+ {
+ return info;
+ }
+
+ if (profile is not null)
+ {
+ // set device specific data
+ var item = _libraryManager.GetItemById(itemId);
- if (profile is not null)
+ foreach (var mediaSource in info.MediaSources)
{
- // set device specific data
- var item = _libraryManager.GetItemById(itemId);
+ _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());
+ }
- 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);
+ }
- _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 (autoOpenLiveStream.Value)
+ if (mediaSource is not null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
{
- 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);
+ 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 };
- }
+ info.MediaSources = new[] { openStreamResult.MediaSource };
}
-
- return info;
}
- /// <summary>
- /// Opens a media source.
- /// </summary>
- /// <param name="openToken">The open token.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="playSessionId">The play session id.</param>
- /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
- /// <param name="startTimeTicks">The start time in ticks.</param>
- /// <param name="audioStreamIndex">The audio stream index.</param>
- /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
- /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
- /// <param name="itemId">The item id.</param>
- /// <param name="openLiveStreamDto">The open live stream dto.</param>
- /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
- /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
- /// <response code="200">Media source opened.</response>
- /// <returns>A <see cref="Task"/> containing a <see cref="LiveStreamResponse"/>.</returns>
- [HttpPost("LiveStreams/Open")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<LiveStreamResponse>> OpenLiveStream(
- [FromQuery] string? openToken,
- [FromQuery] Guid? userId,
- [FromQuery] string? playSessionId,
- [FromQuery] int? maxStreamingBitrate,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] Guid? itemId,
- [FromBody] OpenLiveStreamDto? openLiveStreamDto,
- [FromQuery] bool? enableDirectPlay,
- [FromQuery] bool? enableDirectStream)
+ return info;
+ }
+
+ /// <summary>
+ /// Opens a media source.
+ /// </summary>
+ /// <param name="openToken">The open token.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
+ /// <param name="startTimeTicks">The start time in ticks.</param>
+ /// <param name="audioStreamIndex">The audio stream index.</param>
+ /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
+ /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="openLiveStreamDto">The open live stream dto.</param>
+ /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
+ /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
+ /// <response code="200">Media source opened.</response>
+ /// <returns>A <see cref="Task"/> containing a <see cref="LiveStreamResponse"/>.</returns>
+ [HttpPost("LiveStreams/Open")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<LiveStreamResponse>> OpenLiveStream(
+ [FromQuery] string? openToken,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] Guid? itemId,
+ [FromBody] OpenLiveStreamDto? openLiveStreamDto,
+ [FromQuery] bool? enableDirectPlay,
+ [FromQuery] bool? enableDirectStream)
+ {
+ var request = new LiveStreamRequest
{
- var request = new LiveStreamRequest
- {
- OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
- UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
- PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
- MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
- StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
- AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex,
- SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex,
- MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels,
- ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty,
- DeviceProfile = openLiveStreamDto?.DeviceProfile,
- EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true,
- EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
- DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
- };
- return await _mediaInfoHelper.OpenMediaSource(HttpContext, request).ConfigureAwait(false);
- }
+ OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
+ UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
+ PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
+ MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
+ StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
+ AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex,
+ SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex,
+ MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels,
+ ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty,
+ DeviceProfile = openLiveStreamDto?.DeviceProfile,
+ EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true,
+ EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
+ DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
+ };
+ return await _mediaInfoHelper.OpenMediaSource(HttpContext, request).ConfigureAwait(false);
+ }
- /// <summary>
- /// Closes a media source.
- /// </summary>
- /// <param name="liveStreamId">The livestream id.</param>
- /// <response code="204">Livestream closed.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("LiveStreams/Close")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CloseLiveStream([FromQuery, Required] string liveStreamId)
+ /// <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
{
- await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
- return NoContent();
+ Random.Shared.NextBytes(buffer);
+ return File(buffer, MediaTypeNames.Application.Octet);
}
-
- /// <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)
+ finally
{
- byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
- try
- {
- Random.Shared.NextBytes(buffer);
- return File(buffer, MediaTypeNames.Application.Octet);
- }
- finally
- {
- ArrayPool<byte>.Shared.Return(buffer);
- }
+ ArrayPool<byte>.Shared.Return(buffer);
}
}
}
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 3cf079362..a9336f6d2 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
@@ -18,122 +17,122 @@ using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Movies controller.
+/// </summary>
+[Authorize]
+public class MoviesController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
/// <summary>
- /// Movies controller.
+ /// Initializes a new instance of the <see cref="MoviesController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class MoviesController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public MoviesController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IServerConfigurationManager serverConfigurationManager)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MoviesController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public MoviesController(
- IUserManager userManager,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IServerConfigurationManager serverConfigurationManager)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _serverConfigurationManager = serverConfigurationManager;
- }
-
- /// <summary>
- /// Gets movie recommendations.
- /// </summary>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. The fields to return.</param>
- /// <param name="categoryLimit">The max number of categories to return.</param>
- /// <param name="itemLimit">The max number of items to return per category.</param>
- /// <response code="200">Movie recommendations returned.</response>
- /// <returns>The list of movie recommendations.</returns>
- [HttpGet("Recommendations")]
- public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
- [FromQuery] Guid? userId,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] int categoryLimit = 5,
- [FromQuery] int itemLimit = 8)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User);
-
- var categories = new List<RecommendationDto>();
-
- var parentIdGuid = parentId ?? Guid.Empty;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- var query = new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[]
- {
- BaseItemKind.Movie,
- // nameof(Trailer),
- // nameof(LiveTvProgram)
- },
- // IsMovie = true
- OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) },
- Limit = 7,
- ParentId = parentIdGuid,
- Recursive = true,
- IsPlayed = true,
- DtoOptions = dtoOptions
- };
+ /// <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 recentlyPlayedMovies = _libraryManager.GetItemList(query);
+ var categories = new List<RecommendationDto>();
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
- }
+ var parentIdGuid = parentId ?? Guid.Empty;
- var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ var query = new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[]
{
- 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();
+ BaseItemKind.Movie,
+ // nameof(Trailer),
+ // nameof(LiveTvProgram)
+ },
+ // IsMovie = true
+ OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) },
+ Limit = 7,
+ ParentId = parentIdGuid,
+ Recursive = true,
+ IsPlayed = true,
+ DtoOptions = dtoOptions
+ };
+
+ var recentlyPlayedMovies = _libraryManager.GetItemList(query);
+
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
- var categoryTypes = new List<IEnumerator<RecommendationDto>>
+ 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
similarToRecentlyPlayed,
@@ -146,181 +145,180 @@ namespace Jellyfin.Api.Controllers
hasActorFromRecentlyPlayed
};
- while (categories.Count < categoryLimit)
- {
- var allEmpty = true;
+ while (categories.Count < categoryLimit)
+ {
+ var allEmpty = true;
- foreach (var category in categoryTypes)
+ foreach (var category in categoryTypes)
+ {
+ if (category.MoveNext())
{
- if (category.MoveNext())
- {
- categories.Add(category.Current);
- allEmpty = false;
+ categories.Add(category.Current);
+ allEmpty = false;
- if (categories.Count >= categoryLimit)
- {
- break;
- }
+ if (categories.Count >= categoryLimit)
+ {
+ break;
}
}
-
- if (allEmpty)
- {
- break;
- }
}
- return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable());
- }
-
- private IEnumerable<RecommendationDto> GetWithDirector(
- User? user,
- IEnumerable<string> names,
- int itemLimit,
- DtoOptions dtoOptions,
- RecommendationType type)
- {
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ if (allEmpty)
{
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
+ break;
}
+ }
- 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);
+ return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable());
+ }
- yield return new RecommendationDto
- {
- BaselineItemName = name,
- CategoryId = name.GetMD5(),
- RecommendationType = type,
- Items = returnItems
- };
- }
- }
+ private IEnumerable<RecommendationDto> GetWithDirector(
+ User? user,
+ IEnumerable<string> names,
+ int itemLimit,
+ DtoOptions dtoOptions,
+ RecommendationType type)
+ {
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
}
- private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+ foreach (var name in names)
{
- 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)
+ var items = _libraryManager.GetItemList(
+ new InternalItemsQuery(user)
{
Person = name,
// Account for duplicates by IMDb id, since the database doesn't support this yet
Limit = itemLimit + 2,
+ PersonTypes = new[] { PersonType.Director },
IncludeItemTypes = itemTypes.ToArray(),
IsMovie = true,
EnableGroupByMetadataKey = true,
DtoOptions = dtoOptions
}).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
- .Take(itemLimit)
- .ToList();
+ .Take(itemLimit)
+ .ToList();
- if (items.Count > 0)
- {
- var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
+ if (items.Count > 0)
+ {
+ var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
- yield return new RecommendationDto
- {
- BaselineItemName = name,
- CategoryId = name.GetMD5(),
- RecommendationType = type,
- Items = returnItems
- };
- }
+ 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)
+ {
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
- private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
+ foreach (var name in names)
{
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
- }
+ Person = name,
+ // Account for duplicates by IMDb id, since the database doesn't support this yet
+ Limit = itemLimit + 2,
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
+ }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
+ .Take(itemLimit)
+ .ToList();
- foreach (var item in baselineItems)
+ if (items.Count > 0)
{
- var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- Limit = itemLimit,
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- SimilarTo = item,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- });
+ var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
- if (similar.Count > 0)
+ yield return new RecommendationDto
{
- var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
-
- yield return new RecommendationDto
- {
- BaselineItemName = item.Name,
- CategoryId = item.Id,
- RecommendationType = type,
- Items = returnItems
- };
- }
+ BaselineItemName = name,
+ CategoryId = name.GetMD5(),
+ RecommendationType = type,
+ Items = returnItems
+ };
}
}
+ }
- private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
+ 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)
{
- var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director })
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
+
+ foreach (var item in baselineItems)
+ {
+ var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
- MaxListOrder = 3
+ Limit = itemLimit,
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ SimilarTo = item,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
});
- var itemIds = items.Select(i => i.Id).ToList();
+ if (similar.Count > 0)
+ {
+ var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
- return people
- .Where(i => itemIds.Contains(i.ItemId))
- .Select(i => i.Name)
- .DistinctNames();
+ yield return new RecommendationDto
+ {
+ BaselineItemName = item.Name,
+ CategoryId = item.Id,
+ RecommendationType = type,
+ Items = returnItems
+ };
+ }
}
+ }
- private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
+ private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
+ {
+ var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director })
{
- var people = _libraryManager.GetPeople(new InternalPeopleQuery(
- new[] { PersonType.Director },
- Array.Empty<string>()));
+ MaxListOrder = 3
+ });
- var itemIds = items.Select(i => i.Id).ToList();
+ var itemIds = items.Select(i => i.Id).ToList();
- return people
- .Where(i => itemIds.Contains(i.ItemId))
- .Select(i => i.Name)
- .DistinctNames();
- }
+ 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();
}
}
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index f4fb5f44a..3db1d89c1 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -18,181 +17,185 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The music genres controller.
+/// </summary>
+[Authorize]
+public class MusicGenresController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// The music genres controller.
+ /// Initializes a new instance of the <see cref="MusicGenresController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class MusicGenresController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+ public MusicGenresController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="MusicGenresController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
- public MusicGenresController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- }
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ }
- /// <summary>
- /// Gets all music genres from a given item, folder, or the entire library.
- /// </summary>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="searchTerm">The search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="userId">User id.</param>
- /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
- /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
- /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
- /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
- /// <param name="enableImages">Optional, include image information in output.</param>
- /// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
- /// <response code="200">Music genres returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns>
- [HttpGet]
- [Obsolete("Use GetGenres instead")]
- public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery] bool? isFavorite,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] Guid? userId,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery] bool? enableImages = true,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
+ /// <summary>
+ /// Gets all music genres from a given item, folder, or the entire library.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="searchTerm">The search term.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+ /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <param name="userId">User id.</param>
+ /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+ /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+ /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
+ /// <response code="200">Music genres returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns>
+ [HttpGet]
+ [Obsolete("Use GetGenres instead")]
+ public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ User? user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- var parentItem = _libraryManager.GetParentItem(parentId, userId);
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
- var query = new InternalItemsQuery(user)
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
+ };
+
+ if (parentId.HasValue)
+ {
+ if (parentItem is Folder)
{
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount,
- OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
- };
-
- if (parentId.HasValue)
+ query.AncestorIds = new[] { parentId.Value };
+ }
+ else
{
- if (parentItem is Folder)
- {
- query.AncestorIds = new[] { parentId.Value };
- }
- else
- {
- query.ItemIds = new[] { parentId.Value };
- }
+ query.ItemIds = new[] { parentId.Value };
}
+ }
- var result = _libraryManager.GetMusicGenres(query);
+ var result = _libraryManager.GetMusicGenres(query);
- var shouldIncludeItemTypes = includeItemTypes.Length != 0;
- return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
- }
+ 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);
+ /// <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;
+ MusicGenre? item;
- if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
- {
- item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre);
- }
- else
- {
- item = _libraryManager.GetMusicGenre(genreName);
- }
+ if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre);
+ }
+ else
+ {
+ item = _libraryManager.GetMusicGenre(genreName);
+ }
- if (userId.HasValue && !userId.Value.Equals(default))
- {
- var user = _userManager.GetUserById(userId.Value);
+ if (item is null)
+ {
+ return NotFound();
+ }
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ if (userId.HasValue && !userId.Value.Equals(default))
+ {
+ var user = _userManager.GetUserById(userId.Value);
- return _dtoService.GetBaseItemDto(item, dtoOptions);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
- private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
- where T : BaseItem, new()
+ 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
{
- var result = libraryManager.GetItemList(new InternalItemsQuery
- {
- Name = name.Replace(BaseItem.SlugChar, '&'),
- IncludeItemTypes = new[] { baseItemKind },
- DtoOptions = dtoOptions
- }).OfType<T>().FirstOrDefault();
+ 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();
- 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;
- }
+ return result;
}
}
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
deleted file mode 100644
index a28556476..000000000
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System.Collections.Generic;
-using Jellyfin.Api.Constants;
-using MediaBrowser.Controller.Notifications;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Notifications;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Jellyfin.Api.Controllers
-{
- /// <summary>
- /// The notification controller.
- /// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class NotificationsController : BaseJellyfinApiController
- {
- private readonly INotificationManager _notificationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="NotificationsController" /> class.
- /// </summary>
- /// <param name="notificationManager">The notification manager.</param>
- public NotificationsController(INotificationManager notificationManager)
- {
- _notificationManager = notificationManager;
- }
-
- /// <summary>
- /// Gets notification types.
- /// </summary>
- /// <response code="200">All notification types returned.</response>
- /// <returns>An <cref see="OkResult"/> containing a list of all notification types.</returns>
- [HttpGet("Types")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
- {
- return _notificationManager.GetNotificationTypes();
- }
-
- /// <summary>
- /// Gets notification services.
- /// </summary>
- /// <response code="200">All notification services returned.</response>
- /// <returns>An <cref see="OkResult"/> containing a list of all notification services.</returns>
- [HttpGet("Services")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable<NameIdPair> GetNotificationServices()
- {
- return _notificationManager.GetNotificationServices();
- }
- }
-}
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 10f967dcd..0ba5e995f 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -11,157 +11,156 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Package Controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class PackageController : BaseJellyfinApiController
{
+ private readonly IInstallationManager _installationManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
/// <summary>
- /// Package Controller.
+ /// Initializes a new instance of the <see cref="PackageController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class PackageController : BaseJellyfinApiController
+ /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager)
{
- private readonly IInstallationManager _installationManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PackageController"/> class.
- /// </summary>
- /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager)
- {
- _installationManager = installationManager;
- _serverConfigurationManager = serverConfigurationManager;
- }
+ _installationManager = installationManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- /// <summary>
- /// Gets a package by name or assembly GUID.
- /// </summary>
- /// <param name="name">The name of the package.</param>
- /// <param name="assemblyGuid">The GUID of the associated assembly.</param>
- /// <response code="200">Package retrieved.</response>
- /// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
- [HttpGet("Packages/{name}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<PackageInfo>> GetPackageInfo(
- [FromRoute, Required] string name,
- [FromQuery] Guid? assemblyGuid)
+ /// <summary>
+ /// Gets a package by name or assembly GUID.
+ /// </summary>
+ /// <param name="name">The name of the package.</param>
+ /// <param name="assemblyGuid">The GUID of the associated assembly.</param>
+ /// <response code="200">Package retrieved.</response>
+ /// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
+ [HttpGet("Packages/{name}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<PackageInfo>> GetPackageInfo(
+ [FromRoute, Required] string name,
+ [FromQuery] Guid? assemblyGuid)
+ {
+ var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
+ var result = _installationManager.FilterPackages(
+ packages,
+ name,
+ assemblyGuid ?? default)
+ .FirstOrDefault();
+
+ if (result is null)
{
- var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
- var result = _installationManager.FilterPackages(
- packages,
- name,
- assemblyGuid ?? default)
- .FirstOrDefault();
-
- if (result is null)
- {
- return NotFound();
- }
-
- return result;
+ return NotFound();
}
- /// <summary>
- /// Gets available packages.
- /// </summary>
- /// <response code="200">Available packages returned.</response>
- /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
- [HttpGet("Packages")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<IEnumerable<PackageInfo>> GetPackages()
- {
- IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
+ return result;
+ }
- return packages;
- }
+ /// <summary>
+ /// Gets available packages.
+ /// </summary>
+ /// <response code="200">Available packages returned.</response>
+ /// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
+ [HttpGet("Packages")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<IEnumerable<PackageInfo>> GetPackages()
+ {
+ IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
- /// <summary>
- /// Installs a package.
- /// </summary>
- /// <param name="name">Package name.</param>
- /// <param name="assemblyGuid">GUID of the associated assembly.</param>
- /// <param name="version">Optional version. Defaults to latest version.</param>
- /// <param name="repositoryUrl">Optional. Specify the repository to install from.</param>
- /// <response code="204">Package found.</response>
- /// <response code="404">Package not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
- [HttpPost("Packages/Installed/{name}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [Authorize(Policy = Policies.RequiresElevation)]
- public async Task<ActionResult> InstallPackage(
- [FromRoute, Required] string name,
- [FromQuery] Guid? assemblyGuid,
- [FromQuery] string? version,
- [FromQuery] string? repositoryUrl)
- {
- var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
- if (!string.IsNullOrEmpty(repositoryUrl))
- {
- packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)))
- .ToList();
- }
-
- var package = _installationManager.GetCompatibleVersions(
- packages,
- name,
- assemblyGuid ?? Guid.Empty,
- specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version))
- .FirstOrDefault();
-
- if (package is null)
- {
- return NotFound();
- }
-
- await _installationManager.InstallPackage(package).ConfigureAwait(false);
-
- return NoContent();
- }
+ return packages;
+ }
- /// <summary>
- /// Cancels a package installation.
- /// </summary>
- /// <param name="packageId">Installation Id.</param>
- /// <response code="204">Installation cancelled.</response>
- /// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
- [HttpDelete("Packages/Installing/{packageId}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CancelPackageInstallation(
- [FromRoute, Required] Guid packageId)
+ /// <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))
{
- _installationManager.CancelInstallation(packageId);
- return NoContent();
+ packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)))
+ .ToList();
}
- /// <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());
- }
+ var package = _installationManager.GetCompatibleVersions(
+ packages,
+ name,
+ assemblyGuid ?? Guid.Empty,
+ specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version))
+ .FirstOrDefault();
- /// <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)
+ if (package is null)
{
- _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
- _serverConfigurationManager.SaveConfiguration();
- return NoContent();
+ return NotFound();
}
+
+ await _installationManager.InstallPackage(package).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Cancels a package installation.
+ /// </summary>
+ /// <param name="packageId">Installation Id.</param>
+ /// <response code="204">Installation cancelled.</response>
+ /// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
+ [HttpDelete("Packages/Installing/{packageId}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult CancelPackageInstallation(
+ [FromRoute, Required] Guid packageId)
+ {
+ _installationManager.CancelInstallation(packageId);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets all package repositories.
+ /// </summary>
+ /// <response code="200">Package repositories returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the list of package repositories.</returns>
+ [HttpGet("Repositories")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories()
+ {
+ return Ok(_serverConfigurationManager.Configuration.PluginRepositories.AsEnumerable());
+ }
+
+ /// <summary>
+ /// Sets the enabled and existing package repositories.
+ /// </summary>
+ /// <param name="repositoryInfos">The list of package repositories.</param>
+ /// <response code="204">Package repositories saved.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Repositories")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos)
+ {
+ _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
+ _serverConfigurationManager.SaveConfiguration();
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 09f7281ec..5310f50b1 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
@@ -15,125 +14,124 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Persons controller.
+/// </summary>
+[Authorize]
+public class PersonsController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// Persons controller.
+ /// Initializes a new instance of the <see cref="PersonsController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class PersonsController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public PersonsController(
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IUserManager userManager)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IUserManager _userManager;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _userManager = userManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="PersonsController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public PersonsController(
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IUserManager userManager)
- {
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _userManager = userManager;
- }
+ /// <summary>
+ /// Gets all persons.
+ /// </summary>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="searchTerm">The search term.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param>
+ /// <param name="enableUserData">Optional, include user data.</param>
+ /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param>
+ /// <param name="userId">User id.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <response code="200">Persons returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetPersons(
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery] Guid? appearsInItemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool? enableImages = true)
+ {
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- /// <summary>
- /// Gets all persons.
- /// </summary>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="searchTerm">The search term.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="filters">Optional. Specify additional filters to apply.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param>
- /// <param name="enableUserData">Optional, include user data.</param>
- /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param>
- /// <param name="userId">User id.</param>
- /// <param name="enableImages">Optional, include image information in output.</param>
- /// <response code="200">Persons returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetPersons(
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery] bool? isFavorite,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery] Guid? appearsInItemId,
- [FromQuery] Guid? userId,
- [FromQuery] bool? enableImages = true)
+ var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
+ var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery(
+ personTypes,
+ excludePersonTypes)
{
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ NameContains = searchTerm,
+ User = user,
+ IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
+ AppearsInItemId = appearsInItemId ?? Guid.Empty,
+ Limit = limit ?? 0
+ });
- User? user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ return new QueryResult<BaseItemDto>(
+ peopleItems
+ .Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user))
+ .ToArray());
+ }
- 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
- });
+ /// <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);
- return new QueryResult<BaseItemDto>(
- peopleItems
- .Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user))
- .ToArray());
+ var item = _libraryManager.GetPerson(name);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <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)
+ if (userId.HasValue && !userId.Value.Equals(default))
{
- 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);
+ var user = _userManager.GetUserById(userId.Value);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
}
}
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index e0c565da1..79c0d3c7b 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.PlaylistDtos;
@@ -20,202 +19,201 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Playlists controller.
+/// </summary>
+[Authorize]
+public class PlaylistsController : BaseJellyfinApiController
{
+ private readonly IPlaylistManager _playlistManager;
+ private readonly IDtoService _dtoService;
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+
/// <summary>
- /// Playlists controller.
+ /// Initializes a new instance of the <see cref="PlaylistsController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class PlaylistsController : BaseJellyfinApiController
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public PlaylistsController(
+ IDtoService dtoService,
+ IPlaylistManager playlistManager,
+ IUserManager userManager,
+ ILibraryManager libraryManager)
{
- private readonly IPlaylistManager _playlistManager;
- private readonly IDtoService _dtoService;
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PlaylistsController"/> class.
- /// </summary>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public PlaylistsController(
- IDtoService dtoService,
- IPlaylistManager playlistManager,
- IUserManager userManager,
- ILibraryManager libraryManager)
- {
- _dtoService = dtoService;
- _playlistManager = playlistManager;
- _userManager = userManager;
- _libraryManager = libraryManager;
- }
+ _dtoService = dtoService;
+ _playlistManager = playlistManager;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ }
- /// <summary>
- /// Creates a new playlist.
- /// </summary>
- /// <remarks>
- /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
- /// Query parameters are obsolete.
- /// </remarks>
- /// <param name="name">The playlist name.</param>
- /// <param name="ids">The item ids.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="mediaType">The media type.</param>
- /// <param name="createPlaylistRequest">The create playlist payload.</param>
- /// <returns>
- /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
- /// The task result contains an <see cref="OkResult"/> indicating success.
- /// </returns>
- [HttpPost]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
- [FromQuery, ParameterObsolete] string? name,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
- [FromQuery, ParameterObsolete] Guid? userId,
- [FromQuery, ParameterObsolete] string? mediaType,
- [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
+ /// <summary>
+ /// Creates a new playlist.
+ /// </summary>
+ /// <remarks>
+ /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
+ /// Query parameters are obsolete.
+ /// </remarks>
+ /// <param name="name">The playlist name.</param>
+ /// <param name="ids">The item ids.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="mediaType">The media type.</param>
+ /// <param name="createPlaylistRequest">The create playlist payload.</param>
+ /// <returns>
+ /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
+ /// The task result contains an <see cref="OkResult"/> indicating success.
+ /// </returns>
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
+ [FromQuery, ParameterObsolete] string? name,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
+ [FromQuery, ParameterObsolete] Guid? userId,
+ [FromQuery, ParameterObsolete] string? mediaType,
+ [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
+ {
+ if (ids.Count == 0)
{
- if (ids.Count == 0)
- {
- ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
- }
-
- var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
- {
- Name = name ?? createPlaylistRequest?.Name,
- ItemIdList = ids,
- UserId = userId ?? createPlaylistRequest?.UserId ?? default,
- MediaType = mediaType ?? createPlaylistRequest?.MediaType
- }).ConfigureAwait(false);
-
- return result;
+ ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
}
- /// <summary>
- /// Adds items to a playlist.
- /// </summary>
- /// <param name="playlistId">The playlist id.</param>
- /// <param name="ids">Item id, comma delimited.</param>
- /// <param name="userId">The userId.</param>
- /// <response code="204">Items added to playlist.</response>
- /// <returns>An <see cref="NoContentResult"/> on success.</returns>
- [HttpPost("{playlistId}/Items")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddToPlaylist(
- [FromRoute, Required] Guid playlistId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
- [FromQuery] Guid? userId)
+ var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{
- await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false);
- return NoContent();
- }
+ Name = name ?? createPlaylistRequest?.Name,
+ ItemIdList = ids,
+ UserId = userId ?? createPlaylistRequest?.UserId ?? default,
+ MediaType = mediaType ?? createPlaylistRequest?.MediaType
+ }).ConfigureAwait(false);
- /// <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();
- }
+ return result;
+ }
- /// <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>
+ /// 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>
- /// 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>
+ /// 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();
+ }
- var user = userId.Equals(default)
- ? null
- : _userManager.GetUserById(userId);
+ /// <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();
+ }
- var items = playlist.GetManageableItems().ToArray();
+ /// <summary>
+ /// Gets the original items of a playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <param name="userId">User id.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <response code="200">Original playlist returned.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>The original playlist items.</returns>
+ [HttpGet("{playlistId}/Items")]
+ public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
+ [FromRoute, Required] Guid playlistId,
+ [FromQuery, Required] Guid userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+ {
+ var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
+ if (playlist is null)
+ {
+ return NotFound();
+ }
- var count = items.Length;
+ var user = userId.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId);
- if (startIndex.HasValue)
- {
- items = items.Skip(startIndex.Value).ToArray();
- }
+ var items = playlist.GetManageableItems().ToArray();
- if (limit.HasValue)
- {
- items = items.Take(limit.Value).ToArray();
- }
+ var count = items.Length;
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ if (startIndex.HasValue)
+ {
+ items = items.Skip(startIndex.Value).ToArray();
+ }
- var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
+ if (limit.HasValue)
+ {
+ items = items.Take(limit.Value).ToArray();
+ }
- for (int index = 0; index < dtos.Count; index++)
- {
- dtos[index].PlaylistItemId = items[index].Item1.Id;
- }
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var result = new QueryResult<BaseItemDto>(
- startIndex,
- count,
- dtos);
+ var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
- return result;
+ 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;
}
}
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 58f9b7d35..8ad553bcb 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -2,11 +2,11 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
@@ -16,350 +16,385 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Playstate controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class PlaystateController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataRepository;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ISessionManager _sessionManager;
+ private readonly ILogger<PlaystateController> _logger;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+
/// <summary>
- /// Playstate controller.
+ /// Initializes a new instance of the <see cref="PlaystateController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class PlaystateController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ /// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param>
+ public PlaystateController(
+ IUserManager userManager,
+ IUserDataManager userDataRepository,
+ ILibraryManager libraryManager,
+ ISessionManager sessionManager,
+ ILoggerFactory loggerFactory,
+ TranscodingJobHelper transcodingJobHelper)
{
- private readonly IUserManager _userManager;
- private readonly IUserDataManager _userDataRepository;
- private readonly ILibraryManager _libraryManager;
- private readonly ISessionManager _sessionManager;
- private readonly ILogger<PlaystateController> _logger;
- private readonly TranscodingJobHelper _transcodingJobHelper;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PlaystateController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
- /// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param>
- public PlaystateController(
- IUserManager userManager,
- IUserDataManager userDataRepository,
- ILibraryManager libraryManager,
- ISessionManager sessionManager,
- ILoggerFactory loggerFactory,
- TranscodingJobHelper transcodingJobHelper)
- {
- _userManager = userManager;
- _userDataRepository = userDataRepository;
- _libraryManager = libraryManager;
- _sessionManager = sessionManager;
- _logger = loggerFactory.CreateLogger<PlaystateController>();
+ _userManager = userManager;
+ _userDataRepository = userDataRepository;
+ _libraryManager = libraryManager;
+ _sessionManager = sessionManager;
+ _logger = loggerFactory.CreateLogger<PlaystateController>();
- _transcodingJobHelper = transcodingJobHelper;
- }
+ _transcodingJobHelper = transcodingJobHelper;
+ }
- /// <summary>
- /// Marks an item as played for user.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <param name="datePlayed">Optional. The date the item was played.</param>
- /// <response code="200">Item marked as played.</response>
- /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
- [HttpPost("Users/{userId}/PlayedItems/{itemId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
- [FromRoute, Required] Guid userId,
- [FromRoute, Required] Guid itemId,
- [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
+ /// <summary>
+ /// Marks an item as played for user.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="datePlayed">Optional. The date the item was played.</param>
+ /// <response code="200">Item marked as played.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/> if item was not found.</returns>
+ [HttpPost("Users/{userId}/PlayedItems/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
+ [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
{
- var user = _userManager.GetUserById(userId);
- var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var dto = UpdatePlayedStatus(user, itemId, true, datePlayed);
- foreach (var additionalUserInfo in session.AdditionalUsers)
- {
- var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
- UpdatePlayedStatus(additionalUser, itemId, true, datePlayed);
- }
+ return NotFound();
+ }
+
+ var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- return dto;
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ return NotFound();
}
- /// <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>
- /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
- [HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ var dto = UpdatePlayedStatus(user, item, true, datePlayed);
+ foreach (var additionalUserInfo in session.AdditionalUsers)
{
- var user = _userManager.GetUserById(userId);
- var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var dto = UpdatePlayedStatus(user, itemId, false, null);
- foreach (var additionalUserInfo in session.AdditionalUsers)
+ var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
+ if (additionalUser is null)
{
- var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
- UpdatePlayedStatus(additionalUser, itemId, false, null);
+ return NotFound();
}
- return dto;
+ UpdatePlayedStatus(additionalUser, item, true, datePlayed);
}
- /// <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();
- }
+ return dto;
+ }
- /// <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)
+ /// <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);
+ if (user is null)
{
- 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();
+ return NotFound();
}
- /// <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)
+ var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var item = _libraryManager.GetItemById(itemId);
+
+ if (item is null)
{
- _transcodingJobHelper.PingTranscodingJob(playSessionId, null);
- return NoContent();
+ return NotFound();
}
- /// <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)
+ var dto = UpdatePlayedStatus(user, item, false, null);
+ foreach (var additionalUserInfo in session.AdditionalUsers)
{
- _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
- if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
+ var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
+ if (additionalUser is null)
{
- await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+ return NotFound();
}
- playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
- return NoContent();
+ UpdatePlayedStatus(additionalUser, item, false, null);
}
- /// <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)
+ 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))
{
- 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();
+ await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
- /// <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)
+ 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
{
- 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();
- }
+ 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 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)
+ /// <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
{
- 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);
- }
+ 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
+ };
- playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
- return NoContent();
- }
+ 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
+ };
- /// <summary>
- /// Updates the played status.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <param name="itemId">The item id.</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, Guid itemId, bool wasPlayed, DateTime? datePlayed)
+ _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
+ if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
- var item = _libraryManager.GetItemById(itemId);
+ await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
+ }
- if (wasPlayed)
- {
- item.MarkPlayed(user, datePlayed, true);
- }
- else
- {
- item.MarkUnplayed(user);
- }
+ playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
+ return NoContent();
+ }
- return _userDataRepository.GetUserDataDto(item, user);
+ /// <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);
}
- private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
+ return _userDataRepository.GetUserDataDto(item, user);
+ }
+
+ private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
+ {
+ if (method == PlayMethod.Transcode)
{
- if (method == PlayMethod.Transcode)
+ var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId);
+ if (job is null)
{
- var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId);
- if (job is null)
- {
- return PlayMethod.DirectPlay;
- }
+ return PlayMethod.DirectPlay;
}
-
- return method;
}
+
+ return method;
}
}
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index b8a09990a..4726cf066 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
@@ -17,250 +16,249 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Plugins controller.
+/// </summary>
+[Authorize]
+public class PluginsController : BaseJellyfinApiController
{
+ private readonly IInstallationManager _installationManager;
+ private readonly IPluginManager _pluginManager;
+ private readonly JsonSerializerOptions _serializerOptions;
+
/// <summary>
- /// Plugins controller.
+ /// Initializes a new instance of the <see cref="PluginsController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class PluginsController : BaseJellyfinApiController
+ /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
+ /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
+ public PluginsController(
+ IInstallationManager installationManager,
+ IPluginManager pluginManager)
{
- private readonly IInstallationManager _installationManager;
- private readonly IPluginManager _pluginManager;
- private readonly JsonSerializerOptions _serializerOptions;
+ _installationManager = installationManager;
+ _pluginManager = pluginManager;
+ _serializerOptions = JsonDefaults.Options;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="PluginsController"/> class.
- /// </summary>
- /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
- /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
- public PluginsController(
- IInstallationManager installationManager,
- IPluginManager pluginManager)
+ /// <summary>
+ /// Gets a list of currently installed plugins.
+ /// </summary>
+ /// <response code="200">Installed plugins returned.</response>
+ /// <returns>List of currently installed plugins.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
+ {
+ return Ok(_pluginManager.Plugins
+ .OrderBy(p => p.Name)
+ .Select(p => p.GetPluginInfo()));
+ }
+
+ /// <summary>
+ /// Enables a disabled plugin.
+ /// </summary>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <param name="version">Plugin version.</param>
+ /// <response code="204">Plugin enabled.</response>
+ /// <response code="404">Plugin not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
+ [HttpPost("{pluginId}/{version}/Enable")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId, version);
+ if (plugin is null)
{
- _installationManager = installationManager;
- _pluginManager = pluginManager;
- _serializerOptions = JsonDefaults.Options;
+ return NotFound();
}
- /// <summary>
- /// Gets a list of currently installed plugins.
- /// </summary>
- /// <response code="200">Installed plugins returned.</response>
- /// <returns>List of currently installed plugins.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
+ _pluginManager.EnablePlugin(plugin);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Disable a plugin.
+ /// </summary>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <param name="version">Plugin version.</param>
+ /// <response code="204">Plugin disabled.</response>
+ /// <response code="404">Plugin not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
+ [HttpPost("{pluginId}/{version}/Disable")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId, version);
+ if (plugin is null)
{
- return Ok(_pluginManager.Plugins
- .OrderBy(p => p.Name)
- .Select(p => p.GetPluginInfo()));
+ return NotFound();
}
- /// <summary>
- /// Enables a disabled plugin.
- /// </summary>
- /// <param name="pluginId">Plugin id.</param>
- /// <param name="version">Plugin version.</param>
- /// <response code="204">Plugin enabled.</response>
- /// <response code="404">Plugin not found.</response>
- /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
- [HttpPost("{pluginId}/{version}/Enable")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
- {
- var plugin = _pluginManager.GetPlugin(pluginId, version);
- if (plugin is null)
- {
- return NotFound();
- }
+ _pluginManager.DisablePlugin(plugin);
+ return NoContent();
+ }
- _pluginManager.EnablePlugin(plugin);
- return NoContent();
+ /// <summary>
+ /// Uninstalls a plugin by version.
+ /// </summary>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <param name="version">Plugin version.</param>
+ /// <response code="204">Plugin uninstalled.</response>
+ /// <response code="404">Plugin not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
+ [HttpDelete("{pluginId}/{version}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId, version);
+ if (plugin is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Disable a plugin.
- /// </summary>
- /// <param name="pluginId">Plugin id.</param>
- /// <param name="version">Plugin version.</param>
- /// <response code="204">Plugin disabled.</response>
- /// <response code="404">Plugin not found.</response>
- /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
- [HttpPost("{pluginId}/{version}/Disable")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
- {
- var plugin = _pluginManager.GetPlugin(pluginId, version);
- if (plugin is null)
- {
- return NotFound();
- }
+ _installationManager.UninstallPlugin(plugin);
+ return NoContent();
+ }
- _pluginManager.DisablePlugin(plugin);
- return NoContent();
- }
+ /// <summary>
+ /// Uninstalls a plugin.
+ /// </summary>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <response code="204">Plugin uninstalled.</response>
+ /// <response code="404">Plugin not found.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
+ [HttpDelete("{pluginId}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("Please use the UninstallPluginByVersion API.")]
+ public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
+ {
+ // If no version is given, return the current instance.
+ var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList();
- /// <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();
- }
+ // Select the un-instanced one first.
+ var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
+ if (plugin is not null)
+ {
_installationManager.UninstallPlugin(plugin);
return NoContent();
}
- /// <summary>
- /// Uninstalls a plugin.
- /// </summary>
- /// <param name="pluginId">Plugin id.</param>
- /// <response code="204">Plugin uninstalled.</response>
- /// <response code="404">Plugin not found.</response>
- /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
- [HttpDelete("{pluginId}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [Obsolete("Please use the UninstallPluginByVersion API.")]
- public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
- {
- // If no version is given, return the current instance.
- var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList();
-
- // Select the un-instanced one first.
- var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
-
- if (plugin is not null)
- {
- _installationManager.UninstallPlugin(plugin);
- return NoContent();
- }
+ return NotFound();
+ }
- return NotFound();
+ /// <summary>
+ /// Gets plugin configuration.
+ /// </summary>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <response code="200">Plugin configuration returned.</response>
+ /// <response code="404">Plugin not found or plugin configuration not found.</response>
+ /// <returns>Plugin configuration.</returns>
+ [HttpGet("{pluginId}/Configuration")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId);
+ if (plugin?.Instance is IHasPluginConfiguration configPlugin)
+ {
+ return configPlugin.Configuration;
}
- /// <summary>
- /// Gets plugin configuration.
- /// </summary>
- /// <param name="pluginId">Plugin id.</param>
- /// <response code="200">Plugin configuration returned.</response>
- /// <response code="404">Plugin not found or plugin configuration not found.</response>
- /// <returns>Plugin configuration.</returns>
- [HttpGet("{pluginId}/Configuration")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
- {
- var plugin = _pluginManager.GetPlugin(pluginId);
- if (plugin?.Instance is IHasPluginConfiguration configPlugin)
- {
- return configPlugin.Configuration;
- }
+ return NotFound();
+ }
+ /// <summary>
+ /// Updates plugin configuration.
+ /// </summary>
+ /// <remarks>
+ /// Accepts plugin configuration as JSON body.
+ /// </remarks>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <response code="204">Plugin configuration updated.</response>
+ /// <response code="404">Plugin not found or plugin does not have configuration.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
+ [HttpPost("{pluginId}/Configuration")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId);
+ if (plugin?.Instance is not IHasPluginConfiguration configPlugin)
+ {
return NotFound();
}
- /// <summary>
- /// 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();
- }
+ var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
+ .ConfigureAwait(false);
- var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
- .ConfigureAwait(false);
+ if (configuration is not null)
+ {
+ configPlugin.UpdateConfiguration(configuration);
+ }
- if (configuration is not null)
- {
- configPlugin.UpdateConfiguration(configuration);
- }
+ return NoContent();
+ }
- return NoContent();
+ /// <summary>
+ /// Gets a plugin's image.
+ /// </summary>
+ /// <param name="pluginId">Plugin id.</param>
+ /// <param name="version">Plugin version.</param>
+ /// <response code="200">Plugin image returned.</response>
+ /// <returns>Plugin's image.</returns>
+ [HttpGet("{pluginId}/{version}/Image")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ [AllowAnonymous]
+ public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
+ {
+ var plugin = _pluginManager.GetPlugin(pluginId, version);
+ if (plugin is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// 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 imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
+ if (plugin.Manifest.ImagePath is null || !System.IO.File.Exists(imagePath))
{
- var plugin = _pluginManager.GetPlugin(pluginId, 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));
+ return NotFound();
}
- /// <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);
+ imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
+ return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
+ }
- if (plugin is not null)
- {
- return plugin.Manifest;
- }
+ /// <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);
- return NotFound();
+ if (plugin is not null)
+ {
+ return plugin.Manifest;
}
+
+ return NotFound();
}
}
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 6dbcdae22..503b9d372 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
@@ -13,126 +12,125 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Quick connect controller.
+/// </summary>
+public class QuickConnectController : BaseJellyfinApiController
{
+ private readonly IQuickConnect _quickConnect;
+ private readonly IAuthorizationContext _authContext;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="QuickConnectController"/> class.
+ /// </summary>
+ /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
+ /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+ public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext)
+ {
+ _quickConnect = quickConnect;
+ _authContext = authContext;
+ }
+
/// <summary>
- /// Quick connect controller.
+ /// Gets the current quick connect state.
/// </summary>
- public class QuickConnectController : BaseJellyfinApiController
+ /// <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()
{
- private readonly IQuickConnect _quickConnect;
- private readonly IAuthorizationContext _authContext;
+ return _quickConnect.IsEnabled;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="QuickConnectController"/> class.
- /// </summary>
- /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
- /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
- public QuickConnectController(IQuickConnect quickConnect, IAuthorizationContext authContext)
+ /// <summary>
+ /// Initiate a new quick connect request.
+ /// </summary>
+ /// <response code="200">Quick connect request successfully created.</response>
+ /// <response code="401">Quick connect is not active on this server.</response>
+ /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
+ [HttpPost("Initiate")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect()
+ {
+ try
{
- _quickConnect = quickConnect;
- _authContext = authContext;
+ var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+ return _quickConnect.TryConnect(auth);
}
-
- /// <summary>
- /// Gets the current quick connect state.
- /// </summary>
- /// <response code="200">Quick connect state returned.</response>
- /// <returns>Whether Quick Connect is enabled on the server or not.</returns>
- [HttpGet("Enabled")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<bool> GetQuickConnectEnabled()
+ catch (AuthenticationException)
{
- return _quickConnect.IsEnabled;
+ 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>
- /// 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()
+ /// <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)
{
- try
- {
- var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
- return _quickConnect.TryConnect(auth);
- }
- catch (AuthenticationException)
- {
- return Unauthorized("Quick connect is disabled");
- }
+ 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>
+ /// 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]
+ [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;
- /// <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)
+ if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator)))
{
- try
- {
- return _quickConnect.CheckRequestStatus(secret);
- }
- catch (ResourceNotFoundException)
- {
- return NotFound("Unknown secret");
- }
- catch (AuthenticationException)
- {
- return Unauthorized("Quick connect is disabled");
- }
+ return Forbid("Unknown user id");
}
- /// <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)
+ try
{
- var currentUserId = User.GetUserId();
- var actualUserId = userId ?? currentUserId;
-
- if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator)))
- {
- return Forbid("Unknown user id");
- }
-
- try
- {
- return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false);
- }
- catch (AuthenticationException)
- {
- return Unauthorized("Quick connect is disabled");
- }
+ return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false);
+ }
+ catch (AuthenticationException)
+ {
+ return Unauthorized("Quick connect is disabled");
}
}
}
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index da9e8cf90..5c77db240 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -15,165 +15,164 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Remote Images Controller.
+/// </summary>
+[Route("")]
+public class RemoteImageController : BaseJellyfinApiController
{
+ private readonly IProviderManager _providerManager;
+ private readonly IServerApplicationPaths _applicationPaths;
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RemoteImageController"/> class.
+ /// </summary>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public RemoteImageController(
+ IProviderManager providerManager,
+ IServerApplicationPaths applicationPaths,
+ ILibraryManager libraryManager)
+ {
+ _providerManager = providerManager;
+ _applicationPaths = applicationPaths;
+ _libraryManager = libraryManager;
+ }
+
/// <summary>
- /// Remote Images Controller.
+ /// Gets available remote images for an item.
/// </summary>
- [Route("")]
- public class RemoteImageController : BaseJellyfinApiController
+ /// <param name="itemId">Item Id.</param>
+ /// <param name="type">The image type.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="providerName">Optional. The image provider to use.</param>
+ /// <param name="includeAllLanguages">Optional. Include all languages.</param>
+ /// <response code="200">Remote Images returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>Remote Image Result.</returns>
+ [HttpGet("Items/{itemId}/RemoteImages")]
+ [Authorize]
+ [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)
{
- private readonly IProviderManager _providerManager;
- private readonly IServerApplicationPaths _applicationPaths;
- private readonly ILibraryManager _libraryManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="RemoteImageController"/> class.
- /// </summary>
- /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
- /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public RemoteImageController(
- IProviderManager providerManager,
- IServerApplicationPaths applicationPaths,
- ILibraryManager libraryManager)
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- _providerManager = providerManager;
- _applicationPaths = applicationPaths;
- _libraryManager = libraryManager;
+ return NotFound();
}
- /// <summary>
- /// Gets available remote images for an item.
- /// </summary>
- /// <param name="itemId">Item Id.</param>
- /// <param name="type">The image type.</param>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="providerName">Optional. The image provider to use.</param>
- /// <param name="includeAllLanguages">Optional. Include all languages.</param>
- /// <response code="200">Remote Images returned.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>Remote Image Result.</returns>
- [HttpGet("Items/{itemId}/RemoteImages")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
- [FromRoute, Required] Guid itemId,
- [FromQuery] ImageType? type,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? providerName,
- [FromQuery] bool includeAllLanguages = false)
+ var images = await _providerManager.GetAvailableRemoteImages(
+ item,
+ new RemoteImageQuery(providerName ?? string.Empty)
+ {
+ IncludeAllLanguages = includeAllLanguages,
+ IncludeDisabledProviders = true,
+ ImageType = type
+ },
+ CancellationToken.None)
+ .ConfigureAwait(false);
+
+ var imageArray = images.ToArray();
+ var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
+ if (type.HasValue)
{
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
-
- var images = await _providerManager.GetAvailableRemoteImages(
- item,
- new RemoteImageQuery(providerName ?? string.Empty)
- {
- IncludeAllLanguages = includeAllLanguages,
- IncludeDisabledProviders = true,
- ImageType = type
- },
- CancellationToken.None)
- .ConfigureAwait(false);
-
- var imageArray = images.ToArray();
- var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
- if (type.HasValue)
- {
- allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value));
- }
-
- var result = new RemoteImageResult
- {
- TotalRecordCount = imageArray.Length,
- Providers = allProviders.Select(o => o.Name)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray()
- };
-
- if (startIndex.HasValue)
- {
- imageArray = imageArray.Skip(startIndex.Value).ToArray();
- }
-
- if (limit.HasValue)
- {
- imageArray = imageArray.Take(limit.Value).ToArray();
- }
-
- result.Images = imageArray;
- return result;
+ allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value));
}
- /// <summary>
- /// Gets available remote image providers for an item.
- /// </summary>
- /// <param name="itemId">Item Id.</param>
- /// <response code="200">Returned remote image providers.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>List of remote image providers.</returns>
- [HttpGet("Items/{itemId}/RemoteImages/Providers")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute, Required] Guid itemId)
+ var result = new RemoteImageResult
{
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ TotalRecordCount = imageArray.Length,
+ Providers = allProviders.Select(o => o.Name)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray()
+ };
- return Ok(_providerManager.GetRemoteImageProviderInfo(item));
+ if (startIndex.HasValue)
+ {
+ imageArray = imageArray.Skip(startIndex.Value).ToArray();
}
- /// <summary>
- /// Downloads a remote image for an item.
- /// </summary>
- /// <param name="itemId">Item Id.</param>
- /// <param name="type">The image type.</param>
- /// <param name="imageUrl">The image url.</param>
- /// <response code="204">Remote image downloaded.</response>
- /// <response code="404">Remote image not found.</response>
- /// <returns>Download status.</returns>
- [HttpPost("Items/{itemId}/RemoteImages/Download")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DownloadRemoteImage(
- [FromRoute, Required] Guid itemId,
- [FromQuery, Required] ImageType type,
- [FromQuery] string? imageUrl)
+ if (limit.HasValue)
{
- var item = _libraryManager.GetItemById(itemId);
- if (item is null)
- {
- return NotFound();
- }
+ imageArray = imageArray.Take(limit.Value).ToArray();
+ }
- await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None)
- .ConfigureAwait(false);
+ result.Images = imageArray;
+ return result;
+ }
- await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
+ /// <summary>
+ /// Gets available remote image providers for an item.
+ /// </summary>
+ /// <param name="itemId">Item Id.</param>
+ /// <response code="200">Returned remote image providers.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>List of remote image providers.</returns>
+ [HttpGet("Items/{itemId}/RemoteImages/Providers")]
+ [Authorize]
+ [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 NotFound();
}
- /// <summary>
- /// Gets the full cache path.
- /// </summary>
- /// <param name="filename">The filename.</param>
- /// <returns>System.String.</returns>
- private string GetFullCachePath(string filename)
+ 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 Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
+ 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);
}
}
diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index 832e14505..c8fa11ac6 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -8,154 +8,153 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Scheduled Tasks Controller.
+/// </summary>
+[Authorize(Policy = Policies.RequiresElevation)]
+public class ScheduledTasksController : BaseJellyfinApiController
{
+ private readonly ITaskManager _taskManager;
+
/// <summary>
- /// Scheduled Tasks Controller.
+ /// Initializes a new instance of the <see cref="ScheduledTasksController"/> class.
/// </summary>
- [Authorize(Policy = Policies.RequiresElevation)]
- public class ScheduledTasksController : BaseJellyfinApiController
+ /// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
+ public ScheduledTasksController(ITaskManager taskManager)
{
- private readonly ITaskManager _taskManager;
+ _taskManager = taskManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="ScheduledTasksController"/> class.
- /// </summary>
- /// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
- public ScheduledTasksController(ITaskManager taskManager)
- {
- _taskManager = taskManager;
- }
+ /// <summary>
+ /// Get tasks.
+ /// </summary>
+ /// <param name="isHidden">Optional filter tasks that are hidden, or not.</param>
+ /// <param name="isEnabled">Optional filter tasks that are enabled, or not.</param>
+ /// <response code="200">Scheduled tasks retrieved.</response>
+ /// <returns>The list of scheduled tasks.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IEnumerable<TaskInfo> GetTasks(
+ [FromQuery] bool? isHidden,
+ [FromQuery] bool? isEnabled)
+ {
+ IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
- /// <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)
+ foreach (var task in tasks)
{
- IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
-
- foreach (var task in tasks)
+ if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask)
{
- if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask)
+ if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden)
{
- if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden)
- {
- continue;
- }
-
- if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled)
- {
- continue;
- }
+ continue;
}
- yield return ScheduledTaskHelpers.GetTaskInfo(task);
+ 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));
+ yield return ScheduledTaskHelpers.GetTaskInfo(task);
+ }
+ }
- if (task is null)
- {
- return NotFound();
- }
+ /// <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));
- return ScheduledTaskHelpers.GetTaskInfo(task);
+ if (task is null)
+ {
+ return NotFound();
}
- /// <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));
+ return ScheduledTaskHelpers.GetTaskInfo(task);
+ }
- if (task is null)
- {
- return NotFound();
- }
+ /// <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));
- _taskManager.Execute(task, new TaskOptions());
- return NoContent();
+ if (task is null)
+ {
+ return NotFound();
}
- /// <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));
+ _taskManager.Execute(task, new TaskOptions());
+ return NoContent();
+ }
- if (task is null)
- {
- return NotFound();
- }
+ /// <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));
- _taskManager.Cancel(task);
- return NoContent();
+ if (task is null)
+ {
+ return NotFound();
}
- /// <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();
- }
+ _taskManager.Cancel(task);
+ return NoContent();
+ }
- task.Triggers = triggerInfos;
- 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();
}
}
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 3b7719f37..a25b43345 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -3,7 +3,6 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@@ -20,247 +19,246 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Search controller.
+/// </summary>
+[Route("Search/Hints")]
+[Authorize]
+public class SearchController : BaseJellyfinApiController
{
+ private readonly ISearchEngine _searchEngine;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly IImageProcessor _imageProcessor;
+
/// <summary>
- /// Search controller.
+ /// Initializes a new instance of the <see cref="SearchController"/> class.
/// </summary>
- [Route("Search/Hints")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class SearchController : BaseJellyfinApiController
+ /// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</param>
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
+ /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
+ public SearchController(
+ ISearchEngine searchEngine,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IImageProcessor imageProcessor)
{
- private readonly ISearchEngine _searchEngine;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly IImageProcessor _imageProcessor;
+ _searchEngine = searchEngine;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _imageProcessor = imageProcessor;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="SearchController"/> class.
- /// </summary>
- /// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</param>
- /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
- /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
- public SearchController(
- ISearchEngine searchEngine,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IImageProcessor imageProcessor)
+ /// <summary>
+ /// Gets the search hint result.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
+ /// <param name="searchTerm">The search term to filter on.</param>
+ /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimited.</param>
+ /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimited.</param>
+ /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimited.</param>
+ /// <param name="parentId">If specified, only children of the parent are returned.</param>
+ /// <param name="isMovie">Optional filter for movies.</param>
+ /// <param name="isSeries">Optional filter for series.</param>
+ /// <param name="isNews">Optional filter for news.</param>
+ /// <param name="isKids">Optional filter for kids.</param>
+ /// <param name="isSports">Optional filter for sports.</param>
+ /// <param name="includePeople">Optional filter whether to include people.</param>
+ /// <param name="includeMedia">Optional filter whether to include media.</param>
+ /// <param name="includeGenres">Optional filter whether to include genres.</param>
+ /// <param name="includeStudios">Optional filter whether to include studios.</param>
+ /// <param name="includeArtists">Optional filter whether to include artists.</param>
+ /// <response code="200">Search hint returned.</response>
+ /// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns>
+ [HttpGet]
+ [Description("Gets search hints based on a search term")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<SearchHintResult> GetSearchHints(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] Guid? userId,
+ [FromQuery, Required] string searchTerm,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery] Guid? parentId,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery] bool includePeople = true,
+ [FromQuery] bool includeMedia = true,
+ [FromQuery] bool includeGenres = true,
+ [FromQuery] bool includeStudios = true,
+ [FromQuery] bool includeArtists = true)
+ {
+ var result = _searchEngine.GetSearchHints(new SearchQuery
{
- _searchEngine = searchEngine;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _imageProcessor = imageProcessor;
- }
+ Limit = limit,
+ SearchTerm = searchTerm,
+ IncludeArtists = includeArtists,
+ IncludeGenres = includeGenres,
+ IncludeMedia = includeMedia,
+ IncludePeople = includePeople,
+ IncludeStudios = includeStudios,
+ StartIndex = startIndex,
+ UserId = userId ?? Guid.Empty,
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
+ MediaTypes = mediaTypes,
+ ParentId = parentId,
- /// <summary>
- /// Gets the search hint result.
- /// </summary>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
- /// <param name="searchTerm">The search term to filter on.</param>
- /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimited.</param>
- /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimited.</param>
- /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimited.</param>
- /// <param name="parentId">If specified, only children of the parent are returned.</param>
- /// <param name="isMovie">Optional filter for movies.</param>
- /// <param name="isSeries">Optional filter for series.</param>
- /// <param name="isNews">Optional filter for news.</param>
- /// <param name="isKids">Optional filter for kids.</param>
- /// <param name="isSports">Optional filter for sports.</param>
- /// <param name="includePeople">Optional filter whether to include people.</param>
- /// <param name="includeMedia">Optional filter whether to include media.</param>
- /// <param name="includeGenres">Optional filter whether to include genres.</param>
- /// <param name="includeStudios">Optional filter whether to include studios.</param>
- /// <param name="includeArtists">Optional filter whether to include artists.</param>
- /// <response code="200">Search hint returned.</response>
- /// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns>
- [HttpGet]
- [Description("Gets search hints based on a search term")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<SearchHintResult> GetSearchHints(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] Guid? userId,
- [FromQuery, Required] string searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery] Guid? parentId,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery] bool includePeople = true,
- [FromQuery] bool includeMedia = true,
- [FromQuery] bool includeGenres = true,
- [FromQuery] bool includeStudios = true,
- [FromQuery] bool includeArtists = true)
- {
- var result = _searchEngine.GetSearchHints(new SearchQuery
- {
- Limit = limit,
- SearchTerm = searchTerm,
- IncludeArtists = includeArtists,
- IncludeGenres = includeGenres,
- IncludeMedia = includeMedia,
- IncludePeople = includePeople,
- IncludeStudios = includeStudios,
- StartIndex = startIndex,
- UserId = userId ?? Guid.Empty,
- IncludeItemTypes = includeItemTypes,
- ExcludeItemTypes = excludeItemTypes,
- MediaTypes = mediaTypes,
- ParentId = parentId,
+ IsKids = isKids,
+ IsMovie = isMovie,
+ IsNews = isNews,
+ IsSeries = isSeries,
+ IsSports = isSports
+ });
- IsKids = isKids,
- IsMovie = isMovie,
- IsNews = isNews,
- IsSeries = isSeries,
- IsSports = isSports
- });
+ return new SearchHintResult(result.Items.Select(GetSearchHintResult).ToArray(), result.TotalRecordCount);
+ }
- 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;
- /// <summary>
- /// Gets the search hint result.
- /// </summary>
- /// <param name="hintInfo">The hint info.</param>
- /// <returns>SearchHintResult.</returns>
- private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
+ var result = new SearchHint
{
- var item = hintInfo.Item;
-
- var result = new SearchHint
- {
- Name = item.Name,
- IndexNumber = item.IndexNumber,
- ParentIndexNumber = item.ParentIndexNumber,
- Id = item.Id,
- Type = item.GetBaseItemKind(),
- MediaType = item.MediaType,
- MatchedTerm = hintInfo.MatchedTerm,
- RunTimeTicks = item.RunTimeTicks,
- ProductionYear = item.ProductionYear,
- ChannelId = item.ChannelId,
- EndDate = item.EndDate
- };
+ Name = item.Name,
+ IndexNumber = item.IndexNumber,
+ ParentIndexNumber = item.ParentIndexNumber,
+ Id = item.Id,
+ Type = item.GetBaseItemKind(),
+ MediaType = item.MediaType,
+ MatchedTerm = hintInfo.MatchedTerm,
+ RunTimeTicks = item.RunTimeTicks,
+ ProductionYear = item.ProductionYear,
+ ChannelId = item.ChannelId,
+ EndDate = item.EndDate
+ };
#pragma warning disable CS0618
- // Kept for compatibility with older clients
- result.ItemId = result.Id;
+ // Kept for compatibility with older clients
+ result.ItemId = result.Id;
#pragma warning restore CS0618
- if (item.IsFolder)
- {
- result.IsFolder = true;
- }
-
- var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
+ if (item.IsFolder)
+ {
+ result.IsFolder = true;
+ }
- if (primaryImageTag is not null)
- {
- result.PrimaryImageTag = primaryImageTag;
- result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item);
- }
+ var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
- SetThumbImageInfo(result, item);
- SetBackdropImageInfo(result, item);
+ if (primaryImageTag is not null)
+ {
+ result.PrimaryImageTag = primaryImageTag;
+ result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(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();
- }
+ SetThumbImageInfo(result, item);
+ SetBackdropImageInfo(result, item);
- 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;
+ 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();
+ }
- MusicAlbum musicAlbum = song.AlbumEntity;
+ break;
+ case MusicAlbum album:
+ result.Artists = album.Artists;
+ result.AlbumArtist = album.AlbumArtist;
+ break;
+ case Audio song:
+ result.AlbumArtist = song.AlbumArtists?.FirstOrDefault();
+ result.Artists = song.Artists;
- if (musicAlbum is not null)
- {
- result.Album = musicAlbum.Name;
- result.AlbumId = musicAlbum.Id;
- }
- else
- {
- result.Album = song.Album;
- }
+ MusicAlbum musicAlbum = song.AlbumEntity;
- break;
- }
+ if (musicAlbum is not null)
+ {
+ result.Album = musicAlbum.Name;
+ result.AlbumId = musicAlbum.Id;
+ }
+ else
+ {
+ result.Album = song.Album;
+ }
- if (!item.ChannelId.Equals(default))
- {
- var channel = _libraryManager.GetItemById(item.ChannelId);
- result.ChannelName = channel?.Name;
- }
+ break;
+ }
- return result;
+ if (!item.ChannelId.Equals(default))
+ {
+ var channel = _libraryManager.GetItemById(item.ChannelId);
+ result.ChannelName = channel?.Name;
}
- private void SetThumbImageInfo(SearchHint hint, BaseItem item)
+ return result;
+ }
+
+ private void SetThumbImageInfo(SearchHint hint, BaseItem item)
+ {
+ var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
+
+ if (itemWithImage is null && item is Episode)
{
- var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
+ itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
+ }
- if (itemWithImage is null && item is Episode)
- {
- itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
- }
+ itemWithImage ??= GetParentWithImage<BaseItem>(item, ImageType.Thumb);
- itemWithImage ??= GetParentWithImage<BaseItem>(item, ImageType.Thumb);
+ if (itemWithImage is not null)
+ {
+ var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
- if (itemWithImage is not null)
+ if (tag 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);
- }
+ 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);
- private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
+ if (itemWithImage is not null)
{
- var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
- ?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
+ var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
- if (itemWithImage is not null)
+ if (tag 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);
- }
+ 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));
- }
+ private T? GetParentWithImage<T>(BaseItem item, ImageType type)
+ where T : BaseItem
+ {
+ return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
}
}
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 25f930135..e93456de6 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -19,480 +19,483 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The session controller.
+/// </summary>
+[Route("")]
+public class SessionController : BaseJellyfinApiController
{
+ private readonly ISessionManager _sessionManager;
+ private readonly IUserManager _userManager;
+ private readonly IDeviceManager _deviceManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SessionController"/> class.
+ /// </summary>
+ /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
+ /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
+ /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
+ public SessionController(
+ ISessionManager sessionManager,
+ IUserManager userManager,
+ IDeviceManager deviceManager)
+ {
+ _sessionManager = sessionManager;
+ _userManager = userManager;
+ _deviceManager = deviceManager;
+ }
+
/// <summary>
- /// The session controller.
+ /// Gets a list of sessions.
/// </summary>
- [Route("")]
- public class SessionController : BaseJellyfinApiController
+ /// <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]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<SessionInfo>> GetSessions(
+ [FromQuery] Guid? controllableByUserId,
+ [FromQuery] string? deviceId,
+ [FromQuery] int? activeWithinSeconds)
{
- private readonly ISessionManager _sessionManager;
- private readonly IUserManager _userManager;
- private readonly IDeviceManager _deviceManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SessionController"/> class.
- /// </summary>
- /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
- /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
- /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
- public SessionController(
- ISessionManager sessionManager,
- IUserManager userManager,
- IDeviceManager deviceManager)
+ var result = _sessionManager.Sessions;
+
+ if (!string.IsNullOrEmpty(deviceId))
{
- _sessionManager = sessionManager;
- _userManager = userManager;
- _deviceManager = deviceManager;
+ result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
}
- /// <summary>
- /// Gets a list of sessions.
- /// </summary>
- /// <param name="controllableByUserId">Filter by sessions that a given user is allowed to remote control.</param>
- /// <param name="deviceId">Filter by device Id.</param>
- /// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param>
- /// <response code="200">List of sessions returned.</response>
- /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns>
- [HttpGet("Sessions")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<SessionInfo>> GetSessions(
- [FromQuery] Guid? controllableByUserId,
- [FromQuery] string? deviceId,
- [FromQuery] int? activeWithinSeconds)
+ if (controllableByUserId.HasValue && !controllableByUserId.Equals(default))
{
- var result = _sessionManager.Sessions;
+ result = result.Where(i => i.SupportsRemoteControl);
- if (!string.IsNullOrEmpty(deviceId))
+ var user = _userManager.GetUserById(controllableByUserId.Value);
+ if (user is null)
{
- result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
+ return NotFound();
}
- if (controllableByUserId.HasValue && !controllableByUserId.Equals(default))
+ if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
{
- result = result.Where(i => i.SupportsRemoteControl);
-
- var user = _userManager.GetUserById(controllableByUserId.Value);
-
- if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
- {
- result = result.Where(i => i.UserId.Equals(default) || i.ContainsUser(controllableByUserId.Value));
- }
+ result = result.Where(i => i.UserId.Equals(default) || i.ContainsUser(controllableByUserId.Value));
+ }
- if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
- {
- result = result.Where(i => !i.UserId.Equals(default));
- }
+ if (!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);
- }
+ 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 =>
+ result = result.Where(i =>
+ {
+ if (!string.IsNullOrWhiteSpace(i.DeviceId))
{
- if (!string.IsNullOrWhiteSpace(i.DeviceId))
+ if (!_deviceManager.CanAccessDevice(user, i.DeviceId))
{
- if (!_deviceManager.CanAccessDevice(user, i.DeviceId))
- {
- return false;
- }
+ return false;
}
+ }
- return true;
- });
- }
-
- return Ok(result);
+ return true;
+ });
}
- /// <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)
+ 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]
+ [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
{
- 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();
- }
+ 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)
+ /// <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]
+ [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
{
- 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]
+ [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()
{
- 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();
- }
+ Command = command,
+ ControllingUserId = controllingUserId,
+ SeekPositionTicks = seekPositionTicks,
+ },
+ 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)
+ /// <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]
+ [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
{
- 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();
- }
+ Name = command,
+ ControllingUserId = currentSession.UserId
+ };
- /// <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);
- await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
- 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]
+ [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);
- /// <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 generalCommand = new GeneralCommand
{
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ Name = command,
+ ControllingUserId = currentSession.UserId
+ };
- var generalCommand = new GeneralCommand
- {
- Name = command,
- ControllingUserId = currentSession.UserId
- };
+ await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None)
+ .ConfigureAwait(false);
- await _sessionManager.SendGeneralCommand(currentSession.Id, sessionId, generalCommand, CancellationToken.None)
- .ConfigureAwait(false);
+ return NoContent();
+ }
- 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]
+ [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);
- /// <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);
- ArgumentNullException.ThrowIfNull(command);
+ command.ControllingUserId = currentSession.UserId;
- command.ControllingUserId = currentSession.UserId;
+ await _sessionManager.SendGeneralCommand(
+ currentSession.Id,
+ sessionId,
+ command,
+ CancellationToken.None)
+ .ConfigureAwait(false);
- await _sessionManager.SendGeneralCommand(
- currentSession.Id,
- sessionId,
- command,
- CancellationToken.None)
- .ConfigureAwait(false);
+ return NoContent();
+ }
- 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]
+ [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>
- /// 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);
- await _sessionManager.SendMessageCommand(
- await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false),
- sessionId,
- command,
- CancellationToken.None)
- .ConfigureAwait(false);
+ return NoContent();
+ }
- 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]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult AddUserToSession(
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] Guid userId)
+ {
+ _sessionManager.AddAdditionalUser(sessionId, userId);
+ 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]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult RemoveUserFromSession(
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] Guid userId)
+ {
+ _sessionManager.RemoveAdditionalUser(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)
+ /// <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]
+ [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))
{
- _sessionManager.RemoveAdditionalUser(sessionId, userId);
- return NoContent();
+ id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
}
- /// <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)
+ _sessionManager.ReportCapabilities(id, new ClientCapabilities
{
- 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();
- }
+ 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)
+ /// <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]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> PostFullCapabilities(
+ [FromQuery] string? id,
+ [FromBody, Required] ClientCapabilitiesDto capabilities)
+ {
+ if (string.IsNullOrWhiteSpace(id))
{
- if (string.IsNullOrWhiteSpace(id))
- {
- id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- }
+ id = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ }
- _sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
+ _sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
- return NoContent();
- }
+ return NoContent();
+ }
- /// <summary>
- /// Reports that a session is viewing an item.
- /// </summary>
- /// <param name="sessionId">The session id.</param>
- /// <param name="itemId">The item id.</param>
- /// <response code="204">Session reported to server.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpPost("Sessions/Viewing")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> ReportViewing(
- [FromQuery] string? sessionId,
- [FromQuery, Required] string? itemId)
- {
- string session = sessionId ?? await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ /// <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]
+ [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();
- }
+ _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>
+ /// 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]
+ [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 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();
- }
+ /// <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();
}
}
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index eec5779e6..aab390d1f 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -10,141 +10,140 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The startup wizard controller.
+/// </summary>
+[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
+public class StartupController : BaseJellyfinApiController
{
+ private readonly IServerConfigurationManager _config;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// The startup wizard controller.
+ /// Initializes a new instance of the <see cref="StartupController" /> class.
/// </summary>
- [Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
- public class StartupController : BaseJellyfinApiController
+ /// <param name="config">The server configuration manager.</param>
+ /// <param name="userManager">The user manager.</param>
+ public StartupController(IServerConfigurationManager config, IUserManager userManager)
{
- private readonly IServerConfigurationManager _config;
- private readonly IUserManager _userManager;
+ _config = config;
+ _userManager = userManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="StartupController" /> class.
- /// </summary>
- /// <param name="config">The server configuration manager.</param>
- /// <param name="userManager">The user manager.</param>
- public StartupController(IServerConfigurationManager config, IUserManager userManager)
- {
- _config = config;
- _userManager = userManager;
- }
+ /// <summary>
+ /// Completes the startup wizard.
+ /// </summary>
+ /// <response code="204">Startup wizard completed.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Complete")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult CompleteWizard()
+ {
+ _config.Configuration.IsStartupWizardCompleted = true;
+ _config.SaveConfiguration();
+ return NoContent();
+ }
- /// <summary>
- /// Completes the startup wizard.
- /// </summary>
- /// <response code="204">Startup wizard completed.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Complete")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CompleteWizard()
+ /// <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.Configuration.IsStartupWizardCompleted = true;
- _config.SaveConfiguration();
- return NoContent();
- }
+ UICulture = _config.Configuration.UICulture,
+ MetadataCountryCode = _config.Configuration.MetadataCountryCode,
+ PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
+ };
+ }
- /// <summary>
- /// Gets the initial startup wizard configuration.
- /// </summary>
- /// <response code="200">Initial startup wizard configuration retrieved.</response>
- /// <returns>An <see cref="OkResult"/> containing the initial startup wizard configuration.</returns>
- [HttpGet("Configuration")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
- {
- return new StartupConfigurationDto
- {
- UICulture = _config.Configuration.UICulture,
- MetadataCountryCode = _config.Configuration.MetadataCountryCode,
- PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
- };
- }
+ /// <summary>
+ /// Sets the initial startup wizard configuration.
+ /// </summary>
+ /// <param name="startupConfiguration">The updated startup configuration.</param>
+ /// <response code="204">Configuration saved.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Configuration")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration)
+ {
+ _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty;
+ _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty;
+ _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty;
+ _config.SaveConfiguration();
+ return NoContent();
+ }
- /// <summary>
- /// Sets 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>
- /// 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)
+ /// <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
{
- NetworkConfiguration settings = _config.GetNetworkConfiguration();
- settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
- settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
- _config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings);
- return NoContent();
- }
+ Name = user.Username,
+ Password = user.Password
+ };
+ }
- /// <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();
- /// <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)
+ if (startupUserDto.Name is not null)
{
- var user = _userManager.Users.First();
-
- if (startupUserDto.Name is not null)
- {
- user.Username = startupUserDto.Name;
- }
-
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+ user.Username = startupUserDto.Name;
+ }
- if (!string.IsNullOrEmpty(startupUserDto.Password))
- {
- await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
- }
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
- return NoContent();
+ if (!string.IsNullOrEmpty(startupUserDto.Password))
+ {
+ await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
}
+
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index 1288fb512..21965e956 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -16,141 +15,140 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Studios controller.
+/// </summary>
+[Authorize]
+public class StudiosController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+
/// <summary>
- /// Studios controller.
+ /// Initializes a new instance of the <see cref="StudiosController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class StudiosController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ public StudiosController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="StudiosController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- public StudiosController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- }
+ /// <summary>
+ /// Gets all studios from a given item, folder, or the entire library.
+ /// </summary>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="searchTerm">Optional. Search term.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+ /// <param name="enableUserData">Optional, include user data.</param>
+ /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <param name="userId">User id.</param>
+ /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+ /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+ /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <param name="enableTotalRecordCount">Total record count.</param>
+ /// <response code="200">Studios returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the studios.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetStudios(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? searchTerm,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery] bool? isFavorite,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] Guid? userId,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery] bool? enableImages = true,
+ [FromQuery] bool enableTotalRecordCount = true)
+ {
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- /// <summary>
- /// Gets all studios from a given item, folder, or the entire library.
- /// </summary>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="searchTerm">Optional. Search term.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="enableUserData">Optional, include user data.</param>
- /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="userId">User id.</param>
- /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
- /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
- /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
- /// <param name="enableImages">Optional, include image information in output.</param>
- /// <param name="enableTotalRecordCount">Total record count.</param>
- /// <response code="200">Studios returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the studios.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetStudios(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] string? searchTerm,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery] bool? isFavorite,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] Guid? userId,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery] bool? enableImages = true,
- [FromQuery] bool enableTotalRecordCount = true)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ User? user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- User? user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
- var parentItem = _libraryManager.GetParentItem(parentId, userId);
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ StartIndex = startIndex,
+ Limit = limit,
+ IsFavorite = isFavorite,
+ NameLessThan = nameLessThan,
+ NameStartsWith = nameStartsWith,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
+ DtoOptions = dtoOptions,
+ SearchTerm = searchTerm,
+ EnableTotalRecordCount = enableTotalRecordCount
+ };
- var query = new InternalItemsQuery(user)
+ if (parentId.HasValue)
+ {
+ if (parentItem is Folder)
{
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
- };
-
- if (parentId.HasValue)
+ query.AncestorIds = new[] { parentId.Value };
+ }
+ else
{
- if (parentItem is Folder)
- {
- query.AncestorIds = new[] { parentId.Value };
- }
- else
- {
- query.ItemIds = new[] { parentId.Value };
- }
+ query.ItemIds = new[] { parentId.Value };
}
-
- var result = _libraryManager.GetStudios(query);
- var shouldIncludeItemTypes = includeItemTypes.Length != 0;
- return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
- /// <summary>
- /// Gets a studio by name.
- /// </summary>
- /// <param name="name">Studio name.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <response code="200">Studio returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the studio.</returns>
- [HttpGet("{name}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId)
- {
- var dtoOptions = new DtoOptions().AddClientFields(User);
+ var result = _libraryManager.GetStudios(query);
+ var shouldIncludeItemTypes = includeItemTypes.Length != 0;
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
+ }
- var item = _libraryManager.GetStudio(name);
- if (userId.HasValue && !userId.Equals(default))
- {
- var user = _userManager.GetUserById(userId.Value);
+ /// <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);
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ var item = _libraryManager.GetStudio(name);
+ if (userId.HasValue && !userId.Equals(default))
+ {
+ var user = _userManager.GetUserById(userId.Value);
- return _dtoService.GetBaseItemDto(item, dtoOptions);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
}
}
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index c3ce1868e..e38421338 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -30,522 +30,521 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <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>
+ /// Initializes a new instance of the <see cref="SubtitleController"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
+ /// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
+ /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
+ /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
+ /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
+ public SubtitleController(
+ IServerConfigurationManager serverConfigurationManager,
+ ILibraryManager libraryManager,
+ ISubtitleManager subtitleManager,
+ ISubtitleEncoder subtitleEncoder,
+ IMediaSourceManager mediaSourceManager,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ ILogger<SubtitleController> logger)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+ _libraryManager = libraryManager;
+ _subtitleManager = subtitleManager;
+ _subtitleEncoder = subtitleEncoder;
+ _mediaSourceManager = mediaSourceManager;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ _logger = logger;
+ }
+
/// <summary>
- /// Subtitle controller.
+ /// Deletes an external subtitle file.
/// </summary>
- [Route("")]
- public class SubtitleController : BaseJellyfinApiController
+ /// <param name="itemId">The item id.</param>
+ /// <param name="index">The index of the subtitle file.</param>
+ /// <response code="204">Subtitle deleted.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Videos/{itemId}/Subtitles/{index}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<Task> DeleteSubtitle(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int index)
{
- 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>
- /// Initializes a new instance of the <see cref="SubtitleController"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
- /// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
- /// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
- /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
- /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
- public SubtitleController(
- IServerConfigurationManager serverConfigurationManager,
- ILibraryManager libraryManager,
- ISubtitleManager subtitleManager,
- ISubtitleEncoder subtitleEncoder,
- IMediaSourceManager mediaSourceManager,
- IProviderManager providerManager,
- IFileSystem fileSystem,
- ILogger<SubtitleController> logger)
+ var item = _libraryManager.GetItemById(itemId);
+
+ if (item is null)
{
- _serverConfigurationManager = serverConfigurationManager;
- _libraryManager = libraryManager;
- _subtitleManager = subtitleManager;
- _subtitleEncoder = subtitleEncoder;
- _mediaSourceManager = mediaSourceManager;
- _providerManager = providerManager;
- _fileSystem = fileSystem;
- _logger = logger;
+ return NotFound();
}
- /// <summary>
- /// Deletes an external subtitle file.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="index">The index of the subtitle file.</param>
- /// <response code="204">Subtitle deleted.</response>
- /// <response code="404">Item not found.</response>
- /// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Videos/{itemId}/Subtitles/{index}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<Task> DeleteSubtitle(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] int index)
- {
- var item = _libraryManager.GetItemById(itemId);
+ _subtitleManager.DeleteSubtitles(item, index);
+ return NoContent();
+ }
- if (item is null)
- {
- return NotFound();
- }
+ /// <summary>
+ /// Search remote subtitles.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="language">The language of the subtitles.</param>
+ /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
+ /// <response code="200">Subtitles retrieved.</response>
+ /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
+ [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
+ [Authorize]
+ [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);
- _subtitleManager.DeleteSubtitles(item, index);
- return NoContent();
- }
+ 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]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> DownloadRemoteSubtitles(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string subtitleId)
+ {
+ var video = (Video)_libraryManager.GetItemById(itemId);
- /// <summary>
- /// Search remote subtitles.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="language">The language of the subtitles.</param>
- /// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
- /// <response code="200">Subtitles retrieved.</response>
- /// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
- [HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
- [FromRoute, Required] Guid itemId,
- [FromRoute, Required] string language,
- [FromQuery] bool? isPerfectMatch)
+ try
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
+ .ConfigureAwait(false);
- return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false);
+ _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
}
-
- /// <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)
+ catch (Exception ex)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ _logger.LogError(ex, "Error downloading subtitles");
+ }
- try
- {
- await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
- .ConfigureAwait(false);
+ return NoContent();
+ }
- _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error downloading subtitles");
- }
+ /// <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]
+ [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 NoContent();
- }
+ return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format));
+ }
- /// <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);
+ /// <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;
- return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format));
+ if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
+ {
+ format = "json";
}
- /// <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)
+ if (string.IsNullOrEmpty(format))
{
- // 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 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 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);
+ var subtitleStream = mediaSource.MediaStreams
+ .First(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
- return PhysicalFile(subtitleStream.Path, MimeTypes.GetMimeType(subtitleStream.Path));
- }
+ return PhysicalFile(subtitleStream.Path, MimeTypes.GetMimeType(subtitleStream.Path));
+ }
- if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
+ 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))
{
- 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);
+ using var reader = new StreamReader(stream);
- var text = await reader.ReadToEndAsync().ConfigureAwait(false);
+ var text = await reader.ReadToEndAsync().ConfigureAwait(false);
- text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal);
+ 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(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format));
}
-
- return File(
- await EncodeSubtitles(
- itemId.Value,
- mediaSourceId,
- index.Value,
- format,
- startPositionTicks,
- endPositionTicks,
- copyTimestamps).ConfigureAwait(false),
- MimeTypes.GetMimeType("file." + format));
}
- /// <summary>
- /// Gets subtitles in a specified format.
- /// </summary>
- /// <param name="routeItemId">The (route) item id.</param>
- /// <param name="routeMediaSourceId">The (route) media source id.</param>
- /// <param name="routeIndex">The (route) subtitle stream index.</param>
- /// <param name="routeStartPositionTicks">The (route) start position of the subtitle in ticks.</param>
- /// <param name="routeFormat">The (route) format of the returned subtitle.</param>
- /// <param name="itemId">The item id.</param>
- /// <param name="mediaSourceId">The media source id.</param>
- /// <param name="index">The subtitle stream index.</param>
- /// <param name="startPositionTicks">The start position of the subtitle in ticks.</param>
- /// <param name="format">The format of the returned subtitle.</param>
- /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
- /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
- /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
- /// <response code="200">File returned.</response>
- /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
- [HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesFile("text/*")]
- public Task<ActionResult> GetSubtitleWithTicks(
- [FromRoute, Required] Guid routeItemId,
- [FromRoute, Required] string routeMediaSourceId,
- [FromRoute, Required] int routeIndex,
- [FromRoute, Required] long routeStartPositionTicks,
- [FromRoute, Required] string routeFormat,
- [FromQuery, ParameterObsolete] Guid? itemId,
- [FromQuery, ParameterObsolete] string? mediaSourceId,
- [FromQuery, ParameterObsolete] int? index,
- [FromQuery, ParameterObsolete] long? startPositionTicks,
- [FromQuery, ParameterObsolete] string? format,
- [FromQuery] long? endPositionTicks,
- [FromQuery] bool copyTimestamps = false,
- [FromQuery] bool addVttTimeMap = false)
- {
- return GetSubtitle(
- routeItemId,
- routeMediaSourceId,
- routeIndex,
- routeFormat,
- itemId,
+ return File(
+ await EncodeSubtitles(
+ itemId.Value,
mediaSourceId,
- index,
+ index.Value,
format,
+ startPositionTicks,
endPositionTicks,
- copyTimestamps,
- addVttTimeMap,
- startPositionTicks ?? routeStartPositionTicks);
- }
+ copyTimestamps).ConfigureAwait(false),
+ MimeTypes.GetMimeType("file." + format));
+ }
- /// <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);
+ /// <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);
+ }
- var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
+ /// <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]
+ [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 runtime = mediaSource.RunTimeTicks ?? -1;
+ var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
- if (runtime <= 0)
- {
- throw new ArgumentException("HLS Subtitles are not supported for this media.");
- }
+ var runtime = mediaSource.RunTimeTicks ?? -1;
- 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)");
- }
+ if (runtime <= 0)
+ {
+ throw new ArgumentException("HLS Subtitles are not supported for this media.");
+ }
- 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");
+ 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)");
+ }
- long positionTicks = 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");
- var accessToken = User.GetToken();
+ long positionTicks = 0;
- while (positionTicks < runtime)
- {
- var remaining = runtime - positionTicks;
- var lengthTicks = Math.Min(remaining, segmentLengthTicks);
+ var accessToken = User.GetToken();
- builder.Append("#EXTINF:")
- .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds)
- .Append(',')
- .AppendLine();
+ while (positionTicks < runtime)
+ {
+ var remaining = runtime - positionTicks;
+ var lengthTicks = Math.Min(remaining, segmentLengthTicks);
- var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
+ builder.Append("#EXTINF:")
+ .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds)
+ .Append(',')
+ .AppendLine();
- 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);
+ var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
- builder.AppendLine(url);
+ 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);
- positionTicks += segmentLengthTicks;
- }
+ builder.AppendLine(url);
- builder.AppendLine("#EXT-X-ENDLIST");
- return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+ positionTicks += segmentLengthTicks;
}
- /// <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();
- }
- }
+ builder.AppendLine("#EXT-X-ENDLIST");
+ return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
+ }
- /// <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)
+ /// <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))
{
- var item = _libraryManager.GetItemById(id);
+ 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 _subtitleEncoder.GetSubtitles(
- item,
- mediaSourceId,
- index,
- format,
- startPositionTicks,
- endPositionTicks ?? 0,
- copyTimestamps,
- CancellationToken.None);
+ return NoContent();
}
+ }
- /// <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;
+ /// <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]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IEnumerable<FontFile> GetFallbackFontList()
+ {
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var fallbackFontPath = encodingOptions.FallbackFontPath;
- if (!string.IsNullOrEmpty(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)
{
- 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)
{
- sizeCounter += fontFile.Size;
- if (sizeCounter >= MaxSize)
- {
- _logger.LogWarning("Some fonts will not be sent due to size limitations");
- yield break;
- }
-
- yield return fontFile;
+ _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;
- }
}
+ 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)
+ /// <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]
+ [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 encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var fallbackFontPath = encodingOptions.FallbackFontPath;
+ var fontFile = _fileSystem.GetFiles(fallbackFontPath)
+ .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+ var fileSize = fontFile?.Length;
- if (!string.IsNullOrEmpty(fallbackFontPath))
+ if (fontFile is not null && fileSize is not null && fileSize > 0)
{
- 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");
- }
+ _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize);
+ return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
}
else
{
- _logger.LogWarning("The path of fallback font folder has not been set");
- encodingOptions.EnableFallbackFont = false;
+ _logger.LogWarning("The selected font is null or empty");
}
-
- // returning HTTP 204 will break the SubtitlesOctopus
- return Ok();
}
+ else
+ {
+ _logger.LogWarning("The path of fallback font folder has not been set");
+ encodingOptions.EnableFallbackFont = false;
+ }
+
+ // returning HTTP 204 will break the SubtitlesOctopus
+ return Ok();
}
}
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index 1cf528153..5b808f257 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
@@ -13,80 +12,79 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The suggestions controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class SuggestionsController : BaseJellyfinApiController
{
+ private readonly IDtoService _dtoService;
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+
/// <summary>
- /// The suggestions controller.
+ /// Initializes a new instance of the <see cref="SuggestionsController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class SuggestionsController : BaseJellyfinApiController
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public SuggestionsController(
+ IDtoService dtoService,
+ IUserManager userManager,
+ ILibraryManager libraryManager)
{
- private readonly IDtoService _dtoService;
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
+ _dtoService = dtoService;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="SuggestionsController"/> class.
- /// </summary>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public SuggestionsController(
- IDtoService dtoService,
- IUserManager userManager,
- ILibraryManager libraryManager)
- {
- _dtoService = dtoService;
- _userManager = userManager;
- _libraryManager = libraryManager;
- }
+ /// <summary>
+ /// Gets suggestions.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="mediaType">The media types.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="startIndex">Optional. The start index.</param>
+ /// <param name="limit">Optional. The limit.</param>
+ /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param>
+ /// <response code="200">Suggestions returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns>
+ [HttpGet("Users/{userId}/Suggestions")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
+ [FromRoute, Required] Guid userId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool enableTotalRecordCount = false)
+ {
+ var user = userId.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId);
- /// <summary>
- /// Gets suggestions.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="mediaType">The media types.</param>
- /// <param name="type">The type.</param>
- /// <param name="startIndex">Optional. The start index.</param>
- /// <param name="limit">Optional. The limit.</param>
- /// <param name="enableTotalRecordCount">Whether to enable the total record count.</param>
- /// <response code="200">Suggestions returned.</response>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the suggestions.</returns>
- [HttpGet("Users/{userId}/Suggestions")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
- [FromRoute, Required] Guid userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool enableTotalRecordCount = false)
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
{
- var user = userId.Equals(default)
- ? null
- : _userManager.GetUserById(userId);
-
- var dtoOptions = new DtoOptions().AddClientFields(User);
- var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
- {
- OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
- MediaTypes = mediaType,
- IncludeItemTypes = type,
- IsVirtualItem = false,
- StartIndex = startIndex,
- Limit = limit,
- DtoOptions = dtoOptions,
- EnableTotalRecordCount = enableTotalRecordCount,
- Recursive = true
- });
+ OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
+ MediaTypes = mediaType,
+ IncludeItemTypes = type,
+ IsVirtualItem = false,
+ StartIndex = startIndex,
+ Limit = limit,
+ DtoOptions = dtoOptions,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ Recursive = true
+ });
- var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
+ var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
- return new QueryResult<BaseItemDto>(
- startIndex,
- result.TotalRecordCount,
- dtoList);
- }
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ result.TotalRecordCount,
+ dtoList);
}
}
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index 99347246e..23abba7dc 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -16,409 +16,408 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The sync play controller.
+/// </summary>
+[Authorize(Policy = Policies.SyncPlayHasAccess)]
+public class SyncPlayController : BaseJellyfinApiController
{
+ private readonly ISessionManager _sessionManager;
+ private readonly ISyncPlayManager _syncPlayManager;
+ private readonly IUserManager _userManager;
+
/// <summary>
- /// The sync play controller.
+ /// Initializes a new instance of the <see cref="SyncPlayController"/> class.
/// </summary>
- [Authorize(Policy = Policies.SyncPlayHasAccess)]
- public class SyncPlayController : BaseJellyfinApiController
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public SyncPlayController(
+ ISessionManager sessionManager,
+ ISyncPlayManager syncPlayManager,
+ IUserManager userManager)
{
- private readonly ISessionManager _sessionManager;
- private readonly ISyncPlayManager _syncPlayManager;
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SyncPlayController"/> class.
- /// </summary>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- /// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public SyncPlayController(
- ISessionManager sessionManager,
- ISyncPlayManager syncPlayManager,
- IUserManager userManager)
- {
- _sessionManager = sessionManager;
- _syncPlayManager = syncPlayManager;
- _userManager = userManager;
- }
+ _sessionManager = sessionManager;
+ _syncPlayManager = syncPlayManager;
+ _userManager = userManager;
+ }
- /// <summary>
- /// Create a new SyncPlay group.
- /// </summary>
- /// <param name="requestData">The settings of the new group.</param>
- /// <response code="204">New group created.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("New")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayCreateGroup)]
- public async Task<ActionResult> SyncPlayCreateGroup(
- [FromBody, Required] NewGroupRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
- _syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Create a new SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The settings of the new group.</param>
+ /// <response code="204">New group created.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("New")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayCreateGroup)]
+ public async Task<ActionResult> SyncPlayCreateGroup(
+ [FromBody, Required] NewGroupRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
+ _syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Join an existing SyncPlay group.
- /// </summary>
- /// <param name="requestData">The group to join.</param>
- /// <response code="204">Group join successful.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Join")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayJoinGroup)]
- public async Task<ActionResult> SyncPlayJoinGroup(
- [FromBody, Required] JoinGroupRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new JoinGroupRequest(requestData.GroupId);
- _syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Join an existing SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The group to join.</param>
+ /// <response code="204">Group join successful.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Join")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayJoinGroup)]
+ public async Task<ActionResult> SyncPlayJoinGroup(
+ [FromBody, Required] JoinGroupRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new JoinGroupRequest(requestData.GroupId);
+ _syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Leave the joined SyncPlay group.
- /// </summary>
- /// <response code="204">Group leave successful.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Leave")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayLeaveGroup()
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new LeaveGroupRequest();
- _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Leave the joined SyncPlay group.
+ /// </summary>
+ /// <response code="204">Group leave successful.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Leave")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayLeaveGroup()
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new LeaveGroupRequest();
+ _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Gets all SyncPlay groups.
- /// </summary>
- /// <response code="200">Groups returned.</response>
- /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
- [HttpGet("List")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [Authorize(Policy = Policies.SyncPlayJoinGroup)]
- public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups()
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new ListGroupsRequest();
- return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable());
- }
+ /// <summary>
+ /// Gets all SyncPlay groups.
+ /// </summary>
+ /// <response code="200">Groups returned.</response>
+ /// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
+ [HttpGet("List")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [Authorize(Policy = Policies.SyncPlayJoinGroup)]
+ public async Task<ActionResult<IEnumerable<GroupInfoDto>>> SyncPlayGetGroups()
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new ListGroupsRequest();
+ return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable());
+ }
- /// <summary>
- /// Request to set new playlist in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new playlist to play in the group.</param>
- /// <response code="204">Queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("SetNewQueue")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySetNewQueue(
- [FromBody, Required] PlayRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new PlayGroupRequest(
- requestData.PlayingQueue,
- requestData.PlayingItemPosition,
- requestData.StartPositionTicks);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to set new playlist in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new playlist to play in the group.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetNewQueue")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySetNewQueue(
+ [FromBody, Required] PlayRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new PlayGroupRequest(
+ requestData.PlayingQueue,
+ requestData.PlayingItemPosition,
+ requestData.StartPositionTicks);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to change playlist item in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new item to play.</param>
- /// <response code="204">Queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("SetPlaylistItem")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySetPlaylistItem(
- [FromBody, Required] SetPlaylistItemRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to change playlist item in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new item to play.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetPlaylistItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySetPlaylistItem(
+ [FromBody, Required] SetPlaylistItemRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to remove items from the playlist in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The items to remove.</param>
- /// <response code="204">Queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("RemoveFromPlaylist")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayRemoveFromPlaylist(
- [FromBody, Required] RemoveFromPlaylistRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to remove items from the playlist in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The items to remove.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("RemoveFromPlaylist")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayRemoveFromPlaylist(
+ [FromBody, Required] RemoveFromPlaylistRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds, requestData.ClearPlaylist, requestData.ClearPlayingItem);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to move an item in the playlist in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new position for the item.</param>
- /// <response code="204">Queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("MovePlaylistItem")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayMovePlaylistItem(
- [FromBody, Required] MovePlaylistItemRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to move an item in the playlist in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new position for the item.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("MovePlaylistItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayMovePlaylistItem(
+ [FromBody, Required] MovePlaylistItemRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to queue items to the playlist of a SyncPlay group.
- /// </summary>
- /// <param name="requestData">The items to add.</param>
- /// <response code="204">Queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Queue")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayQueue(
- [FromBody, Required] QueueRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to queue items to the playlist of a SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The items to add.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Queue")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayQueue(
+ [FromBody, Required] QueueRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request unpause in SyncPlay group.
- /// </summary>
- /// <response code="204">Unpause update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Unpause")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayUnpause()
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new UnpauseGroupRequest();
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request unpause in SyncPlay group.
+ /// </summary>
+ /// <response code="204">Unpause update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Unpause")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayUnpause()
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new UnpauseGroupRequest();
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request pause in SyncPlay group.
- /// </summary>
- /// <response code="204">Pause update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Pause")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayPause()
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new PauseGroupRequest();
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request pause in SyncPlay group.
+ /// </summary>
+ /// <response code="204">Pause update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Pause")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayPause()
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new PauseGroupRequest();
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request stop in SyncPlay group.
- /// </summary>
- /// <response code="204">Stop update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Stop")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayStop()
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new StopGroupRequest();
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request stop in SyncPlay group.
+ /// </summary>
+ /// <response code="204">Stop update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Stop")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayStop()
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new StopGroupRequest();
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request seek in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new playback position.</param>
- /// <response code="204">Seek update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Seek")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySeek(
- [FromBody, Required] SeekRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request seek in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new playback position.</param>
+ /// <response code="204">Seek update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Seek")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySeek(
+ [FromBody, Required] SeekRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Notify SyncPlay group that member is buffering.
- /// </summary>
- /// <param name="requestData">The player status.</param>
- /// <response code="204">Group state update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Buffering")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayBuffering(
- [FromBody, Required] BufferRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new BufferGroupRequest(
- requestData.When,
- requestData.PositionTicks,
- requestData.IsPlaying,
- requestData.PlaylistItemId);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Notify SyncPlay group that member is buffering.
+ /// </summary>
+ /// <param name="requestData">The player status.</param>
+ /// <response code="204">Group state update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Buffering")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayBuffering(
+ [FromBody, Required] BufferRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new BufferGroupRequest(
+ requestData.When,
+ requestData.PositionTicks,
+ requestData.IsPlaying,
+ requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Notify SyncPlay group that member is ready for playback.
- /// </summary>
- /// <param name="requestData">The player status.</param>
- /// <response code="204">Group state update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Ready")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayReady(
- [FromBody, Required] ReadyRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new ReadyGroupRequest(
- requestData.When,
- requestData.PositionTicks,
- requestData.IsPlaying,
- requestData.PlaylistItemId);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Notify SyncPlay group that member is ready for playback.
+ /// </summary>
+ /// <param name="requestData">The player status.</param>
+ /// <response code="204">Group state update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Ready")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayReady(
+ [FromBody, Required] ReadyRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new ReadyGroupRequest(
+ requestData.When,
+ requestData.PositionTicks,
+ requestData.IsPlaying,
+ requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request SyncPlay group to ignore member during group-wait.
- /// </summary>
- /// <param name="requestData">The settings to set.</param>
- /// <response code="204">Member state updated.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("SetIgnoreWait")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySetIgnoreWait(
- [FromBody, Required] IgnoreWaitRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request SyncPlay group to ignore member during group-wait.
+ /// </summary>
+ /// <param name="requestData">The settings to set.</param>
+ /// <response code="204">Member state updated.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetIgnoreWait")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySetIgnoreWait(
+ [FromBody, Required] IgnoreWaitRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request next item in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The current item information.</param>
- /// <response code="204">Next item update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("NextItem")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayNextItem(
- [FromBody, Required] NextItemRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request next item in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The current item information.</param>
+ /// <response code="204">Next item update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("NextItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayNextItem(
+ [FromBody, Required] NextItemRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request previous item in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The current item information.</param>
- /// <response code="204">Previous item update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("PreviousItem")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlayPreviousItem(
- [FromBody, Required] PreviousItemRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request previous item in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The current item information.</param>
+ /// <response code="204">Previous item update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("PreviousItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlayPreviousItem(
+ [FromBody, Required] PreviousItemRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to set repeat mode in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new repeat mode.</param>
- /// <response code="204">Play queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("SetRepeatMode")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySetRepeatMode(
- [FromBody, Required] SetRepeatModeRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to set repeat mode in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new repeat mode.</param>
+ /// <response code="204">Play queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetRepeatMode")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySetRepeatMode(
+ [FromBody, Required] SetRepeatModeRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Request to set shuffle mode in SyncPlay group.
- /// </summary>
- /// <param name="requestData">The new shuffle mode.</param>
- /// <response code="204">Play queue update sent to all group members.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("SetShuffleMode")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [Authorize(Policy = Policies.SyncPlayIsInGroup)]
- public async Task<ActionResult> SyncPlaySetShuffleMode(
- [FromBody, Required] SetShuffleModeRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Request to set shuffle mode in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new shuffle mode.</param>
+ /// <response code="204">Play queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetShuffleMode")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Authorize(Policy = Policies.SyncPlayIsInGroup)]
+ public async Task<ActionResult> SyncPlaySetShuffleMode(
+ [FromBody, Required] SetShuffleModeRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
- /// <summary>
- /// Update session ping.
- /// </summary>
- /// <param name="requestData">The new ping.</param>
- /// <response code="204">Ping updated.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Ping")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> SyncPlayPing(
- [FromBody, Required] PingRequestDto requestData)
- {
- var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var syncPlayRequest = new PingGroupRequest(requestData.Ping);
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- return NoContent();
- }
+ /// <summary>
+ /// Update session ping.
+ /// </summary>
+ /// <param name="requestData">The new ping.</param>
+ /// <response code="204">Ping updated.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Ping")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task<ActionResult> SyncPlayPing(
+ [FromBody, Required] PingRequestDto requestData)
+ {
+ var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
+ var syncPlayRequest = new PingGroupRequest(requestData.Ping);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 2d594293e..4ab705f40 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -20,204 +20,203 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The system controller.
+/// </summary>
+public class SystemController : BaseJellyfinApiController
{
+ private readonly IServerApplicationHost _appHost;
+ private readonly IApplicationPaths _appPaths;
+ private readonly IFileSystem _fileSystem;
+ private readonly INetworkManager _network;
+ private readonly ILogger<SystemController> _logger;
+
/// <summary>
- /// The system controller.
+ /// Initializes a new instance of the <see cref="SystemController"/> class.
/// </summary>
- public class SystemController : BaseJellyfinApiController
+ /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
+ /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
+ /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
+ /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
+ public SystemController(
+ IServerConfigurationManager serverConfigurationManager,
+ IServerApplicationHost appHost,
+ IFileSystem fileSystem,
+ INetworkManager network,
+ ILogger<SystemController> logger)
{
- private readonly IServerApplicationHost _appHost;
- private readonly IApplicationPaths _appPaths;
- private readonly IFileSystem _fileSystem;
- private readonly INetworkManager _network;
- private readonly ILogger<SystemController> _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SystemController"/> class.
- /// </summary>
- /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
- /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
- /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
- /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
- public SystemController(
- IServerConfigurationManager serverConfigurationManager,
- IServerApplicationHost appHost,
- IFileSystem fileSystem,
- INetworkManager network,
- ILogger<SystemController> logger)
- {
- _appPaths = serverConfigurationManager.ApplicationPaths;
- _appHost = appHost;
- _fileSystem = fileSystem;
- _network = network;
- _logger = logger;
- }
+ _appPaths = serverConfigurationManager.ApplicationPaths;
+ _appHost = appHost;
+ _fileSystem = fileSystem;
+ _network = network;
+ _logger = logger;
+ }
- /// <summary>
- /// Gets information about the server.
- /// </summary>
- /// <response code="200">Information retrieved.</response>
- /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
- [HttpGet("Info")]
- [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<SystemInfo> GetSystemInfo()
- {
- return _appHost.GetSystemInfo(Request);
- }
+ /// <summary>
+ /// Gets information about the server.
+ /// </summary>
+ /// <response code="200">Information retrieved.</response>
+ /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
+ [HttpGet("Info")]
+ [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<SystemInfo> GetSystemInfo()
+ {
+ return _appHost.GetSystemInfo(Request);
+ }
- /// <summary>
- /// Gets public information about the server.
- /// </summary>
- /// <response code="200">Information retrieved.</response>
- /// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns>
- [HttpGet("Info/Public")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
- {
- return _appHost.GetPublicSystemInfo(Request);
- }
+ /// <summary>
+ /// 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>
+ /// 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()
+ /// <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 () =>
{
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- _appHost.Restart();
- });
- return NoContent();
- }
+ await Task.Delay(100).ConfigureAwait(false);
+ _appHost.Restart();
+ });
+ return NoContent();
+ }
- /// <summary>
- /// Shuts down the application.
- /// </summary>
- /// <response code="204">Server shut down.</response>
- /// <returns>No content. Server shut down.</returns>
- [HttpPost("Shutdown")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult ShutdownApplication()
+ /// <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 () =>
{
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- await _appHost.Shutdown().ConfigureAwait(false);
- });
- return NoContent();
- }
+ 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;
- /// <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()
+ try
{
- 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;
+ files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false);
}
-
- /// <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()
+ catch (IOException ex)
{
- return new EndPointInfo
- {
- IsLocal = HttpContext.IsLocal(),
- IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())
- };
+ _logger.LogError(ex, "Error getting logs");
+ files = Enumerable.Empty<FileSystemMetadata>();
}
- /// <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 result = files.Select(i => new LogFile
{
- var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
- .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
+ 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();
- // 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");
- }
+ return result;
+ }
- /// <summary>
- /// Gets wake on lan information.
- /// </summary>
- /// <response code="200">Information retrieved.</response>
- /// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns>
- [HttpGet("WakeOnLanInfo")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [Obsolete("This endpoint is obsolete.")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
+ /// <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]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<EndPointInfo> GetEndpointInfo()
+ {
+ return new EndPointInfo
{
- var result = _network.GetMacAddresses()
- .Select(i => new WakeOnLanInfo(i));
- return Ok(result);
- }
+ 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]
+ [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);
}
}
diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs
index e7c5a7125..d7304cf42 100644
--- a/Jellyfin.Api/Controllers/TimeSyncController.cs
+++ b/Jellyfin.Api/Controllers/TimeSyncController.cs
@@ -3,32 +3,31 @@ using MediaBrowser.Model.SyncPlay;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The time sync controller.
+/// </summary>
+[Route("")]
+public class TimeSyncController : BaseJellyfinApiController
{
/// <summary>
- /// The time sync controller.
+ /// Gets the current UTC time.
/// </summary>
- [Route("")]
- public class TimeSyncController : BaseJellyfinApiController
+ /// <response code="200">Time returned.</response>
+ /// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns>
+ [HttpGet("GetUtcTime")]
+ [ProducesResponseType(statusCode: StatusCodes.Status200OK)]
+ public ActionResult<UtcTimeResponse> GetUtcTime()
{
- /// <summary>
- /// Gets the current UTC time.
- /// </summary>
- /// <response code="200">Time returned.</response>
- /// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns>
- [HttpGet("GetUtcTime")]
- [ProducesResponseType(statusCode: StatusCodes.Status200OK)]
- public ActionResult<UtcTimeResponse> GetUtcTime()
- {
- // Important to keep the following line at the beginning
- var requestReceptionTime = DateTime.UtcNow;
+ // Important to keep the following line at the beginning
+ var requestReceptionTime = DateTime.UtcNow;
- // Important to keep the following line at the end
- var responseTransmissionTime = DateTime.UtcNow;
+ // Important to keep the following line at the end
+ var responseTransmissionTime = DateTime.UtcNow;
- // Implementing NTP on such a high level results in this useless
- // information being sent. On the other hand it enables future additions.
- return new UtcTimeResponse(requestReceptionTime, responseTransmissionTime);
- }
+ // Implementing NTP on such a high level results in this useless
+ // information being sent. On the other hand it enables future additions.
+ return new UtcTimeResponse(requestReceptionTime, responseTransmissionTime);
}
}
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 53a839e43..b5b640620 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -1,6 +1,4 @@
using System;
-using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto;
@@ -10,290 +8,289 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The trailers controller.
+/// </summary>
+[Authorize]
+public class TrailersController : BaseJellyfinApiController
{
+ private readonly ItemsController _itemsController;
+
/// <summary>
- /// The trailers controller.
+ /// Initializes a new instance of the <see cref="TrailersController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class TrailersController : BaseJellyfinApiController
+ /// <param name="itemsController">Instance of <see cref="ItemsController"/>.</param>
+ public TrailersController(ItemsController itemsController)
{
- private readonly ItemsController _itemsController;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="TrailersController"/> class.
- /// </summary>
- /// <param name="itemsController">Instance of <see cref="ItemsController"/>.</param>
- public TrailersController(ItemsController itemsController)
- {
- _itemsController = itemsController;
- }
+ _itemsController = itemsController;
+ }
- /// <summary>
- /// Finds movies and trailers similar to a given trailer.
- /// </summary>
- /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
- /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
- /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
- /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
- /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
- /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
- /// <param name="hasTrailer">Optional filter by items with trailers.</param>
- /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
- /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
- /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
- /// <param name="isHd">Optional filter by items that are HD or not.</param>
- /// <param name="is4K">Optional filter by items that are 4K or not.</param>
- /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
- /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
- /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
- /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
- /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
- /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
- /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
- /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
- /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
- /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
- /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
- /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
- /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
- /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
- /// <param name="isMovie">Optional filter for live tv movies.</param>
- /// <param name="isSeries">Optional filter for live tv series.</param>
- /// <param name="isNews">Optional filter for live tv news.</param>
- /// <param name="isKids">Optional filter for live tv kids.</param>
- /// <param name="isSports">Optional filter for live tv sports.</param>
- /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
- /// <param name="searchTerm">Optional. Filter based on a search term.</param>
- /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
- /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
- /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
- /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
- /// <param name="enableUserData">Optional, include user data.</param>
- /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
- /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
- /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
- /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
- /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
- /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
- /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
- /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
- /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
- /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
- /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
- /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
- /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
- /// <param name="isLocked">Optional filter by items that are locked.</param>
- /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
- /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
- /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
- /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
- /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
- /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
- /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
- /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
- /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
- /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
- /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
- /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
- /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
- /// <param name="enableImages">Optional, include image information in output.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetTrailers(
- [FromQuery] Guid? userId,
- [FromQuery] string? maxOfficialRating,
- [FromQuery] bool? hasThemeSong,
- [FromQuery] bool? hasThemeVideo,
- [FromQuery] bool? hasSubtitles,
- [FromQuery] bool? hasSpecialFeature,
- [FromQuery] bool? hasTrailer,
- [FromQuery] Guid? adjacentTo,
- [FromQuery] int? parentIndexNumber,
- [FromQuery] bool? hasParentalRating,
- [FromQuery] bool? isHd,
- [FromQuery] bool? is4K,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
- [FromQuery] bool? isMissing,
- [FromQuery] bool? isUnaired,
- [FromQuery] double? minCommunityRating,
- [FromQuery] double? minCriticRating,
- [FromQuery] DateTime? minPremiereDate,
- [FromQuery] DateTime? minDateLastSaved,
- [FromQuery] DateTime? minDateLastSavedForUser,
- [FromQuery] DateTime? maxPremiereDate,
- [FromQuery] bool? hasOverview,
- [FromQuery] bool? hasImdbId,
- [FromQuery] bool? hasTmdbId,
- [FromQuery] bool? hasTvdbId,
- [FromQuery] bool? isMovie,
- [FromQuery] bool? isSeries,
- [FromQuery] bool? isNews,
- [FromQuery] bool? isKids,
- [FromQuery] bool? isSports,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool? recursive,
- [FromQuery] string? searchTerm,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery] bool? isPlayed,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
- [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? person,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
- [FromQuery] string? minOfficialRating,
- [FromQuery] bool? isLocked,
- [FromQuery] bool? isPlaceHolder,
- [FromQuery] bool? hasOfficialRating,
- [FromQuery] bool? collapseBoxSetItems,
- [FromQuery] int? minWidth,
- [FromQuery] int? minHeight,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] bool? is3D,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
- [FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool? enableImages = true)
- {
- var includeItemTypes = new[] { BaseItemKind.Trailer };
+ /// <summary>
+ /// Finds movies and trailers similar to a given trailer.
+ /// </summary>
+ /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
+ /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
+ /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
+ /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
+ /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
+ /// <param name="hasTrailer">Optional filter by items with trailers.</param>
+ /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
+ /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
+ /// <param name="isHd">Optional filter by items that are HD or not.</param>
+ /// <param name="is4K">Optional filter by items that are 4K or not.</param>
+ /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
+ /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
+ /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+ /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
+ /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
+ /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
+ /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
+ /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
+ /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
+ /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
+ /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
+ /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
+ /// <param name="isMovie">Optional filter for live tv movies.</param>
+ /// <param name="isSeries">Optional filter for live tv series.</param>
+ /// <param name="isNews">Optional filter for live tv news.</param>
+ /// <param name="isKids">Optional filter for live tv kids.</param>
+ /// <param name="isSports">Optional filter for live tv sports.</param>
+ /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
+ /// <param name="searchTerm">Optional. Filter based on a search term.</param>
+ /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+ /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
+ /// <param name="enableUserData">Optional, include user data.</param>
+ /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
+ /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
+ /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
+ /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+ /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
+ /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
+ /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
+ /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
+ /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
+ /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
+ /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
+ /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
+ /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
+ /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="isLocked">Optional filter by items that are locked.</param>
+ /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
+ /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
+ /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
+ /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
+ /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
+ /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
+ /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
+ /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
+ /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
+ /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+ /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+ /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+ /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+ /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetTrailers(
+ [FromQuery] Guid? userId,
+ [FromQuery] string? maxOfficialRating,
+ [FromQuery] bool? hasThemeSong,
+ [FromQuery] bool? hasThemeVideo,
+ [FromQuery] bool? hasSubtitles,
+ [FromQuery] bool? hasSpecialFeature,
+ [FromQuery] bool? hasTrailer,
+ [FromQuery] Guid? adjacentTo,
+ [FromQuery] int? parentIndexNumber,
+ [FromQuery] bool? hasParentalRating,
+ [FromQuery] bool? isHd,
+ [FromQuery] bool? is4K,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
+ [FromQuery] bool? isMissing,
+ [FromQuery] bool? isUnaired,
+ [FromQuery] double? minCommunityRating,
+ [FromQuery] double? minCriticRating,
+ [FromQuery] DateTime? minPremiereDate,
+ [FromQuery] DateTime? minDateLastSaved,
+ [FromQuery] DateTime? minDateLastSavedForUser,
+ [FromQuery] DateTime? maxPremiereDate,
+ [FromQuery] bool? hasOverview,
+ [FromQuery] bool? hasImdbId,
+ [FromQuery] bool? hasTmdbId,
+ [FromQuery] bool? hasTvdbId,
+ [FromQuery] bool? isMovie,
+ [FromQuery] bool? isSeries,
+ [FromQuery] bool? isNews,
+ [FromQuery] bool? isKids,
+ [FromQuery] bool? isSports,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? recursive,
+ [FromQuery] string? searchTerm,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery] bool? isPlayed,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
+ [FromQuery] string? minOfficialRating,
+ [FromQuery] bool? isLocked,
+ [FromQuery] bool? isPlaceHolder,
+ [FromQuery] bool? hasOfficialRating,
+ [FromQuery] bool? collapseBoxSetItems,
+ [FromQuery] int? minWidth,
+ [FromQuery] int? minHeight,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] bool? is3D,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true)
+ {
+ var includeItemTypes = new[] { BaseItemKind.Trailer };
- return _itemsController
- .GetItems(
- userId,
- maxOfficialRating,
- hasThemeSong,
- hasThemeVideo,
- hasSubtitles,
- hasSpecialFeature,
- hasTrailer,
- adjacentTo,
- parentIndexNumber,
- hasParentalRating,
- isHd,
- is4K,
- locationTypes,
- excludeLocationTypes,
- isMissing,
- isUnaired,
- minCommunityRating,
- minCriticRating,
- minPremiereDate,
- minDateLastSaved,
- minDateLastSavedForUser,
- maxPremiereDate,
- hasOverview,
- hasImdbId,
- hasTmdbId,
- hasTvdbId,
- isMovie,
- isSeries,
- isNews,
- isKids,
- isSports,
- excludeItemIds,
- startIndex,
- limit,
- recursive,
- searchTerm,
- sortOrder,
- parentId,
- fields,
- excludeItemTypes,
- includeItemTypes,
- filters,
- isFavorite,
- mediaTypes,
- imageTypes,
- sortBy,
- isPlayed,
- genres,
- officialRatings,
- tags,
- years,
- enableUserData,
- imageTypeLimit,
- enableImageTypes,
- person,
- personIds,
- personTypes,
- studios,
- artists,
- excludeArtistIds,
- artistIds,
- albumArtistIds,
- contributingArtistIds,
- albums,
- albumIds,
- ids,
- videoTypes,
- minOfficialRating,
- isLocked,
- isPlaceHolder,
- hasOfficialRating,
- collapseBoxSetItems,
- minWidth,
- minHeight,
- maxWidth,
- maxHeight,
- is3D,
- seriesStatus,
- nameStartsWithOrGreater,
- nameStartsWith,
- nameLessThan,
- studioIds,
- genreIds,
- enableTotalRecordCount,
- enableImages);
- }
+ return _itemsController
+ .GetItems(
+ userId,
+ maxOfficialRating,
+ hasThemeSong,
+ hasThemeVideo,
+ hasSubtitles,
+ hasSpecialFeature,
+ hasTrailer,
+ adjacentTo,
+ parentIndexNumber,
+ hasParentalRating,
+ isHd,
+ is4K,
+ locationTypes,
+ excludeLocationTypes,
+ isMissing,
+ isUnaired,
+ minCommunityRating,
+ minCriticRating,
+ minPremiereDate,
+ minDateLastSaved,
+ minDateLastSavedForUser,
+ maxPremiereDate,
+ hasOverview,
+ hasImdbId,
+ hasTmdbId,
+ hasTvdbId,
+ isMovie,
+ isSeries,
+ isNews,
+ isKids,
+ isSports,
+ excludeItemIds,
+ startIndex,
+ limit,
+ recursive,
+ searchTerm,
+ sortOrder,
+ parentId,
+ fields,
+ excludeItemTypes,
+ includeItemTypes,
+ filters,
+ isFavorite,
+ mediaTypes,
+ imageTypes,
+ sortBy,
+ isPlayed,
+ genres,
+ officialRatings,
+ tags,
+ years,
+ enableUserData,
+ imageTypeLimit,
+ enableImageTypes,
+ person,
+ personIds,
+ personTypes,
+ studios,
+ artists,
+ excludeArtistIds,
+ artistIds,
+ albumArtistIds,
+ contributingArtistIds,
+ albums,
+ albumIds,
+ ids,
+ videoTypes,
+ minOfficialRating,
+ isLocked,
+ isPlaceHolder,
+ hasOfficialRating,
+ collapseBoxSetItems,
+ minWidth,
+ minHeight,
+ maxWidth,
+ maxHeight,
+ is3D,
+ seriesStatus,
+ nameStartsWithOrGreater,
+ nameStartsWith,
+ nameLessThan,
+ studioIds,
+ genreIds,
+ enableTotalRecordCount,
+ enableImages);
}
}
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 7f4f4d077..b0760f97c 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
@@ -19,366 +18,365 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The tv shows controller.
+/// </summary>
+[Route("Shows")]
+[Authorize]
+public class TvShowsController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IDtoService _dtoService;
+ private readonly ITVSeriesManager _tvSeriesManager;
+
/// <summary>
- /// The tv shows controller.
+ /// Initializes a new instance of the <see cref="TvShowsController"/> class.
/// </summary>
- [Route("Shows")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class TvShowsController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="tvSeriesManager">Instance of the <see cref="ITVSeriesManager"/> interface.</param>
+ public TvShowsController(
+ IUserManager userManager,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ ITVSeriesManager tvSeriesManager)
{
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IDtoService _dtoService;
- private readonly ITVSeriesManager _tvSeriesManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="TvShowsController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="tvSeriesManager">Instance of the <see cref="ITVSeriesManager"/> interface.</param>
- public TvShowsController(
- IUserManager userManager,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- ITVSeriesManager tvSeriesManager)
- {
- _userManager = userManager;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _tvSeriesManager = tvSeriesManager;
- }
-
- /// <summary>
- /// Gets a list of next up episodes.
- /// </summary>
- /// <param name="userId">The user id of the user to get the next up episodes for.</param>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="seriesId">Optional. Filter by series id.</param>
- /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="enableImages">Optional. Include image information in output.</param>
- /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
- /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
- /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
- /// <param name="enableRewatching">Whether to include watched episode in next up results.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
- [HttpGet("NextUp")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] Guid? seriesId,
- [FromQuery] Guid? parentId,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] bool? enableUserData,
- [FromQuery] DateTime? nextUpDateCutoff,
- [FromQuery] bool enableTotalRecordCount = true,
- [FromQuery] bool disableFirstEpisode = false,
- [FromQuery] bool enableRewatching = false)
- {
- var options = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
-
- 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;
+ _userManager = userManager;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _tvSeriesManager = tvSeriesManager;
+ }
- var options = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ /// <summary>
+ /// Gets a list of next up episodes.
+ /// </summary>
+ /// <param name="userId">The user id of the user to get the next up episodes for.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="seriesId">Optional. Filter by series id.</param>
+ /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</param>
+ /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
+ /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
+ /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
+ /// <param name="enableRewatching">Whether to include watched episode in next up results.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
+ [HttpGet("NextUp")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] Guid? seriesId,
+ [FromQuery] Guid? parentId,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] DateTime? nextUpDateCutoff,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool disableFirstEpisode = false,
+ [FromQuery] bool enableRewatching = false)
+ {
+ var options = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ var result = _tvSeriesManager.GetNextUp(
+ new NextUpQuery
{
- IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
- MinPremiereDate = minPremiereDate,
- StartIndex = startIndex,
Limit = limit,
- ParentId = parentIdGuid,
- Recursive = true,
- DtoOptions = options
- });
+ ParentId = parentId,
+ SeriesId = seriesId,
+ StartIndex = startIndex,
+ UserId = userId ?? Guid.Empty,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ DisableFirstEpisode = disableFirstEpisode,
+ NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
+ EnableRewatching = enableRewatching
+ },
+ options);
+
+ var 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);
+ }
- var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user);
+ /// <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);
- return new QueryResult<BaseItemDto>(
- startIndex,
- itemsResult.Count,
- returnItems);
- }
+ var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1);
- /// <summary>
- /// Gets episodes for a tv season.
- /// </summary>
- /// <param name="seriesId">The series id.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
- /// <param name="season">Optional filter by season number.</param>
- /// <param name="seasonId">Optional. Filter by season id.</param>
- /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
- /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
- /// <param name="startItemId">Optional. Skip through the list until a given item is found.</param>
- /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="enableImages">Optional, include image information in output.</param>
- /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
- [HttpGet("{seriesId}/Episodes")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
- [FromRoute, Required] Guid seriesId,
- [FromQuery] Guid? userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] int? season,
- [FromQuery] Guid? seasonId,
- [FromQuery] bool? isMissing,
- [FromQuery] Guid? adjacentTo,
- [FromQuery] Guid? startItemId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] bool? enableUserData,
- [FromQuery] string? sortBy)
- {
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ var parentIdGuid = parentId ?? Guid.Empty;
- List<BaseItem> episodes;
+ var options = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var dtoOptions = 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,
+ ParentId = parentIdGuid,
+ Recursive = true,
+ DtoOptions = options
+ });
+
+ var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user);
+
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ itemsResult.Count,
+ returnItems);
+ }
- if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
- {
- var item = _libraryManager.GetItemById(seasonId.Value);
- if (item is not Season seasonItem)
- {
- return NotFound("No season exists with Id " + seasonId);
- }
+ /// <summary>
+ /// Gets episodes for a tv season.
+ /// </summary>
+ /// <param name="seriesId">The series id.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+ /// <param name="season">Optional filter by season number.</param>
+ /// <param name="seasonId">Optional. Filter by season id.</param>
+ /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
+ /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="startItemId">Optional. Skip through the list until a given item is found.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
+ [HttpGet("{seriesId}/Episodes")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
+ [FromRoute, Required] Guid seriesId,
+ [FromQuery] Guid? userId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery] int? season,
+ [FromQuery] Guid? seasonId,
+ [FromQuery] bool? isMissing,
+ [FromQuery] Guid? adjacentTo,
+ [FromQuery] Guid? startItemId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? enableImages,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] string? sortBy)
+ {
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
- episodes = seasonItem.GetEpisodes(user, dtoOptions);
- }
- else if (season.HasValue) // Season number was supplied. Get episodes by season number
- {
- if (_libraryManager.GetItemById(seriesId) is not Series series)
- {
- return NotFound("Series not found");
- }
-
- var seasonItem = series
- .GetSeasons(user, dtoOptions)
- .FirstOrDefault(i => i.IndexNumber == season.Value);
-
- episodes = seasonItem is null ?
- new List<BaseItem>()
- : ((Season)seasonItem).GetEpisodes(user, dtoOptions);
- }
- else // No season number or season id was supplied. Returning all episodes.
- {
- if (_libraryManager.GetItemById(seriesId) is not Series series)
- {
- return NotFound("Series not found");
- }
+ List<BaseItem> episodes;
- episodes = series.GetEpisodes(user, dtoOptions).ToList();
- }
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- // Filter after the fact in case the ui doesn't want them
- if (isMissing.HasValue)
+ if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
+ {
+ var item = _libraryManager.GetItemById(seasonId.Value);
+ if (item is not Season seasonItem)
{
- var val = isMissing.Value;
- episodes = episodes
- .Where(i => ((Episode)i).IsMissingEpisode == val)
- .ToList();
+ return NotFound("No season exists with Id " + seasonId);
}
- if (startItemId.HasValue)
+ 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)
{
- episodes = episodes
- .SkipWhile(i => !startItemId.Value.Equals(i.Id))
- .ToList();
+ return NotFound("Series not found");
}
- // This must be the last filter
- if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default))
- {
- episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList();
- }
+ var seasonItem = series
+ .GetSeasons(user, dtoOptions)
+ .FirstOrDefault(i => i.IndexNumber == season.Value);
- if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
+ episodes = seasonItem is null ?
+ new List<BaseItem>()
+ : ((Season)seasonItem).GetEpisodes(user, dtoOptions);
+ }
+ else // No season number or season id was supplied. Returning all episodes.
+ {
+ if (_libraryManager.GetItemById(seriesId) is not Series series)
{
- episodes.Shuffle();
+ return NotFound("Series not found");
}
- var returnItems = episodes;
+ episodes = series.GetEpisodes(user, dtoOptions).ToList();
+ }
- if (startIndex.HasValue || limit.HasValue)
- {
- returnItems = ApplyPaging(episodes, startIndex, limit).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();
+ }
- var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
+ if (startItemId.HasValue)
+ {
+ episodes = episodes
+ .SkipWhile(i => !startItemId.Value.Equals(i.Id))
+ .ToList();
+ }
- return new QueryResult<BaseItemDto>(
- startIndex,
- episodes.Count,
- dtos);
+ // This must be the last filter
+ if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default))
+ {
+ episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList();
}
- /// <summary>
- /// Gets seasons for a tv series.
- /// </summary>
- /// <param name="seriesId">The series id.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
- /// <param name="isSpecialSeason">Optional. Filter by special season.</param>
- /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
- /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
- /// <param name="enableImages">Optional. Include image information in output.</param>
- /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
- [HttpGet("{seriesId}/Seasons")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
- [FromRoute, Required] Guid seriesId,
- [FromQuery] Guid? userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] bool? isSpecialSeason,
- [FromQuery] bool? isMissing,
- [FromQuery] Guid? adjacentTo,
- [FromQuery] bool? enableImages,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] bool? enableUserData)
+ if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ episodes.Shuffle();
+ }
- if (_libraryManager.GetItemById(seriesId) is not Series series)
- {
- return NotFound("Series not found");
- }
+ var returnItems = episodes;
- var seasons = series.GetItemList(new InternalItemsQuery(user)
- {
- IsMissing = isMissing,
- IsSpecialSeason = isSpecialSeason,
- AdjacentTo = adjacentTo
- });
+ if (startIndex.HasValue || limit.HasValue)
+ {
+ returnItems = ApplyPaging(episodes, startIndex, limit).ToList();
+ }
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
- var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ episodes.Count,
+ dtos);
+ }
- return new QueryResult<BaseItemDto>(returnItems);
+ /// <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");
}
- /// <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)
+ var seasons = series.GetItemList(new InternalItemsQuery(user)
{
- // Start at
- if (startIndex.HasValue)
- {
- items = items.Skip(startIndex.Value);
- }
+ IsMissing = isMissing,
+ IsSpecialSeason = isSpecialSeason,
+ AdjacentTo = adjacentTo
+ });
- // Return limit
- if (limit.HasValue)
- {
- items = items.Take(limit.Value);
- }
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user);
- return items;
+ 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;
}
}
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index d77126a35..345521597 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -5,7 +5,6 @@ using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -20,197 +19,164 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
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>
- /// The universal audio controller.
+ /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
/// </summary>
- [Route("")]
- public class UniversalAudioController : BaseJellyfinApiController
+ /// <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)
{
- private readonly ILibraryManager _libraryManager;
- private readonly ILogger<UniversalAudioController> _logger;
- private readonly MediaInfoHelper _mediaInfoHelper;
- private readonly AudioHelper _audioHelper;
- private readonly DynamicHlsHelper _dynamicHlsHelper;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _mediaInfoHelper = mediaInfoHelper;
+ _audioHelper = audioHelper;
+ _dynamicHlsHelper = dynamicHlsHelper;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
- /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
- /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
- /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
- public UniversalAudioController(
- ILibraryManager libraryManager,
- ILogger<UniversalAudioController> logger,
- MediaInfoHelper mediaInfoHelper,
- AudioHelper audioHelper,
- DynamicHlsHelper dynamicHlsHelper)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _mediaInfoHelper = mediaInfoHelper;
- _audioHelper = audioHelper;
- _dynamicHlsHelper = dynamicHlsHelper;
- }
+ /// <summary>
+ /// Gets an audio stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">Optional. The audio container.</param>
+ /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+ /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+ /// <param name="userId">Optional. The user id.</param>
+ /// <param name="audioCodec">Optional. The audio codec to transcode to.</param>
+ /// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
+ /// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
+ /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="transcodingContainer">Optional. The container to transcode to.</param>
+ /// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
+ /// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
+ /// <response code="200">Audio stream returned.</response>
+ /// <response code="302">Redirected to remote audio stream.</response>
+ /// <returns>A <see cref="Task"/> containing the audio file.</returns>
+ [HttpGet("Audio/{itemId}/universal")]
+ [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
+ [Authorize]
+ [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);
- /// <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)
+ if (!userId.HasValue || userId.Value.Equals(default))
{
- var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
-
- if (!userId.HasValue || userId.Value.Equals(default))
- {
- userId = User.GetUserId();
- }
-
- _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
-
- var info = await _mediaInfoHelper.GetPlaybackInfo(
- itemId,
- userId,
- mediaSourceId)
- .ConfigureAwait(false);
+ userId = User.GetUserId();
+ }
- // set device specific data
- var item = _libraryManager.GetItemById(itemId);
+ _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
- foreach (var sourceInfo in info.MediaSources)
- {
- _mediaInfoHelper.SetDeviceSpecificData(
- item,
- sourceInfo,
- deviceProfile,
- User,
- maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
- startTimeTicks ?? 0,
- mediaSourceId ?? string.Empty,
- null,
- null,
- maxAudioChannels,
- info.PlaySessionId!,
- userId ?? Guid.Empty,
- true,
- true,
- true,
- true,
- true,
- Request.HttpContext.GetNormalizedRemoteIp());
- }
+ var info = await _mediaInfoHelper.GetPlaybackInfo(
+ itemId,
+ userId,
+ mediaSourceId)
+ .ConfigureAwait(false);
- _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
+ // set device specific data
+ var item = _libraryManager.GetItemById(itemId);
- foreach (var source in info.MediaSources)
- {
- _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video);
- }
+ foreach (var sourceInfo in info.MediaSources)
+ {
+ _mediaInfoHelper.SetDeviceSpecificData(
+ item,
+ sourceInfo,
+ deviceProfile,
+ User,
+ maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
+ startTimeTicks ?? 0,
+ mediaSourceId ?? string.Empty,
+ null,
+ null,
+ maxAudioChannels,
+ info.PlaySessionId!,
+ userId ?? Guid.Empty,
+ true,
+ true,
+ true,
+ true,
+ true,
+ Request.HttpContext.GetNormalizedRemoteIp());
+ }
- var mediaSource = info.MediaSources[0];
- if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
- {
- return Redirect(mediaSource.Path);
- }
+ _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
- var isStatic = mediaSource.SupportsDirectStream;
- if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
- {
- // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
- // ffmpeg option -> file extension
- // mpegts -> ts
- // fmp4 -> mp4
- // TODO: remove this when we switch back to the segment muxer
- var supportedHlsContainers = new[] { "ts", "mp4" };
+ foreach (var source in info.MediaSources)
+ {
+ _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video);
+ }
- 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
- };
+ var mediaSource = info.MediaSources[0];
+ if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
+ {
+ return Redirect(mediaSource.Path);
+ }
- return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
- .ConfigureAwait(false);
- }
+ 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 audioStreamingDto = new StreamingRequestDto
+ var dynamicHlsRequestDto = new HlsAudioRequestDto
{
Id = itemId,
- Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
+ Container = ".m3u8",
Static = isStatic,
PlaySessionId = info.PlaySessionId,
+ // fallback to mpegts if device reports some weird value unsupported by hls
+ SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts",
MediaSourceId = mediaSourceId,
DeviceId = deviceId,
AudioCodec = audioCodec,
@@ -220,121 +186,153 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = maxAudioChannels,
- CopyTimestamps = true,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
StartTimeTicks = startTimeTicks,
- SubtitleMethod = SubtitleDeliveryMethod.Embed,
+ SubtitleMethod = SubtitleDeliveryMethod.Hls,
+ RequireAvc = false,
+ DeInterlace = false,
+ RequireNonAnamorphic = false,
+ EnableMpegtsM2TsMode = false,
TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
- Context = EncodingContext.Static
+ Context = EncodingContext.Static,
+ StreamOptions = new Dictionary<string, string>(),
+ EnableAdaptiveBitrateStreaming = true
};
- return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false);
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
+ .ConfigureAwait(false);
}
- private DeviceProfile GetDeviceProfile(
- string[] containers,
- string? transcodingContainer,
- string? audioCodec,
- string? transcodingProtocol,
- bool? breakOnNonKeyFrames,
- int? transcodingAudioChannels,
- int? maxAudioSampleRate,
- int? maxAudioBitDepth,
- int? maxAudioChannels)
+ var audioStreamingDto = new StreamingRequestDto
{
- var deviceProfile = new DeviceProfile();
+ Id = itemId,
+ Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
+ Static = isStatic,
+ PlaySessionId = info.PlaySessionId,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = true,
+ AllowAudioStreamCopy = true,
+ AllowVideoStreamCopy = true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames,
+ AudioSampleRate = maxAudioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = maxAudioChannels,
+ CopyTimestamps = true,
+ StartTimeTicks = startTimeTicks,
+ SubtitleMethod = SubtitleDeliveryMethod.Embed,
+ TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
+ Context = EncodingContext.Static
+ };
- int len = containers.Length;
- var directPlayProfiles = new DirectPlayProfile[len];
- for (int i = 0; i < len; i++)
- {
- var parts = containers[i].Split('|', StringSplitOptions.RemoveEmptyEntries);
+ return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false);
+ }
- var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
+ 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();
- directPlayProfiles[i] = new DirectPlayProfile
- {
- Type = DlnaProfileType.Audio,
- Container = parts[0],
- AudioCodec = audioCodecs
- };
- }
+ int len = containers.Length;
+ var directPlayProfiles = new DirectPlayProfile[len];
+ for (int i = 0; i < len; i++)
+ {
+ var parts = containers[i].Split('|', StringSplitOptions.RemoveEmptyEntries);
- deviceProfile.DirectPlayProfiles = directPlayProfiles;
+ var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
- deviceProfile.TranscodingProfiles = new[]
+ directPlayProfiles[i] = new DirectPlayProfile
{
- new TranscodingProfile
- {
- Type = DlnaProfileType.Audio,
- Context = EncodingContext.Streaming,
- Container = transcodingContainer ?? "mp3",
- AudioCodec = audioCodec ?? "mp3",
- Protocol = transcodingProtocol ?? "http",
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
- }
+ Type = DlnaProfileType.Audio,
+ Container = parts[0],
+ AudioCodec = audioCodecs
};
+ }
- var codecProfiles = new List<CodecProfile>();
- var conditions = new List<ProfileCondition>();
+ deviceProfile.DirectPlayProfiles = directPlayProfiles;
- if (maxAudioSampleRate.HasValue)
+ deviceProfile.TranscodingProfiles = new[]
+ {
+ new TranscodingProfile
{
- // codec profile
- conditions.Add(
- new ProfileCondition
- {
- Condition = ProfileConditionType.LessThanEqual,
- IsRequired = false,
- Property = ProfileConditionValue.AudioSampleRate,
- Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
- });
+ Type = DlnaProfileType.Audio,
+ Context = EncodingContext.Streaming,
+ Container = transcodingContainer ?? "mp3",
+ AudioCodec = audioCodec ?? "mp3",
+ Protocol = transcodingProtocol ?? "http",
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ MaxAudioChannels = transcodingAudioChannels?.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)
- });
- }
+ var codecProfiles = new List<CodecProfile>();
+ var conditions = new List<ProfileCondition>();
- if (maxAudioChannels.HasValue)
- {
- // codec profile
- conditions.Add(
- new ProfileCondition
- {
- Condition = ProfileConditionType.LessThanEqual,
- IsRequired = false,
- Property = ProfileConditionValue.AudioChannels,
- Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
- });
- }
+ if (maxAudioSampleRate.HasValue)
+ {
+ // codec profile
+ conditions.Add(
+ new ProfileCondition
+ {
+ Condition = ProfileConditionType.LessThanEqual,
+ IsRequired = false,
+ Property = ProfileConditionValue.AudioSampleRate,
+ Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
+ });
+ }
- if (conditions.Count > 0)
- {
- // codec profile
- codecProfiles.Add(
- new CodecProfile
- {
- Type = CodecType.Audio,
- Container = string.Join(',', containers),
- Conditions = conditions.ToArray()
- });
- }
+ if (maxAudioBitDepth.HasValue)
+ {
+ // codec profile
+ conditions.Add(
+ new ProfileCondition
+ {
+ Condition = ProfileConditionType.LessThanEqual,
+ IsRequired = false,
+ Property = ProfileConditionValue.AudioBitDepth,
+ Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
+ });
+ }
- deviceProfile.CodecProfiles = codecProfiles.ToArray();
+ if (maxAudioChannels.HasValue)
+ {
+ // codec profile
+ conditions.Add(
+ new ProfileCondition
+ {
+ Condition = ProfileConditionType.LessThanEqual,
+ IsRequired = false,
+ Property = ProfileConditionValue.AudioChannels,
+ Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
+ });
+ }
- return deviceProfile;
+ if (conditions.Count > 0)
+ {
+ // codec profile
+ codecProfiles.Add(
+ new CodecProfile
+ {
+ Type = CodecType.Audio,
+ Container = string.Join(',', containers),
+ Conditions = conditions.ToArray()
+ });
}
+
+ deviceProfile.CodecProfiles = codecProfiles.ToArray();
+
+ return deviceProfile;
}
}
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 568224a42..b0973b8a1 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -25,564 +25,576 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// User controller.
+/// </summary>
+[Route("Users")]
+public class UserController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly ISessionManager _sessionManager;
+ private readonly INetworkManager _networkManager;
+ private readonly IDeviceManager _deviceManager;
+ private readonly IAuthorizationContext _authContext;
+ private readonly IServerConfigurationManager _config;
+ private readonly ILogger _logger;
+ private readonly IQuickConnect _quickConnectManager;
+
/// <summary>
- /// User controller.
+ /// Initializes a new instance of the <see cref="UserController"/> class.
/// </summary>
- [Route("Users")]
- public class UserController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
+ /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+ /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
+ /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
+ /// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param>
+ public UserController(
+ IUserManager userManager,
+ ISessionManager sessionManager,
+ INetworkManager networkManager,
+ IDeviceManager deviceManager,
+ IAuthorizationContext authContext,
+ IServerConfigurationManager config,
+ ILogger<UserController> logger,
+ IQuickConnect quickConnectManager)
{
- private readonly IUserManager _userManager;
- private readonly ISessionManager _sessionManager;
- private readonly INetworkManager _networkManager;
- private readonly IDeviceManager _deviceManager;
- private readonly IAuthorizationContext _authContext;
- private readonly IServerConfigurationManager _config;
- private readonly ILogger _logger;
- private readonly IQuickConnect _quickConnectManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="UserController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
- /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
- /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
- /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
- /// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param>
- public UserController(
- IUserManager userManager,
- ISessionManager sessionManager,
- INetworkManager networkManager,
- IDeviceManager deviceManager,
- IAuthorizationContext authContext,
- IServerConfigurationManager config,
- ILogger<UserController> logger,
- IQuickConnect quickConnectManager)
- {
- _userManager = userManager;
- _sessionManager = sessionManager;
- _networkManager = networkManager;
- _deviceManager = deviceManager;
- _authContext = authContext;
- _config = config;
- _logger = logger;
- _quickConnectManager = quickConnectManager;
- }
-
- /// <summary>
- /// Gets a list of users.
- /// </summary>
- /// <param name="isHidden">Optional filter by IsHidden=true or false.</param>
- /// <param name="isDisabled">Optional filter by IsDisabled=true or false.</param>
- /// <response code="200">Users returned.</response>
- /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns>
- [HttpGet]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<UserDto>> GetUsers(
- [FromQuery] bool? isHidden,
- [FromQuery] bool? isDisabled)
- {
- var users = Get(isHidden, isDisabled, false, false);
- return Ok(users);
- }
-
- /// <summary>
- /// Gets a list of publicly visible users for display on a login screen.
- /// </summary>
- /// <response code="200">Public users returned.</response>
- /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns>
- [HttpGet("Public")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<UserDto>> GetPublicUsers()
- {
- // If the startup wizard hasn't been completed then just return all users
- if (!_config.Configuration.IsStartupWizardCompleted)
- {
- return Ok(Get(false, false, false, false));
- }
+ _userManager = userManager;
+ _sessionManager = sessionManager;
+ _networkManager = networkManager;
+ _deviceManager = deviceManager;
+ _authContext = authContext;
+ _config = config;
+ _logger = logger;
+ _quickConnectManager = quickConnectManager;
+ }
- return Ok(Get(false, false, true, true));
+ /// <summary>
+ /// Gets a list of users.
+ /// </summary>
+ /// <param name="isHidden">Optional filter by IsHidden=true or false.</param>
+ /// <param name="isDisabled">Optional filter by IsDisabled=true or false.</param>
+ /// <response code="200">Users returned.</response>
+ /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns>
+ [HttpGet]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<UserDto>> GetUsers(
+ [FromQuery] bool? isHidden,
+ [FromQuery] bool? isDisabled)
+ {
+ var users = Get(isHidden, isDisabled, false, false);
+ return Ok(users);
+ }
+
+ /// <summary>
+ /// Gets a list of publicly visible users for display on a login screen.
+ /// </summary>
+ /// <response code="200">Public users returned.</response>
+ /// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns>
+ [HttpGet("Public")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<UserDto>> GetPublicUsers()
+ {
+ // If the startup wizard hasn't been completed then just return all users
+ if (!_config.Configuration.IsStartupWizardCompleted)
+ {
+ return Ok(Get(false, false, false, false));
}
- /// <summary>
- /// Gets a user by Id.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <response code="200">User returned.</response>
- /// <response code="404">User not found.</response>
- /// <returns>An <see cref="UserDto"/> with information about the user or a <see cref="NotFoundResult"/> if the user was not found.</returns>
- [HttpGet("{userId}")]
- [Authorize(Policy = Policies.IgnoreParentalControl)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<UserDto> GetUserById([FromRoute, Required] Guid userId)
+ return Ok(Get(false, false, true, true));
+ }
+
+ /// <summary>
+ /// Gets a user by Id.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <response code="200">User returned.</response>
+ /// <response code="404">User not found.</response>
+ /// <returns>An <see cref="UserDto"/> with information about the user or a <see cref="NotFoundResult"/> if the user was not found.</returns>
+ [HttpGet("{userId}")]
+ [Authorize(Policy = Policies.IgnoreParentalControl)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<UserDto> GetUserById([FromRoute, Required] Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ if (user is null)
{
- var user = _userManager.GetUserById(userId);
+ return NotFound("User not found");
+ }
- if (user is null)
- {
- return NotFound("User not found");
- }
+ var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString());
+ return result;
+ }
- var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString());
- return result;
+ /// <summary>
+ /// Deletes a user.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <response code="204">User deleted.</response>
+ /// <response code="404">User not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns>
+ [HttpDelete("{userId}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Deletes a user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <response code="204">User deleted.</response>
- /// <response code="404">User not found.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="NotFoundResult"/> if the user was not found.</returns>
- [HttpDelete("{userId}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
- {
- var user = _userManager.GetUserById(userId);
- await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
- await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
- return NoContent();
- }
-
- /// <summary>
- /// Authenticates a user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="pw">The password as plain text.</param>
- /// <response code="200">User authenticated.</response>
- /// <response code="403">Sha1-hashed password only is not allowed.</response>
- /// <response code="404">User not found.</response>
- /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationResult"/>.</returns>
- [HttpPost("{userId}/Authenticate")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [Obsolete("Authenticate with username instead")]
- public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
- [FromRoute, Required] Guid userId,
- [FromQuery, Required] string pw)
- {
- var user = _userManager.GetUserById(userId);
-
- if (user is null)
- {
- return NotFound("User not found");
- }
+ await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
+ await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
+ return NoContent();
+ }
- AuthenticateUserByName request = new AuthenticateUserByName
- {
- Username = user.Username,
- Pw = pw
- };
- return await AuthenticateUserByName(request).ConfigureAwait(false);
+ /// <summary>
+ /// Authenticates a user.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="pw">The password as plain text.</param>
+ /// <response code="200">User authenticated.</response>
+ /// <response code="403">Sha1-hashed password only is not allowed.</response>
+ /// <response code="404">User not found.</response>
+ /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationResult"/>.</returns>
+ [HttpPost("{userId}/Authenticate")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Obsolete("Authenticate with username instead")]
+ public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
+ [FromRoute, Required] Guid userId,
+ [FromQuery, Required] string pw)
+ {
+ var user = _userManager.GetUserById(userId);
+
+ if (user is null)
+ {
+ return NotFound("User not found");
}
- /// <summary>
- /// Authenticates a user by name.
- /// </summary>
- /// <param name="request">The <see cref="AuthenticateUserByName"/> request.</param>
- /// <response code="200">User authenticated.</response>
- /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
- [HttpPost("AuthenticateByName")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
+ AuthenticateUserByName request = new AuthenticateUserByName
{
- var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+ Username = user.Username,
+ Pw = pw
+ };
+ return await AuthenticateUserByName(request).ConfigureAwait(false);
+ }
- try
- {
- var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest
- {
- App = auth.Client,
- AppVersion = auth.Version,
- DeviceId = auth.DeviceId,
- DeviceName = auth.Device,
- Password = request.Pw,
- RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(),
- Username = request.Username
- }).ConfigureAwait(false);
-
- return result;
- }
- catch (SecurityException e)
+ /// <summary>
+ /// Authenticates a user by name.
+ /// </summary>
+ /// <param name="request">The <see cref="AuthenticateUserByName"/> request.</param>
+ /// <response code="200">User authenticated.</response>
+ /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
+ [HttpPost("AuthenticateByName")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
+ {
+ var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+
+ try
+ {
+ var result = await _sessionManager.AuthenticateNewSession(new AuthenticationRequest
{
- // rethrow adding IP address to message
- throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
- }
+ App = auth.Client,
+ AppVersion = auth.Version,
+ DeviceId = auth.DeviceId,
+ DeviceName = auth.Device,
+ Password = request.Pw,
+ RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(),
+ Username = request.Username
+ }).ConfigureAwait(false);
+
+ return result;
}
+ catch (SecurityException e)
+ {
+ // rethrow adding IP address to message
+ throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
+ }
+ }
- /// <summary>
- /// Authenticates a user with quick connect.
- /// </summary>
- /// <param name="request">The <see cref="QuickConnectDto"/> request.</param>
- /// <response code="200">User authenticated.</response>
- /// <response code="400">Missing token.</response>
- /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
- [HttpPost("AuthenticateWithQuickConnect")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<AuthenticationResult> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
+ /// <summary>
+ /// Authenticates a user with quick connect.
+ /// </summary>
+ /// <param name="request">The <see cref="QuickConnectDto"/> request.</param>
+ /// <response code="200">User authenticated.</response>
+ /// <response code="400">Missing token.</response>
+ /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
+ [HttpPost("AuthenticateWithQuickConnect")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<AuthenticationResult> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
+ {
+ try
{
- try
- {
- return _quickConnectManager.GetAuthorizedRequest(request.Secret);
- }
- catch (SecurityException e)
- {
- // rethrow adding IP address to message
- throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
- }
+ return _quickConnectManager.GetAuthorizedRequest(request.Secret);
+ }
+ catch (SecurityException e)
+ {
+ // rethrow adding IP address to message
+ throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
}
+ }
- /// <summary>
- /// Updates a user's password.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="request">The <see cref="UpdateUserPassword"/> request.</param>
- /// <response code="204">Password successfully reset.</response>
- /// <response code="403">User is not allowed to update the password.</response>
- /// <response code="404">User not found.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
- [HttpPost("{userId}/Password")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> UpdateUserPassword(
- [FromRoute, Required] Guid userId,
- [FromBody, Required] UpdateUserPassword request)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
- }
+ /// <summary>
+ /// Updates a user's password.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="request">The <see cref="UpdateUserPassword"/> request.</param>
+ /// <response code="204">Password successfully reset.</response>
+ /// <response code="403">User is not allowed to update the password.</response>
+ /// <response code="404">User not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
+ [HttpPost("{userId}/Password")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdateUserPassword(
+ [FromRoute, Required] Guid userId,
+ [FromBody, Required] UpdateUserPassword request)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
+ }
- var user = _userManager.GetUserById(userId);
+ var user = _userManager.GetUserById(userId);
- if (user is null)
- {
- return NotFound("User not found");
- }
+ if (user is null)
+ {
+ return NotFound("User not found");
+ }
- if (request.ResetPassword)
- {
- await _userManager.ResetPassword(user).ConfigureAwait(false);
- }
- else
+ if (request.ResetPassword)
+ {
+ await _userManager.ResetPassword(user).ConfigureAwait(false);
+ }
+ else
+ {
+ if (!User.IsInRole(UserRoles.Administrator) || User.GetUserId().Equals(userId))
{
- if (!User.IsInRole(UserRoles.Administrator))
+ var success = await _userManager.AuthenticateUser(
+ user.Username,
+ request.CurrentPw ?? string.Empty,
+ request.CurrentPw ?? string.Empty,
+ HttpContext.GetNormalizedRemoteIp().ToString(),
+ false).ConfigureAwait(false);
+
+ if (success is null)
{
- var success = await _userManager.AuthenticateUser(
- user.Username,
- request.CurrentPw,
- request.CurrentPw,
- HttpContext.GetNormalizedRemoteIp().ToString(),
- false).ConfigureAwait(false);
-
- if (success is null)
- {
- return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
- }
+ return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
}
+ }
- await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
-
- var currentToken = User.GetToken();
+ await _userManager.ChangePassword(user, request.NewPw ?? string.Empty).ConfigureAwait(false);
- await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
- }
+ var currentToken = User.GetToken();
- return NoContent();
- }
-
- /// <summary>
- /// Updates a user's easy password.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param>
- /// <response code="204">Password successfully reset.</response>
- /// <response code="403">User is not allowed to update the password.</response>
- /// <response code="404">User not found.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
- [HttpPost("{userId}/EasyPassword")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> UpdateUserEasyPassword(
- [FromRoute, Required] Guid userId,
- [FromBody, Required] UpdateUserEasyPassword request)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
- }
+ await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
+ }
- var user = _userManager.GetUserById(userId);
+ return NoContent();
+ }
- if (user is null)
- {
- return NotFound("User not found");
- }
+ /// <summary>
+ /// Updates a user's easy password.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param>
+ /// <response code="204">Password successfully reset.</response>
+ /// <response code="403">User is not allowed to update the password.</response>
+ /// <response code="404">User not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
+ [HttpPost("{userId}/EasyPassword")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> UpdateUserEasyPassword(
+ [FromRoute, Required] Guid userId,
+ [FromBody, Required] UpdateUserEasyPassword request)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
+ }
- if (request.ResetPassword)
- {
- await _userManager.ResetEasyPassword(user).ConfigureAwait(false);
- }
- else
- {
- await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false);
- }
+ var user = _userManager.GetUserById(userId);
- return NoContent();
- }
-
- /// <summary>
- /// Updates a user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="updateUser">The updated user model.</param>
- /// <response code="204">User updated.</response>
- /// <response code="400">User information was not supplied.</response>
- /// <response code="403">User update forbidden.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns>
- [HttpPost("{userId}")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> UpdateUser(
- [FromRoute, Required] Guid userId,
- [FromBody, Required] UserDto updateUser)
- {
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
- }
+ if (user is null)
+ {
+ return NotFound("User not found");
+ }
- var user = _userManager.GetUserById(userId);
+ if (request.ResetPassword)
+ {
+ await _userManager.ResetEasyPassword(user).ConfigureAwait(false);
+ }
+ else
+ {
+ await _userManager.ChangeEasyPassword(user, request.NewPw ?? string.Empty, request.NewPassword ?? string.Empty).ConfigureAwait(false);
+ }
- if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
- {
- await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
- }
+ return NoContent();
+ }
- await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false);
-
- return NoContent();
- }
-
- /// <summary>
- /// Updates a user policy.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="newPolicy">The new user policy.</param>
- /// <response code="204">User policy updated.</response>
- /// <response code="400">User policy was not supplied.</response>
- /// <response code="403">User policy update forbidden.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns>
- [HttpPost("{userId}/Policy")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> UpdateUserPolicy(
- [FromRoute, Required] Guid userId,
- [FromBody, Required] UserPolicy newPolicy)
- {
- var user = _userManager.GetUserById(userId);
-
- // If removing admin access
- if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
- {
- if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
- {
- return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
- }
- }
+ /// <summary>
+ /// Updates a user.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="updateUser">The updated user model.</param>
+ /// <response code="204">User updated.</response>
+ /// <response code="400">User information was not supplied.</response>
+ /// <response code="403">User update forbidden.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure.</returns>
+ [HttpPost("{userId}")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> UpdateUser(
+ [FromRoute, Required] Guid userId,
+ [FromBody, Required] UserDto updateUser)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
- // If disabling
- if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
- {
- return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled.");
- }
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
+ }
- // If disabling
- if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
- {
- if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
- {
- return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
- }
+ if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
+ {
+ await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
+ }
- var currentToken = User.GetToken();
- await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
- }
+ await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false);
- await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
+ return NoContent();
+ }
- return NoContent();
+ /// <summary>
+ /// Updates a user policy.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="newPolicy">The new user policy.</param>
+ /// <response code="204">User policy updated.</response>
+ /// <response code="400">User policy was not supplied.</response>
+ /// <response code="403">User policy update forbidden.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="BadRequestResult"/> or a <see cref="ForbidResult"/> on failure..</returns>
+ [HttpPost("{userId}/Policy")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> UpdateUserPolicy(
+ [FromRoute, Required] Guid userId,
+ [FromBody, Required] UserPolicy newPolicy)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Updates a user configuration.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <param name="userConfig">The new user configuration.</param>
- /// <response code="204">User configuration updated.</response>
- /// <response code="403">User configuration update forbidden.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("{userId}/Configuration")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task<ActionResult> UpdateUserConfiguration(
- [FromRoute, Required] Guid userId,
- [FromBody, Required] UserConfiguration userConfig)
+ // If removing admin access
+ if (!newPolicy.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
{
- if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
+ if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
{
- return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
+ return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
}
-
- await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
-
- return NoContent();
}
- /// <summary>
- /// Creates a user.
- /// </summary>
- /// <param name="request">The create user by name request body.</param>
- /// <response code="200">User created.</response>
- /// <returns>An <see cref="UserDto"/> of the new user.</returns>
- [HttpPost("New")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<UserDto>> CreateUserByName([FromBody, Required] CreateUserByName request)
+ // If disabling
+ if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
{
- var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
+ return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled.");
+ }
- // no need to authenticate password for new user
- if (request.Password is not null)
+ // If disabling
+ if (newPolicy.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
+ {
+ if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
{
- await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
+ return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
}
- var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString());
-
- return result;
+ var currentToken = User.GetToken();
+ await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
}
- /// <summary>
- /// Initiates the forgot password process for a local user.
- /// </summary>
- /// <param name="forgotPasswordRequest">The forgot password request containing the entered username.</param>
- /// <response code="200">Password reset process started.</response>
- /// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
- [HttpPost("ForgotPassword")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
+ await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Updates a user configuration.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="userConfig">The new user configuration.</param>
+ /// <response code="204">User configuration updated.</response>
+ /// <response code="403">User configuration update forbidden.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("{userId}/Configuration")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> UpdateUserConfiguration(
+ [FromRoute, Required] Guid userId,
+ [FromBody, Required] UserConfiguration userConfig)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
{
- var ip = HttpContext.GetNormalizedRemoteIp();
- var isLocal = HttpContext.IsLocal()
- || _networkManager.IsInLocalNetwork(ip);
+ return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
+ }
- if (isLocal)
- {
- _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip);
- }
+ await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
- var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);
+ return NoContent();
+ }
- return result;
+ /// <summary>
+ /// Creates a user.
+ /// </summary>
+ /// <param name="request">The create user by name request body.</param>
+ /// <response code="200">User created.</response>
+ /// <returns>An <see cref="UserDto"/> of the new user.</returns>
+ [HttpPost("New")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<UserDto>> CreateUserByName([FromBody, Required] CreateUserByName request)
+ {
+ var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
+
+ // no need to authenticate password for new user
+ if (request.Password is not null)
+ {
+ await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
}
- /// <summary>
- /// Redeems a forgot password pin.
- /// </summary>
- /// <param name="forgotPasswordPinRequest">The forgot password pin request containing the entered pin.</param>
- /// <response code="200">Pin reset process started.</response>
- /// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
- [HttpPost("ForgotPassword/Pin")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest)
+ var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString());
+
+ return result;
+ }
+
+ /// <summary>
+ /// Initiates the forgot password process for a local user.
+ /// </summary>
+ /// <param name="forgotPasswordRequest">The forgot password request containing the entered username.</param>
+ /// <response code="200">Password reset process started.</response>
+ /// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
+ [HttpPost("ForgotPassword")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
+ {
+ var ip = HttpContext.GetNormalizedRemoteIp();
+ var isLocal = HttpContext.IsLocal()
+ || _networkManager.IsInLocalNetwork(ip);
+
+ if (isLocal)
{
- var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false);
- return result;
+ _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip);
}
- /// <summary>
- /// Gets the user based on auth token.
- /// </summary>
- /// <response code="200">User returned.</response>
- /// <response code="400">Token is not owned by a user.</response>
- /// <returns>A <see cref="UserDto"/> for the authenticated user.</returns>
- [HttpGet("Me")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- public ActionResult<UserDto> GetCurrentUser()
- {
- var userId = User.GetUserId();
- if (userId.Equals(default))
- {
- return BadRequest();
- }
+ var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);
- var user = _userManager.GetUserById(userId);
- if (user is null)
- {
- return BadRequest();
- }
+ return result;
+ }
+
+ /// <summary>
+ /// Redeems a forgot password pin.
+ /// </summary>
+ /// <param name="forgotPasswordPinRequest">The forgot password pin request containing the entered pin.</param>
+ /// <response code="200">Pin reset process started.</response>
+ /// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
+ [HttpPost("ForgotPassword/Pin")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest)
+ {
+ var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false);
+ return result;
+ }
+
+ /// <summary>
+ /// Gets the user based on auth token.
+ /// </summary>
+ /// <response code="200">User returned.</response>
+ /// <response code="400">Token is not owned by a user.</response>
+ /// <returns>A <see cref="UserDto"/> for the authenticated user.</returns>
+ [HttpGet("Me")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public ActionResult<UserDto> GetCurrentUser()
+ {
+ var userId = User.GetUserId();
+ if (userId.Equals(default))
+ {
+ return BadRequest();
+ }
- return _userManager.GetUserDto(user);
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return BadRequest();
}
- private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
+ return _userManager.GetUserDto(user);
+ }
+
+ private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
+ {
+ var users = _userManager.Users;
+
+ if (isDisabled.HasValue)
{
- var users = _userManager.Users;
+ users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == isDisabled.Value);
+ }
- if (isDisabled.HasValue)
- {
- users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == isDisabled.Value);
- }
+ if (isHidden.HasValue)
+ {
+ users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == isHidden.Value);
+ }
- if (isHidden.HasValue)
- {
- users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == isHidden.Value);
- }
+ if (filterByDevice)
+ {
+ var deviceId = User.GetDeviceId();
- if (filterByDevice)
+ if (!string.IsNullOrWhiteSpace(deviceId))
{
- var deviceId = User.GetDeviceId();
-
- if (!string.IsNullOrWhiteSpace(deviceId))
- {
- users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId));
- }
+ users = users.Where(i => _deviceManager.CanAccessDevice(i, deviceId));
}
+ }
- if (filterByNetwork)
+ if (filterByNetwork)
+ {
+ if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()))
{
- if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()))
- {
- users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
- }
+ users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
}
+ }
- var result = users
- .OrderBy(u => u.Username)
- .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString()));
+ var result = users
+ .OrderBy(u => u.Username)
+ .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString()));
- return result;
- }
+ return result;
}
}
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index cd21c5f6f..1233995b4 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -4,10 +4,8 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
-using Jellyfin.Api.Models.UserDtos;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -23,406 +21,446 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// User library controller.
+/// </summary>
+[Route("")]
+[Authorize]
+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>
+ /// Initializes a new instance of the <see cref="UserLibraryController"/> class.
+ /// </summary>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
+ public UserLibraryController(
+ IUserManager userManager,
+ IUserDataManager userDataRepository,
+ ILibraryManager libraryManager,
+ IDtoService dtoService,
+ IUserViewManager userViewManager,
+ IFileSystem fileSystem,
+ ILyricManager lyricManager)
+ {
+ _userManager = userManager;
+ _userDataRepository = userDataRepository;
+ _libraryManager = libraryManager;
+ _dtoService = dtoService;
+ _userViewManager = userViewManager;
+ _fileSystem = fileSystem;
+ _lyricManager = lyricManager;
+ }
+
/// <summary>
- /// User library controller.
+ /// Gets an item from a user's library.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class UserLibraryController : BaseJellyfinApiController
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Item returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the d item.</returns>
+ [HttpGet("Users/{userId}/Items/{itemId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
- 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>
- /// Initializes a new instance of the <see cref="UserLibraryController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
- public UserLibraryController(
- IUserManager userManager,
- IUserDataManager userDataRepository,
- ILibraryManager libraryManager,
- IDtoService dtoService,
- IUserViewManager userViewManager,
- IFileSystem fileSystem,
- ILyricManager lyricManager)
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
{
- _userManager = userManager;
- _userDataRepository = userDataRepository;
- _libraryManager = libraryManager;
- _dtoService = dtoService;
- _userViewManager = userViewManager;
- _fileSystem = fileSystem;
- _lyricManager = lyricManager;
+ return NotFound();
}
- /// <summary>
- /// Gets an item from a user's library.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Item returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the d item.</returns>
- [HttpGet("Users/{userId}/Items/{itemId}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- var user = _userManager.GetUserById(userId);
+ return NotFound();
+ }
- var item = itemId.Equals(default)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
+ await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
- await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
- var dtoOptions = new DtoOptions().AddClientFields(User);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, 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);
+ if (user is null)
+ {
+ return NotFound();
}
- /// <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 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);
+ if (user is null)
{
- var user = _userManager.GetUserById(userId);
- var item = _libraryManager.GetUserRootFolder();
- var dtoOptions = new DtoOptions().AddClientFields(User);
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ return NotFound();
}
- /// <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 item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- var user = _userManager.GetUserById(userId);
+ return NotFound();
+ }
- 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();
- 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);
+ }
- 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>
- /// 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>
- /// 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>
- /// 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>
- /// 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)
+ /// <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);
+ if (user is null)
{
- return UpdateUserItemRatingInternal(userId, itemId, likes);
+ return NotFound();
}
- /// <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 item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- var user = _userManager.GetUserById(userId);
+ return NotFound();
+ }
- var item = itemId.Equals(default)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
- var dtoOptions = new DtoOptions().AddClientFields(User);
+ if (item is IHasTrailers hasTrailers)
+ {
+ var trailers = hasTrailers.LocalTrailers;
+ return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());
+ }
- if (item is IHasTrailers hasTrailers)
- {
- var trailers = hasTrailers.LocalTrailers;
- return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());
- }
+ return Ok(item.GetExtras()
+ .Where(e => e.ExtraType == ExtraType.Trailer)
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
+ }
- return Ok(item.GetExtras()
- .Where(e => e.ExtraType == ExtraType.Trailer)
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
+ /// <summary>
+ /// Gets special features for an item.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Special features returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
+ [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
}
- /// <summary>
- /// Gets special features for an item.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Special features returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the special features.</returns>
- [HttpGet("Users/{userId}/Items/{itemId}/SpecialFeatures")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+ if (item is null)
{
- var user = _userManager.GetUserById(userId);
+ return NotFound();
+ }
- var item = itemId.Equals(default)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
- 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)));
+ }
- 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 (user is null)
+ {
+ return NotFound();
}
- /// <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)
+ if (!isPlayed.HasValue)
{
- var user = _userManager.GetUserById(userId);
-
- if (!isPlayed.HasValue)
+ if (user.HidePlayedInLatest)
{
- if (user.HidePlayedInLatest)
- {
- isPlayed = false;
- }
+ isPlayed = false;
}
+ }
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ 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 list = _userViewManager.GetLatestItems(
+ new LatestItemsQuery
{
- var item = i.Item2[0];
- var childCount = 0;
+ 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;
- }
+ 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);
+ var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user);
- dto.ChildCount = childCount;
+ dto.ChildCount = childCount;
- return dto;
- });
+ return dto;
+ });
- return Ok(dtos);
- }
+ return Ok(dtos);
+ }
- private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
+ private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
+ {
+ if (item is Person)
{
- if (item is Person)
- {
- var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
- var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
+ var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
+ var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
- if (!hasMetdata)
+ if (!hasMetdata)
+ {
+ var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
- var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
- ImageRefreshMode = MetadataRefreshMode.FullRefresh,
- ForceSave = performFullRefresh
- };
-
- await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
- }
+ 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);
+ /// <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);
- // Set favorite status
- data.IsFavorite = isFavorite;
+ var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
- _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ // Get the user data for this item
+ var data = _userDataRepository.GetUserData(user, item);
- return _userDataRepository.GetUserDataDto(item, user);
- }
+ // Set favorite status
+ data.IsFavorite = isFavorite;
- /// <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);
+ _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
- var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
+ return _userDataRepository.GetUserDataDto(item, user);
+ }
- // Get the user data for this item
- var data = _userDataRepository.GetUserData(user, item);
+ /// <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);
- data.Likes = likes;
+ var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
- _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ // Get the user data for this item
+ var data = _userDataRepository.GetUserData(user, item);
- return _userDataRepository.GetUserDataDto(item, user);
- }
+ data.Likes = likes;
- /// <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);
+ _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
- if (user is null)
- {
- return NotFound();
- }
+ return _userDataRepository.GetUserDataDto(item, user);
+ }
- var item = itemId.Equals(default)
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
+ /// <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 (item is null)
- {
- return NotFound();
- }
+ if (user is null)
+ {
+ return NotFound();
+ }
- var result = await _lyricManager.GetLyrics(item).ConfigureAwait(false);
- if (result is not null)
- {
- return Ok(result);
- }
+ var item = itemId.Equals(default)
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
return NotFound();
}
+
+ var result = await _lyricManager.GetLyrics(item).ConfigureAwait(false);
+ if (result is not null)
+ {
+ return Ok(result);
+ }
+
+ return NotFound();
}
}
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index 3aeb444df..838b43234 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
@@ -17,122 +16,121 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// User views controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class UserViewsController : BaseJellyfinApiController
{
+ private readonly IUserManager _userManager;
+ private readonly IUserViewManager _userViewManager;
+ private readonly IDtoService _dtoService;
+ private readonly ILibraryManager _libraryManager;
+
/// <summary>
- /// User views controller.
+ /// Initializes a new instance of the <see cref="UserViewsController"/> class.
/// </summary>
- [Route("")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class UserViewsController : BaseJellyfinApiController
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ public UserViewsController(
+ IUserManager userManager,
+ IUserViewManager userViewManager,
+ IDtoService dtoService,
+ ILibraryManager libraryManager)
{
- private readonly IUserManager _userManager;
- private readonly IUserViewManager _userViewManager;
- private readonly IDtoService _dtoService;
- private readonly ILibraryManager _libraryManager;
+ _userManager = userManager;
+ _userViewManager = userViewManager;
+ _dtoService = dtoService;
+ _libraryManager = libraryManager;
+ }
- /// <summary>
- /// Initializes a new instance of the <see cref="UserViewsController"/> class.
- /// </summary>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- public UserViewsController(
- IUserManager userManager,
- IUserViewManager userViewManager,
- IDtoService dtoService,
- ILibraryManager libraryManager)
+ /// <summary>
+ /// Get user views.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="includeExternalContent">Whether or not to include external views such as channels or live tv.</param>
+ /// <param name="presetViews">Preset views.</param>
+ /// <param name="includeHidden">Whether or not to include hidden content.</param>
+ /// <response code="200">User views returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the user views.</returns>
+ [HttpGet("Users/{userId}/Views")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public QueryResult<BaseItemDto> GetUserViews(
+ [FromRoute, Required] Guid userId,
+ [FromQuery] bool? includeExternalContent,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
+ [FromQuery] bool includeHidden = false)
+ {
+ var query = new UserViewQuery
{
- _userManager = userManager;
- _userViewManager = userViewManager;
- _dtoService = dtoService;
- _libraryManager = libraryManager;
- }
+ UserId = userId,
+ IncludeHidden = includeHidden
+ };
- /// <summary>
- /// Get user views.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="includeExternalContent">Whether or not to include external views such as channels or live tv.</param>
- /// <param name="presetViews">Preset views.</param>
- /// <param name="includeHidden">Whether or not to include hidden content.</param>
- /// <response code="200">User views returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the user views.</returns>
- [HttpGet("Users/{userId}/Views")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public QueryResult<BaseItemDto> GetUserViews(
- [FromRoute, Required] Guid userId,
- [FromQuery] bool? includeExternalContent,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
- [FromQuery] bool includeHidden = false)
+ if (includeExternalContent.HasValue)
{
- var query = new UserViewQuery
- {
- UserId = userId,
- IncludeHidden = includeHidden
- };
+ query.IncludeExternalContent = includeExternalContent.Value;
+ }
- if (includeExternalContent.HasValue)
- {
- query.IncludeExternalContent = includeExternalContent.Value;
- }
+ if (presetViews.Length != 0)
+ {
+ query.PresetViews = presetViews;
+ }
- if (presetViews.Length != 0)
- {
- query.PresetViews = presetViews;
- }
+ var folders = _userViewManager.GetUserViews(query);
- var folders = _userViewManager.GetUserViews(query);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+ var fields = dtoOptions.Fields.ToList();
- 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();
- fields.Add(ItemFields.PrimaryImageAspectRatio);
- fields.Add(ItemFields.DisplayPreferencesId);
- fields.Remove(ItemFields.BasicSyncInfo);
- dtoOptions.Fields = fields.ToArray();
+ var user = _userManager.GetUserById(userId);
- var user = _userManager.GetUserById(userId);
+ var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
+ .ToArray();
- var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
- .ToArray();
+ return new QueryResult<BaseItemDto>(dtos);
+ }
- 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();
}
- /// <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 Ok(_libraryManager.GetUserRootFolder()
+ .GetChildren(user, true)
+ .OfType<Folder>()
+ .Where(UserView.IsEligibleForGrouping)
+ .Select(i => new SpecialViewOptionDto
{
- 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());
- }
+ Name = i.Name,
+ Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
+ })
+ .OrderBy(i => i.Name)
+ .AsEnumerable());
}
}
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index bb3162614..23b9ba46f 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -10,73 +10,72 @@ using MediaBrowser.Controller.MediaEncoding;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Attachments controller.
+/// </summary>
+[Route("Videos")]
+public class VideoAttachmentsController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IAttachmentExtractor _attachmentExtractor;
+
/// <summary>
- /// Attachments controller.
+ /// Initializes a new instance of the <see cref="VideoAttachmentsController"/> class.
/// </summary>
- [Route("Videos")]
- public class VideoAttachmentsController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
+ public VideoAttachmentsController(
+ ILibraryManager libraryManager,
+ IAttachmentExtractor attachmentExtractor)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IAttachmentExtractor _attachmentExtractor;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="VideoAttachmentsController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
- public VideoAttachmentsController(
- ILibraryManager libraryManager,
- IAttachmentExtractor attachmentExtractor)
- {
- _libraryManager = libraryManager;
- _attachmentExtractor = attachmentExtractor;
- }
+ _libraryManager = libraryManager;
+ _attachmentExtractor = attachmentExtractor;
+ }
- /// <summary>
- /// Get video attachment.
- /// </summary>
- /// <param name="videoId">Video ID.</param>
- /// <param name="mediaSourceId">Media Source ID.</param>
- /// <param name="index">Attachment Index.</param>
- /// <response code="200">Attachment retrieved.</response>
- /// <response code="404">Video or attachment not found.</response>
- /// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
- [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")]
- [ProducesFile(MediaTypeNames.Application.Octet)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> GetAttachment(
- [FromRoute, Required] Guid videoId,
- [FromRoute, Required] string mediaSourceId,
- [FromRoute, Required] int index)
+ /// <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
{
- try
+ var item = _libraryManager.GetItemById(videoId);
+ if (item is null)
{
- var item = _libraryManager.GetItemById(videoId);
- if (item is null)
- {
- return NotFound();
- }
+ return NotFound();
+ }
- var (attachment, stream) = await _attachmentExtractor.GetAttachment(
- item,
- mediaSourceId,
- index,
- CancellationToken.None)
- .ConfigureAwait(false);
+ var (attachment, stream) = await _attachmentExtractor.GetAttachment(
+ item,
+ mediaSourceId,
+ index,
+ CancellationToken.None)
+ .ConfigureAwait(false);
- var contentType = string.IsNullOrWhiteSpace(attachment.MimeType)
- ? MediaTypeNames.Application.Octet
- : attachment.MimeType;
+ var contentType = string.IsNullOrWhiteSpace(attachment.MimeType)
+ ? MediaTypeNames.Application.Octet
+ : attachment.MimeType;
- return new FileStreamResult(stream, contentType);
- }
- catch (ResourceNotFoundException e)
- {
- return NotFound(e.Message);
- }
+ return new FileStreamResult(stream, contentType);
+ }
+ catch (ResourceNotFoundException e)
+ {
+ return NotFound(e.Message);
}
}
}
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 64d8fb498..3a61367f7 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -21,7 +21,6 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -32,644 +31,648 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The videos controller.
+/// </summary>
+public class VideosController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IDeviceManager _deviceManager;
+ private readonly TranscodingJobHelper _transcodingJobHelper;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly EncodingHelper _encodingHelper;
+
+ private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VideosController"/> class.
+ /// </summary>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> 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 the <see cref="TranscodingJobHelper"/> class.</param>
+ /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
+ /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
+ public VideosController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService,
+ IDlnaManager dlnaManager,
+ IMediaSourceManager mediaSourceManager,
+ IServerConfigurationManager serverConfigurationManager,
+ IMediaEncoder mediaEncoder,
+ IDeviceManager deviceManager,
+ TranscodingJobHelper transcodingJobHelper,
+ IHttpClientFactory httpClientFactory,
+ EncodingHelper encodingHelper)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ _dlnaManager = dlnaManager;
+ _mediaSourceManager = mediaSourceManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ _mediaEncoder = mediaEncoder;
+ _deviceManager = deviceManager;
+ _transcodingJobHelper = transcodingJobHelper;
+ _httpClientFactory = httpClientFactory;
+ _encodingHelper = encodingHelper;
+ }
+
/// <summary>
- /// The videos controller.
+ /// Gets additional parts for a video.
/// </summary>
- public class VideosController : BaseJellyfinApiController
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <response code="200">Additional parts returned.</response>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the parts.</returns>
+ [HttpGet("{itemId}/AdditionalParts")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
- private readonly IDlnaManager _dlnaManager;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IDeviceManager _deviceManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly EncodingHelper _encodingHelper;
-
- private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="VideosController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> 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 the <see cref="TranscodingJobHelper"/> class.</param>
- /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
- /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
- public VideosController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService,
- IDlnaManager dlnaManager,
- IMediaSourceManager mediaSourceManager,
- IServerConfigurationManager serverConfigurationManager,
- IMediaEncoder mediaEncoder,
- IDeviceManager deviceManager,
- TranscodingJobHelper transcodingJobHelper,
- IHttpClientFactory httpClientFactory,
- EncodingHelper encodingHelper)
+ var user = userId is null || userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ var item = itemId.Equals(default)
+ ? (userId is null || userId.Value.Equals(default)
+ ? _libraryManager.RootFolder
+ : _libraryManager.GetUserRootFolder())
+ : _libraryManager.GetItemById(itemId);
+
+ var dtoOptions = new DtoOptions();
+ dtoOptions = dtoOptions.AddClientFields(User);
+
+ BaseItemDto[] items;
+ if (item is Video video)
{
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- _dlnaManager = dlnaManager;
- _mediaSourceManager = mediaSourceManager;
- _serverConfigurationManager = serverConfigurationManager;
- _mediaEncoder = mediaEncoder;
- _deviceManager = deviceManager;
- _transcodingJobHelper = transcodingJobHelper;
- _httpClientFactory = httpClientFactory;
- _encodingHelper = encodingHelper;
+ items = video.GetAdditionalParts()
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
+ .ToArray();
}
-
- /// <summary>
- /// Gets additional parts for a video.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <response code="200">Additional parts returned.</response>
- /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the parts.</returns>
- [HttpGet("{itemId}/AdditionalParts")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
+ else
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ items = Array.Empty<BaseItemDto>();
+ }
- var item = itemId.Equals(default)
- ? (userId is null || userId.Value.Equals(default)
- ? _libraryManager.RootFolder
- : _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
+ var result = new QueryResult<BaseItemDto>(items);
+ return result;
+ }
- var dtoOptions = new DtoOptions();
- dtoOptions = dtoOptions.AddClientFields(User);
+ /// <summary>
+ /// Removes alternate video sources.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="204">Alternate sources deleted.</response>
+ /// <response code="404">Video not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="NotFoundResult"/> if the video doesn't exist.</returns>
+ [HttpDelete("{itemId}/AlternateSources")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteAlternateSources([FromRoute, Required] Guid itemId)
+ {
+ var video = (Video)_libraryManager.GetItemById(itemId);
- BaseItemDto[] items;
- if (item is Video video)
- {
- items = video.GetAdditionalParts()
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
- .ToArray();
- }
- else
- {
- items = Array.Empty<BaseItemDto>();
- }
+ if (video is null)
+ {
+ return NotFound("The video either does not exist or the id does not belong to a video.");
+ }
- var result = new QueryResult<BaseItemDto>(items);
- return result;
+ if (video.LinkedAlternateVersions.Length == 0)
+ {
+ video = (Video?)_libraryManager.GetItemById(video.PrimaryVersionId);
}
- /// <summary>
- /// Removes alternate video sources.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <response code="204">Alternate sources deleted.</response>
- /// <response code="404">Video not found.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="NotFoundResult"/> if the video doesn't exist.</returns>
- [HttpDelete("{itemId}/AlternateSources")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteAlternateSources([FromRoute, Required] Guid itemId)
+ if (video is null)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ return NotFound();
+ }
- if (video is null)
- {
- return NotFound("The video either does not exist or the id does not belong to a video.");
- }
+ foreach (var link in video.GetLinkedAlternateVersions())
+ {
+ link.SetPrimaryVersionId(null);
+ link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- if (video.LinkedAlternateVersions.Length == 0)
- {
- video = (Video)_libraryManager.GetItemById(video.PrimaryVersionId);
- }
+ await link.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ }
- foreach (var link in video.GetLinkedAlternateVersions())
- {
- link.SetPrimaryVersionId(null);
- link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ video.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ video.SetPrimaryVersionId(null);
+ await video.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- await link.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- }
+ return NoContent();
+ }
- video.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- video.SetPrimaryVersionId(null);
- await video.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ /// <summary>
+ /// Merges videos into a single record.
+ /// </summary>
+ /// <param name="ids">Item id list. This allows multiple, comma delimited.</param>
+ /// <response code="204">Videos merged.</response>
+ /// <response code="400">Supply at least 2 video ids.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns>
+ [HttpPost("MergeVersions")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ {
+ var items = ids
+ .Select(i => _libraryManager.GetItemById(i))
+ .OfType<Video>()
+ .OrderBy(i => i.Id)
+ .ToList();
- return NoContent();
+ if (items.Count < 2)
+ {
+ return BadRequest("Please supply at least two videos to merge.");
}
- /// <summary>
- /// Merges videos into a single record.
- /// </summary>
- /// <param name="ids">Item id list. This allows multiple, comma delimited.</param>
- /// <response code="204">Videos merged.</response>
- /// <response code="400">Supply at least 2 video ids.</response>
- /// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns>
- [HttpPost("MergeVersions")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
+ if (primaryVersion is null)
{
- var items = ids
- .Select(i => _libraryManager.GetItemById(i))
- .OfType<Video>()
- .OrderBy(i => i.Id)
- .ToList();
-
- if (items.Count < 2)
- {
- return BadRequest("Please supply at least two videos to merge.");
- }
-
- var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
- if (primaryVersion is null)
- {
- primaryVersion = items
- .OrderBy(i =>
+ primaryVersion = items
+ .OrderBy(i =>
+ {
+ if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile)
{
- if (i.Video3DFormat.HasValue || i.VideoType != VideoType.VideoFile)
- {
- return 1;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.GetDefaultVideoStream()?.Width ?? 0)
- .First();
- }
+ return 1;
+ }
- var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList();
+ return 0;
+ })
+ .ThenByDescending(i => i.GetDefaultVideoStream()?.Width ?? 0)
+ .First();
+ }
- foreach (var item in items.Where(i => !i.Id.Equals(primaryVersion.Id)))
- {
- item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture));
+ var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList();
- await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ foreach (var item in items.Where(i => !i.Id.Equals(primaryVersion.Id)))
+ {
+ item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture));
- if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase)))
- {
- alternateVersionsOfPrimary.Add(new LinkedChild
- {
- Path = item.Path,
- ItemId = item.Id
- });
- }
+ await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- foreach (var linkedItem in item.LinkedAlternateVersions)
+ if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase)))
+ {
+ alternateVersionsOfPrimary.Add(new LinkedChild
{
- if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
- {
- alternateVersionsOfPrimary.Add(linkedItem);
- }
- }
+ Path = item.Path,
+ ItemId = item.Id
+ });
+ }
- if (item.LinkedAlternateVersions.Length > 0)
+ foreach (var linkedItem in item.LinkedAlternateVersions)
+ {
+ if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
{
- item.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ alternateVersionsOfPrimary.Add(linkedItem);
}
}
- primaryVersion.LinkedAlternateVersions = alternateVersionsOfPrimary.ToArray();
- await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- return NoContent();
+ if (item.LinkedAlternateVersions.Length > 0)
+ {
+ item.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ }
}
- /// <summary>
- /// Gets a video stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
- /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <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">Video stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("{itemId}/stream")]
- [HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesVideoFile]
- public async Task<ActionResult> GetVideoStream(
- [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? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ primaryVersion.LinkedAlternateVersions = alternateVersionsOfPrimary.ToArray();
+ await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets a video stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <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">Video stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("{itemId}/stream")]
+ [HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
+ public async Task<ActionResult> GetVideoStream(
+ [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? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
+ // CTS lifecycle is managed internally.
+ var cancellationTokenSource = new CancellationTokenSource();
+ var streamingRequest = new VideoRequestDto
{
- var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
- // CTS lifecycle is managed internally.
- var cancellationTokenSource = new CancellationTokenSource();
- var streamingRequest = new VideoRequestDto
- {
- Id = itemId,
- Container = container,
- Static = @static ?? false,
- Params = @params,
- Tag = tag,
- DeviceProfileId = deviceProfileId,
- PlaySessionId = playSessionId,
- SegmentContainer = segmentContainer,
- SegmentLength = segmentLength,
- MinSegments = minSegments,
- MediaSourceId = mediaSourceId,
- DeviceId = deviceId,
- AudioCodec = audioCodec,
- EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
- AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
- AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
- BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
- AudioSampleRate = audioSampleRate,
- MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
- MaxAudioBitDepth = maxAudioBitDepth,
- AudioChannels = audioChannels,
- Profile = profile,
- Level = level,
- Framerate = framerate,
- MaxFramerate = maxFramerate,
- CopyTimestamps = copyTimestamps ?? false,
- StartTimeTicks = startTimeTicks,
- Width = width,
- Height = height,
- MaxWidth = maxWidth,
- MaxHeight = maxHeight,
- VideoBitRate = videoBitRate,
- SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
- MaxRefFrames = maxRefFrames,
- MaxVideoBitDepth = maxVideoBitDepth,
- RequireAvc = requireAvc ?? false,
- DeInterlace = deInterlace ?? false,
- RequireNonAnamorphic = requireNonAnamorphic ?? false,
- TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
- CpuCoreLimit = cpuCoreLimit,
- LiveStreamId = liveStreamId,
- EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
- VideoCodec = videoCodec,
- SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodeReasons,
- AudioStreamIndex = audioStreamIndex,
- VideoStreamIndex = videoStreamIndex,
- Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
- };
-
- var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- HttpContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _encodingHelper,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- _transcodingJobType,
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
-
- if (@static.HasValue && @static.Value && state.DirectStreamProvider is not null)
- {
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
+ 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,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? false,
+ DeInterlace = deInterlace ?? false,
+ RequireNonAnamorphic = requireNonAnamorphic ?? false,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Streaming,
+ StreamOptions = streamOptions
+ };
+
+ var state = await StreamingHelpers.GetStreamingState(
+ streamingRequest,
+ HttpContext,
+ _mediaSourceManager,
+ _userManager,
+ _libraryManager,
+ _serverConfigurationManager,
+ _mediaEncoder,
+ _encodingHelper,
+ _dlnaManager,
+ _deviceManager,
+ _transcodingJobHelper,
+ _transcodingJobType,
+ cancellationTokenSource.Token)
+ .ConfigureAwait(false);
- var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
- if (liveStreamInfo is null)
- {
- return NotFound();
- }
+ if (@static.HasValue && @static.Value && state.DirectStreamProvider is not null)
+ {
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
- var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
- // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
- return File(liveStream, MimeTypes.GetMimeType("file.ts"));
+ var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId);
+ if (liveStreamInfo is null)
+ {
+ return NotFound();
}
- // Static remote stream
- if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
- {
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
+ // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
+ return File(liveStream, MimeTypes.GetMimeType("file.ts"));
+ }
- var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
- return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, HttpContext).ConfigureAwait(false);
- }
+ // Static remote stream
+ if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
+ {
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager);
- if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
- {
- return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
- }
+ var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
+ return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, HttpContext).ConfigureAwait(false);
+ }
- var outputPath = state.OutputFilePath;
- var outputPathExists = System.IO.File.Exists(outputPath);
+ if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
+ {
+ return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
+ }
- var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
- var isTranscodeCached = outputPathExists && transcodingJob is not null;
+ var outputPath = state.OutputFilePath;
+ var outputPathExists = System.IO.File.Exists(outputPath);
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager);
+ var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
+ var isTranscodeCached = outputPathExists && transcodingJob is not null;
- // Static stream
- if (@static.HasValue && @static.Value)
- {
- var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
+ StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager);
- if (state.MediaSource.IsInfiniteStream)
- {
- var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
- return File(liveStream, contentType);
- }
+ // Static stream
+ if (@static.HasValue && @static.Value)
+ {
+ var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
- return FileStreamResponseHelpers.GetStaticFileResult(
- state.MediaPath,
- contentType);
+ if (state.MediaSource.IsInfiniteStream)
+ {
+ var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper);
+ return File(liveStream, contentType);
}
- // Need to start ffmpeg (because media can't be returned directly)
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
- return await FileStreamResponseHelpers.GetTranscodedFile(
- state,
- isHeadRequest,
- HttpContext,
- _transcodingJobHelper,
- ffmpegCommandLineArguments,
- _transcodingJobType,
- cancellationTokenSource).ConfigureAwait(false);
+ return FileStreamResponseHelpers.GetStaticFileResult(
+ state.MediaPath,
+ contentType);
}
- /// <summary>
- /// Gets a video stream.
- /// </summary>
- /// <param name="itemId">The item id.</param>
- /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
- /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
- /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
- /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
- /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
- /// <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">Video stream returned.</response>
- /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("{itemId}/stream.{container}")]
- [HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesVideoFile]
- public Task<ActionResult> GetVideoStreamByContainer(
- [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? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? videoBitRate,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
- [FromQuery] int? maxRefFrames,
- [FromQuery] int? maxVideoBitDepth,
- [FromQuery] bool? requireAvc,
- [FromQuery] bool? deInterlace,
- [FromQuery] bool? requireNonAnamorphic,
- [FromQuery] int? transcodingMaxAudioChannels,
- [FromQuery] int? cpuCoreLimit,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] string? videoCodec,
- [FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodeReasons,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? videoStreamIndex,
- [FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
- {
- return GetVideoStream(
- itemId,
- container,
- @static,
- @params,
- tag,
- deviceProfileId,
- playSessionId,
- segmentContainer,
- segmentLength,
- minSegments,
- mediaSourceId,
- deviceId,
- audioCodec,
- enableAutoStreamCopy,
- allowVideoStreamCopy,
- allowAudioStreamCopy,
- breakOnNonKeyFrames,
- audioSampleRate,
- maxAudioBitDepth,
- audioBitRate,
- audioChannels,
- maxAudioChannels,
- profile,
- level,
- framerate,
- maxFramerate,
- copyTimestamps,
- startTimeTicks,
- width,
- height,
- maxWidth,
- maxHeight,
- videoBitRate,
- subtitleStreamIndex,
- subtitleMethod,
- maxRefFrames,
- maxVideoBitDepth,
- requireAvc,
- deInterlace,
- requireNonAnamorphic,
- transcodingMaxAudioChannels,
- cpuCoreLimit,
- liveStreamId,
- enableMpegtsM2TsMode,
- videoCodec,
- subtitleCodec,
- transcodeReasons,
- audioStreamIndex,
- videoStreamIndex,
- context,
- streamOptions);
- }
+ // Need to start ffmpeg (because media can't be returned directly)
+ var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
+ var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
+ return await FileStreamResponseHelpers.GetTranscodedFile(
+ state,
+ isHeadRequest,
+ HttpContext,
+ _transcodingJobHelper,
+ ffmpegCommandLineArguments,
+ _transcodingJobType,
+ cancellationTokenSource).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Gets a video stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </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="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <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">Video stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("{itemId}/stream.{container}")]
+ [HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
+ public Task<ActionResult> GetVideoStreamByContainer(
+ [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? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod? subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ return GetVideoStream(
+ itemId,
+ container,
+ @static,
+ @params,
+ tag,
+ deviceProfileId,
+ playSessionId,
+ segmentContainer,
+ segmentLength,
+ minSegments,
+ mediaSourceId,
+ deviceId,
+ audioCodec,
+ enableAutoStreamCopy,
+ allowVideoStreamCopy,
+ allowAudioStreamCopy,
+ breakOnNonKeyFrames,
+ audioSampleRate,
+ maxAudioBitDepth,
+ audioBitRate,
+ audioChannels,
+ maxAudioChannels,
+ profile,
+ level,
+ framerate,
+ maxFramerate,
+ copyTimestamps,
+ startTimeTicks,
+ width,
+ height,
+ maxWidth,
+ maxHeight,
+ videoBitRate,
+ subtitleStreamIndex,
+ subtitleMethod,
+ maxRefFrames,
+ maxVideoBitDepth,
+ requireAvc,
+ deInterlace,
+ requireNonAnamorphic,
+ transcodingMaxAudioChannels,
+ cpuCoreLimit,
+ liveStreamId,
+ enableMpegtsM2TsMode,
+ videoCodec,
+ subtitleCodec,
+ transcodeReasons,
+ audioStreamIndex,
+ videoStreamIndex,
+ context,
+ streamOptions);
}
}
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index cd85ba221..def37cb97 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -19,208 +18,207 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Years controller.
+/// </summary>
+[Authorize]
+public class YearsController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+
/// <summary>
- /// Years controller.
+ /// Initializes a new instance of the <see cref="YearsController"/> class.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class YearsController : BaseJellyfinApiController
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+ public YearsController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="YearsController"/> class.
- /// </summary>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- public YearsController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService)
- {
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
- }
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ }
- /// <summary>
- /// Get years.
- /// </summary>
- /// <param name="startIndex">Skips over a given number of items within the results. Use for paging.</param>
- /// <param name="limit">Optional. The maximum number of records to return.</param>
- /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
- /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.</param>
- /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
- /// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
- /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="userId">User Id.</param>
- /// <param name="recursive">Search recursively.</param>
- /// <param name="enableImages">Optional. Include image information in output.</param>
- /// <response code="200">Year query returned.</response>
- /// <returns> A <see cref="QueryResult{BaseItemDto}"/> containing the year result.</returns>
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetYears(
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
- [FromQuery] Guid? parentId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
- [FromQuery] bool? enableUserData,
- [FromQuery] int? imageTypeLimit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] Guid? userId,
- [FromQuery] bool recursive = true,
- [FromQuery] bool? enableImages = true)
- {
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ /// <summary>
+ /// Get years.
+ /// </summary>
+ /// <param name="startIndex">Skips over a given number of items within the results. Use for paging.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="enableUserData">Optional. Include user data.</param>
+ /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+ /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+ /// <param name="userId">User Id.</param>
+ /// <param name="recursive">Search recursively.</param>
+ /// <param name="enableImages">Optional. Include image information in output.</param>
+ /// <response code="200">Year query returned.</response>
+ /// <returns> A <see cref="QueryResult{BaseItemDto}"/> containing the year result.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetYears(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] Guid? userId,
+ [FromQuery] bool recursive = true,
+ [FromQuery] bool? enableImages = true)
+ {
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- User? user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
- BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
+ 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)
- {
- ExcludeItemTypes = excludeItemTypes,
- IncludeItemTypes = includeItemTypes,
- MediaTypes = mediaTypes,
- DtoOptions = dtoOptions
- };
+ var query = new InternalItemsQuery(user)
+ {
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ MediaTypes = mediaTypes,
+ DtoOptions = dtoOptions
+ };
+
+ bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
- bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
+ IList<BaseItem> items;
+ if (parentItem.IsFolder)
+ {
+ var folder = (Folder)parentItem;
- IList<BaseItem> items;
- if (parentItem.IsFolder)
+ if (userId.Equals(default))
{
- var folder = (Folder)parentItem;
-
- if (userId.Equals(default))
- {
- items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
- }
- else
- {
- items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList();
- }
+ items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
}
else
{
- items = new[] { parentItem }.Where(Filter).ToList();
+ items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList();
}
+ }
+ else
+ {
+ items = new[] { parentItem }.Where(Filter).ToList();
+ }
- var extractedItems = GetAllItems(items);
+ var extractedItems = GetAllItems(items);
- var filteredItems = _libraryManager.Sort(extractedItems, user, RequestHelpers.GetOrderBy(sortBy, sortOrder));
+ var filteredItems = _libraryManager.Sort(extractedItems, user, RequestHelpers.GetOrderBy(sortBy, sortOrder));
- var ibnItemsArray = filteredItems.ToList();
+ var ibnItemsArray = filteredItems.ToList();
- IEnumerable<BaseItem> ibnItems = ibnItemsArray;
+ IEnumerable<BaseItem> ibnItems = ibnItemsArray;
- if (startIndex.HasValue || limit.HasValue)
+ if (startIndex.HasValue || limit.HasValue)
+ {
+ if (startIndex.HasValue)
{
- if (startIndex.HasValue)
- {
- ibnItems = ibnItems.Skip(startIndex.Value);
- }
-
- if (limit.HasValue)
- {
- ibnItems = ibnItems.Take(limit.Value);
- }
+ ibnItems = ibnItems.Skip(startIndex.Value);
}
- var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>()));
-
- var dtos = tuples.Select(i => _dtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user));
-
- var result = new QueryResult<BaseItemDto>(
- startIndex,
- ibnItemsArray.Count,
- dtos.Where(i => i is not null).ToArray());
- return result;
- }
-
- /// <summary>
- /// Gets a year.
- /// </summary>
- /// <param name="year">The year.</param>
- /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
- /// <response code="200">Year returned.</response>
- /// <response code="404">Year not found.</response>
- /// <returns>
- /// An <see cref="OkResult"/> containing the year,
- /// or a <see cref="NotFoundResult"/> if year not found.
- /// </returns>
- [HttpGet("{year}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<BaseItemDto> GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId)
- {
- var item = _libraryManager.GetYear(year);
- if (item is null)
+ if (limit.HasValue)
{
- return NotFound();
+ ibnItems = ibnItems.Take(limit.Value);
}
+ }
- var dtoOptions = new DtoOptions()
- .AddClientFields(User);
+ var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>()));
- if (userId.HasValue && !userId.Value.Equals(default))
- {
- var user = _userManager.GetUserById(userId.Value);
- return _dtoService.GetBaseItemDto(item, dtoOptions, user);
- }
+ 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;
+ }
- return _dtoService.GetBaseItemDto(item, dtoOptions);
+ /// <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 NotFound();
}
- private bool FilterItem(BaseItem f, IReadOnlyCollection<BaseItemKind> excludeItemTypes, IReadOnlyCollection<BaseItemKind> includeItemTypes, IReadOnlyCollection<string> mediaTypes)
+ var dtoOptions = new DtoOptions()
+ .AddClientFields(User);
+
+ if (userId.HasValue && !userId.Value.Equals(default))
{
- var baseItemKind = f.GetBaseItemKind();
- // Exclude item types
- if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(baseItemKind))
- {
- return false;
- }
+ var user = _userManager.GetUserById(userId.Value);
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
- // Include item types
- if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(baseItemKind))
- {
- return false;
- }
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
+ }
- // Include MediaTypes
- if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
+ 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;
+ }
- return true;
+ // Include item types
+ if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(baseItemKind))
+ {
+ return false;
}
- private IEnumerable<BaseItem> GetAllItems(IEnumerable<BaseItem> items)
+ // Include MediaTypes
+ if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
- return items
- .Select(i => i.ProductionYear ?? 0)
- .Where(i => i > 0)
- .Distinct()
- .Select(year => _libraryManager.GetYear(year));
+ 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));
}
}