aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Controllers
diff options
context:
space:
mode:
authornyanmisaka <nst799610810@gmail.com>2020-11-08 19:11:54 +0800
committernyanmisaka <nst799610810@gmail.com>2020-11-08 19:11:54 +0800
commit737cb727f9f943f8bb55fca8a5c5023a98aca2d7 (patch)
tree8858a423545a03d0f7dfe5c1d1b0ae1157d62c5e /Jellyfin.Api/Controllers
parent05e78ee78c56364971956507f6239ded61f0af87 (diff)
parent96dcd9c87e2eb4b14004368856949e9fde2db261 (diff)
Merge remote-tracking branch 'upstream/master' into fonts
Diffstat (limited to 'Jellyfin.Api/Controllers')
-rw-r--r--Jellyfin.Api/Controllers/ActivityLogController.cs20
-rw-r--r--Jellyfin.Api/Controllers/AlbumsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/ApiKeyController.cs4
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs33
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs176
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs18
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs19
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs9
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs105
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs16
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs17
-rw-r--r--Jellyfin.Api/Controllers/DlnaController.cs7
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs83
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs550
-rw-r--r--Jellyfin.Api/Controllers/EnvironmentController.cs6
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs146
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs21
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs24
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs199
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs33
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs20
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs3
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs11
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs31
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs33
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs15
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs66
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs553
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs7
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs143
-rw-r--r--Jellyfin.Api/Controllers/NotificationsController.cs1
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs26
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs194
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs39
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs21
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs18
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs154
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs15
-rw-r--r--Jellyfin.Api/Controllers/ScheduledTasksController.cs12
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs98
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs27
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs139
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs67
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs11
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs10
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs21
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs315
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs126
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs23
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs7
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs55
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs6
55 files changed, 1252 insertions, 2519 deletions
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index a07cea9c0c..b429cebecb 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -1,7 +1,7 @@
using System;
-using System.Linq;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data.Queries;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
@@ -39,19 +39,19 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
[HttpGet("Entries")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
+ public async Task<ActionResult<QueryResult<ActivityLogEntry>>> GetLogEntries(
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] DateTime? minDate,
[FromQuery] bool? hasUserId)
{
- var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
- entries => entries.Where(entry => entry.DateCreated >= minDate
- && (!hasUserId.HasValue || (hasUserId.Value
- ? entry.UserId != Guid.Empty
- : entry.UserId == Guid.Empty))));
-
- return _activityManager.GetPagedResult(filterFunc, startIndex, limit);
+ return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
+ {
+ StartIndex = startIndex,
+ Limit = limit,
+ MinDate = minDate,
+ HasUserId = hasUserId
+ }).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Api/Controllers/AlbumsController.cs b/Jellyfin.Api/Controllers/AlbumsController.cs
index 190d4bd07c..357f646a2b 100644
--- a/Jellyfin.Api/Controllers/AlbumsController.cs
+++ b/Jellyfin.Api/Controllers/AlbumsController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -52,7 +53,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Albums/{albumId}/Similar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
- [FromRoute] string albumId,
+ [FromRoute, Required] string albumId,
[FromQuery] Guid? userId,
[FromQuery] string? excludeArtistIds,
[FromQuery] int? limit)
@@ -84,7 +85,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Artists/{artistId}/Similar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
- [FromRoute] string artistId,
+ [FromRoute, Required] string artistId,
[FromQuery] Guid? userId,
[FromQuery] string? excludeArtistIds,
[FromQuery] int? limit)
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index 0e28d4c474..e8d6ccdf27 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Keys")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CreateKey([FromQuery, Required] string? app)
+ public ActionResult CreateKey([FromQuery, Required] string app)
{
_authRepo.Create(new AuthenticationInfo
{
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Keys/{key}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult RevokeKey([FromRoute, Required] string? key)
+ public ActionResult RevokeKey([FromRoute, Required] string key)
{
_sessionManager.RevokeToken(key);
return NoContent();
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 3f72830cdd..826fce6b0c 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -1,13 +1,16 @@
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;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -53,7 +56,7 @@ namespace Jellyfin.Api.Controllers
/// <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 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. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</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>
@@ -88,7 +91,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -98,7 +101,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
@@ -144,9 +147,9 @@ namespace Jellyfin.Api.Controllers
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, ',', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
- Genres = RequestHelpers.Split(genres, ',', true),
+ Tags = RequestHelpers.Split(tags, '|', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
@@ -187,7 +190,7 @@ namespace Jellyfin.Api.Controllers
}).Where(i => i != null).Select(i => i!.Id).ToArray();
}
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
@@ -262,7 +265,7 @@ namespace Jellyfin.Api.Controllers
/// <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 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. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</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>
@@ -297,7 +300,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -307,7 +310,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
@@ -353,9 +356,9 @@ namespace Jellyfin.Api.Controllers
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, ',', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
- Genres = RequestHelpers.Split(genres, ',', true),
+ Tags = RequestHelpers.Split(tags, '|', true),
+ OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
@@ -396,7 +399,7 @@ namespace Jellyfin.Api.Controllers
}).Where(i => i != null).Select(i => i!.Id).ToArray();
}
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
@@ -469,7 +472,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the artist.</returns>
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetArtistByName([FromRoute] string name, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index fb3f09ba65..d4c6e4af94 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -1,93 +1,34 @@
using System;
using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading;
+using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// The audio controller.
/// </summary>
- // TODO: In order to autheneticate this in the future, Dlna playback will require updating
+ // TODO: In order to authenticate this in the future, Dlna playback will require updating
public class AudioController : BaseJellyfinApiController
{
- private readonly IDlnaManager _dlnaManager;
- private readonly IAuthorizationContext _authContext;
- private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IFileSystem _fileSystem;
- private readonly ISubtitleEncoder _subtitleEncoder;
- private readonly IConfiguration _configuration;
- private readonly IDeviceManager _deviceManager;
- private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly IHttpClientFactory _httpClientFactory;
+ private readonly AudioHelper _audioHelper;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
/// <summary>
/// Initializes a new instance of the <see cref="AudioController"/> class.
/// </summary>
- /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
- /// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
- /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
- /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
- /// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
- /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
- /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
- /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
- /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
- public AudioController(
- IDlnaManager dlnaManager,
- IUserManager userManger,
- IAuthorizationContext authorizationContext,
- ILibraryManager libraryManager,
- IMediaSourceManager mediaSourceManager,
- IServerConfigurationManager serverConfigurationManager,
- IMediaEncoder mediaEncoder,
- IFileSystem fileSystem,
- ISubtitleEncoder subtitleEncoder,
- IConfiguration configuration,
- IDeviceManager deviceManager,
- TranscodingJobHelper transcodingJobHelper,
- IHttpClientFactory httpClientFactory)
+ /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
+ public AudioController(AudioHelper audioHelper)
{
- _dlnaManager = dlnaManager;
- _authContext = authorizationContext;
- _userManager = userManger;
- _libraryManager = libraryManager;
- _mediaSourceManager = mediaSourceManager;
- _serverConfigurationManager = serverConfigurationManager;
- _mediaEncoder = mediaEncoder;
- _fileSystem = fileSystem;
- _subtitleEncoder = subtitleEncoder;
- _configuration = configuration;
- _deviceManager = deviceManager;
- _transcodingJobHelper = transcodingJobHelper;
- _httpClientFactory = httpClientFactory;
+ _audioHelper = audioHelper;
}
/// <summary>
@@ -144,13 +85,14 @@ namespace Jellyfin.Api.Controllers
/// <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=stream}.{container?}", Name = "GetAudioStreamByContainer")]
+ [HttpGet("{itemId}/stream.{container:required}", Name = "GetAudioStreamByContainer")]
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
- [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadAudioStreamByContainer")]
+ [HttpHead("{itemId}/stream.{container:required}", Name = "HeadAudioStreamByContainer")]
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
public async Task<ActionResult> GetAudioStream(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromRoute] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
@@ -200,10 +142,6 @@ namespace Jellyfin.Api.Controllers
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string>? streamOptions)
{
- bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
-
- var cancellationTokenSource = new CancellationTokenSource();
-
StreamingRequestDto streamingRequest = new StreamingRequestDto
{
Id = itemId,
@@ -257,97 +195,7 @@ namespace Jellyfin.Api.Controllers
StreamOptions = streamOptions
};
- using var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- Request,
- _authContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- _transcodingJobType,
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
-
- if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
- {
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
-
- await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
-
- // TODO (moved from MediaBrowser.Api): Don't hardcode contentType
- return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
- }
-
- // Static remote stream
- if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
- {
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
-
- var httpClient = _httpClientFactory.CreateClient();
- return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
- }
-
- if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
- {
- return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
- }
-
- var outputPath = state.OutputFilePath;
- var outputPathExists = System.IO.File.Exists(outputPath);
-
- var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
- var isTranscodeCached = outputPathExists && transcodingJob != null;
-
- StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
-
- // Static stream
- if (@static.HasValue && @static.Value)
- {
- var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
-
- if (state.MediaSource.IsInfiniteStream)
- {
- await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
- {
- AllowEndOfFile = false
- }.WriteToAsync(Response.Body, CancellationToken.None)
- .ConfigureAwait(false);
-
- return File(Response.Body, contentType);
- }
-
- return FileStreamResponseHelpers.GetStaticFileResult(
- state.MediaPath,
- contentType,
- isHeadRequest,
- this);
- }
-
- // Need to start ffmpeg (because media can't be returned directly)
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
- var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
- return await FileStreamResponseHelpers.GetTranscodedFile(
- state,
- isHeadRequest,
- this,
- _transcodingJobHelper,
- ffmpegCommandLineArguments,
- Request,
- _transcodingJobType,
- cancellationTokenSource).ConfigureAwait(false);
+ return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index bdd7dfd967..20fc96ba83 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
+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.Helpers;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -90,7 +92,7 @@ namespace Jellyfin.Api.Controllers
/// <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] string channelId)
+ public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] string channelId)
{
return _channelManager.GetChannelFeatures(channelId);
}
@@ -104,7 +106,7 @@ namespace Jellyfin.Api.Controllers
/// <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. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</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. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <response code="200">Channel items returned.</response>
@@ -114,13 +116,13 @@ namespace Jellyfin.Api.Controllers
/// </returns>
[HttpGet("{channelId}/Items")]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
- [FromRoute] Guid channelId,
+ [FromRoute, Required] Guid channelId,
[FromQuery] Guid? folderId,
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? sortOrder,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] string? sortBy,
[FromQuery] string? fields)
{
@@ -139,7 +141,7 @@ namespace Jellyfin.Api.Controllers
.AddItemFields(fields)
};
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
@@ -182,7 +184,7 @@ namespace Jellyfin.Api.Controllers
/// <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. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</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. 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="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
/// <response code="200">Latest channel items returned.</response>
@@ -195,7 +197,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] string? fields,
[FromQuery] string? channelIds)
{
@@ -216,7 +218,7 @@ namespace Jellyfin.Api.Controllers
.AddItemFields(fields)
};
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 53821a1885..eae06b767c 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -51,7 +52,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<CollectionCreationResult> CreateCollection(
+ public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
[FromQuery] string? name,
[FromQuery] string? ids,
[FromQuery] Guid? parentId,
@@ -59,14 +60,14 @@ namespace Jellyfin.Api.Controllers
{
var userId = _authContext.GetAuthorizationInfo(Request).UserId;
- var item = _collectionManager.CreateCollection(new CollectionCreationOptions
+ var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
IsLocked = isLocked,
Name = name,
ParentId = parentId,
ItemIdList = RequestHelpers.Split(ids, ',', true),
UserIds = new[] { userId }
- });
+ }).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(Request);
@@ -82,14 +83,14 @@ namespace Jellyfin.Api.Controllers
/// Adds items to a collection.
/// </summary>
/// <param name="collectionId">The collection id.</param>
- /// <param name="itemIds">Item ids, comma delimited.</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 ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
+ public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
{
- _collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
+ await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true);
return NoContent();
}
@@ -97,14 +98,14 @@ namespace Jellyfin.Api.Controllers
/// Removes items from a collection.
/// </summary>
/// <param name="collectionId">The collection id.</param>
- /// <param name="itemIds">Item ids, comma delimited.</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 ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
+ public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
{
- _collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
+ await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 019703dae9..e1c9f69f61 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -1,6 +1,8 @@
using System.ComponentModel.DataAnnotations;
+using System.Net.Mime;
using System.Text.Json;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.ConfigurationDtos;
using MediaBrowser.Common.Json;
@@ -73,7 +75,8 @@ namespace Jellyfin.Api.Controllers
/// <returns>Configuration.</returns>
[HttpGet("Configuration/{key}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<object> GetNamedConfiguration([FromRoute] string? key)
+ [ProducesFile(MediaTypeNames.Application.Json)]
+ public ActionResult<object> GetNamedConfiguration([FromRoute, Required] string key)
{
return _configurationManager.GetConfiguration(key);
}
@@ -87,7 +90,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Configuration/{key}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string? key)
+ public async Task<ActionResult> UpdateNamedConfiguration([FromRoute, Required] string key)
{
var configurationType = _configurationManager.GetConfigurationType(key);
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false);
@@ -117,7 +120,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("MediaEncoder/Path")]
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult UpdateMediaEncoderPath([FromForm, Required] MediaEncoderPathDto mediaEncoderPath)
+ 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 33abe3ccdc..a859ac114c 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Net.Mime;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Models;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller;
@@ -15,6 +17,7 @@ using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
namespace Jellyfin.Api.Controllers
{
@@ -26,39 +29,21 @@ namespace Jellyfin.Api.Controllers
{
private readonly ILogger<DashboardController> _logger;
private readonly IServerApplicationHost _appHost;
- private readonly IConfiguration _appConfig;
- private readonly IServerConfigurationManager _serverConfigurationManager;
- private readonly IResourceFileManager _resourceFileManager;
/// <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="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
- /// <param name="appConfig">Instance of <see cref="IConfiguration"/> interface.</param>
- /// <param name="resourceFileManager">Instance of <see cref="IResourceFileManager"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
public DashboardController(
ILogger<DashboardController> logger,
- IServerApplicationHost appHost,
- IConfiguration appConfig,
- IResourceFileManager resourceFileManager,
- IServerConfigurationManager serverConfigurationManager)
+ IServerApplicationHost appHost)
{
_logger = logger;
_appHost = appHost;
- _appConfig = appConfig;
- _resourceFileManager = resourceFileManager;
- _serverConfigurationManager = serverConfigurationManager;
}
/// <summary>
- /// Gets the path of the directory containing the static web interface content, or null if the server is not
- /// hosting the web client.
- /// </summary>
- private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager);
-
- /// <summary>
/// Gets the configuration pages.
/// </summary>
/// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
@@ -123,6 +108,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("web/ConfigurationPage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
{
IPlugin? plugin = null;
@@ -169,87 +155,6 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- /// <summary>
- /// Gets the robots.txt.
- /// </summary>
- /// <response code="200">Robots.txt returned.</response>
- /// <returns>The robots.txt.</returns>
- [HttpGet("robots.txt")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ApiExplorerSettings(IgnoreApi = true)]
- public ActionResult GetRobotsTxt()
- {
- return GetWebClientResource("robots.txt");
- }
-
- /// <summary>
- /// Gets a resource from the web client.
- /// </summary>
- /// <param name="resourceName">The resource name.</param>
- /// <response code="200">Web client returned.</response>
- /// <response code="404">Server does not host a web client.</response>
- /// <returns>The resource.</returns>
- [HttpGet("web/{*resourceName}")]
- [ApiExplorerSettings(IgnoreApi = true)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult GetWebClientResource([FromRoute] string resourceName)
- {
- if (!_appConfig.HostWebClient() || WebClientUiPath == null)
- {
- return NotFound("Server does not host a web client.");
- }
-
- var path = resourceName;
- var basePath = WebClientUiPath;
-
- var requestPathAndQuery = Request.GetEncodedPathAndQuery();
- // Bounce them to the startup wizard if it hasn't been completed yet
- if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted
- && !requestPathAndQuery.Contains("wizard", StringComparison.OrdinalIgnoreCase)
- && requestPathAndQuery.Contains("index", StringComparison.OrdinalIgnoreCase))
- {
- return Redirect("index.html?start=wizard#!/wizardstart.html");
- }
-
- var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read);
- return File(stream, MimeTypes.GetMimeType(path));
- }
-
- /// <summary>
- /// Gets the favicon.
- /// </summary>
- /// <response code="200">Favicon.ico returned.</response>
- /// <returns>The favicon.</returns>
- [HttpGet("favicon.ico")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ApiExplorerSettings(IgnoreApi = true)]
- public ActionResult GetFavIcon()
- {
- return GetWebClientResource("favicon.ico");
- }
-
- /// <summary>
- /// Gets the path of the directory containing the static web interface content.
- /// </summary>
- /// <param name="appConfig">The app configuration.</param>
- /// <param name="serverConfigManager">The server configuration manager.</param>
- /// <returns>The directory path, or null if the server is not hosting the web client.</returns>
- public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager)
- {
- if (!appConfig.HostWebClient())
- {
- return null;
- }
-
- if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath))
- {
- return serverConfigManager.Configuration.DashboardSourcePath;
- }
-
- return serverConfigManager.ApplicationPaths.WebPath;
- }
-
private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
{
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 23d10e2156..b3e3490c2a 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Devices Controller.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.RequiresElevation)]
public class DevicesController : BaseJellyfinApiController
{
private readonly IDeviceManager _deviceManager;
@@ -46,9 +46,8 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Devices retrieved.</response>
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery, Required] Guid? userId)
+ public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
{
var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
return _deviceManager.GetDevices(deviceQuery);
@@ -62,10 +61,9 @@ namespace Jellyfin.Api.Controllers
/// <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")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string? id)
+ public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
{
var deviceInfo = _deviceManager.GetDevice(id);
if (deviceInfo == null)
@@ -84,10 +82,9 @@ namespace Jellyfin.Api.Controllers
/// <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")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string? id)
+ public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
{
var deviceInfo = _deviceManager.GetDeviceOptions(id);
if (deviceInfo == null)
@@ -107,11 +104,10 @@ namespace Jellyfin.Api.Controllers
/// <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>
[HttpPost("Options")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateDeviceOptions(
- [FromQuery, Required] string? id,
+ [FromQuery, Required] string id,
[FromBody, Required] DeviceOptions deviceOptions)
{
var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
@@ -134,7 +130,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteDevice([FromQuery, Required] string? id)
+ public ActionResult DeleteDevice([FromQuery, Required] string id)
{
var existingDevice = _deviceManager.GetDevice(id);
if (existingDevice == null)
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index c547d0cde3..76f5717e30 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -43,9 +43,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
- [FromRoute] string? displayPreferencesId,
- [FromQuery] [Required] Guid userId,
- [FromQuery] [Required] string? client)
+ [FromRoute, Required] string displayPreferencesId,
+ [FromQuery, Required] Guid userId,
+ [FromQuery, Required] string client)
{
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
@@ -81,6 +81,9 @@ namespace Jellyfin.Api.Controllers
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
+ // This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
+ _displayPreferencesManager.SaveChanges();
+
return dto;
}
@@ -97,9 +100,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
public ActionResult UpdateDisplayPreferences(
- [FromRoute] string? displayPreferencesId,
+ [FromRoute, Required] string displayPreferencesId,
[FromQuery, Required] Guid userId,
- [FromQuery, Required] string? client,
+ [FromQuery, Required] string client,
[FromBody, Required] DisplayPreferencesDto displayPreferences)
{
HomeSectionType[] defaults =
@@ -153,7 +156,6 @@ namespace Jellyfin.Api.Controllers
{
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Parse(key.Substring("landing-".Length)), existingDisplayPreferences.Client);
itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
- _displayPreferencesManager.SaveChanges(itemPreferences);
}
var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client);
@@ -167,8 +169,7 @@ namespace Jellyfin.Api.Controllers
itemPrefs.ViewType = viewType;
}
- _displayPreferencesManager.SaveChanges(existingDisplayPreferences);
- _displayPreferencesManager.SaveChanges(itemPrefs);
+ _displayPreferencesManager.SaveChanges();
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs
index 397299a73f..052a6aff2e 100644
--- a/Jellyfin.Api/Controllers/DlnaController.cs
+++ b/Jellyfin.Api/Controllers/DlnaController.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Model.Dlna;
@@ -59,7 +60,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Profiles/{profileId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceProfile> GetProfile([FromRoute] string profileId)
+ public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId)
{
var profile = _dlnaManager.GetProfile(profileId);
if (profile == null)
@@ -80,7 +81,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Profiles/{profileId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteProfile([FromRoute] string profileId)
+ public ActionResult DeleteProfile([FromRoute, Required] string profileId)
{
var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
if (existingDeviceProfile == null)
@@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Profiles/{profileId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateProfile([FromRoute] string profileId, [FromBody] DeviceProfile deviceProfile)
+ public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile)
{
var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
if (existingDeviceProfile == null)
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 0100d642be..4e6455eaa7 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -1,6 +1,8 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Net.Mime;
using System.Threading.Tasks;
using Emby.Dlna;
using Emby.Dlna.Main;
@@ -17,8 +19,6 @@ namespace Jellyfin.Api.Controllers
[Route("Dlna")]
public class DlnaServerController : BaseJellyfinApiController
{
- private const string XMLContentType = "text/xml; charset=UTF-8";
-
private readonly IDlnaManager _dlnaManager;
private readonly IContentDirectory _contentDirectory;
private readonly IConnectionManager _connectionManager;
@@ -44,9 +44,10 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
[HttpGet("{serverId}/description")]
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
- [Produces(XMLContentType)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult GetDescriptionXml([FromRoute] string serverId)
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
+ public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
{
var url = GetAbsoluteUri();
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
@@ -61,11 +62,13 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Dlna content directory returned.</response>
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
[HttpGet("{serverId}/ContentDirectory")]
- [HttpGet("{serverId}/ContentDirectory.xml", Name = "GetContentDirectory_2")]
- [Produces(XMLContentType)]
+ [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
+ [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult GetContentDirectory([FromRoute] string serverId)
+ public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
{
return Ok(_contentDirectory.GetServiceXml());
}
@@ -74,13 +77,16 @@ namespace Jellyfin.Api.Controllers
/// Gets Dlna media receiver registrar xml.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/MediaReceiverRegistrar")]
- [HttpGet("{serverId}/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
- [Produces(XMLContentType)]
+ [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
+ [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId)
+ public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
{
return Ok(_mediaReceiverRegistrar.GetServiceXml());
}
@@ -89,13 +95,16 @@ namespace Jellyfin.Api.Controllers
/// Gets Dlna media receiver registrar xml.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/ConnectionManager")]
- [HttpGet("{serverId}/ConnectionManager.xml", Name = "GetConnectionManager_2")]
- [Produces(XMLContentType)]
+ [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
+ [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult GetConnectionManager([FromRoute] string serverId)
+ public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
{
return Ok(_connectionManager.GetServiceXml());
}
@@ -104,9 +113,13 @@ namespace Jellyfin.Api.Controllers
/// Process a content directory control request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ContentDirectory/Control")]
- public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute] string serverId)
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [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);
}
@@ -115,9 +128,13 @@ namespace Jellyfin.Api.Controllers
/// Process a connection manager control request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ConnectionManager/Control")]
- public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute] string serverId)
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [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);
}
@@ -126,9 +143,13 @@ namespace Jellyfin.Api.Controllers
/// Process a media receiver registrar control request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
- public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute] string serverId)
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [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);
}
@@ -137,11 +158,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</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)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
{
return ProcessEventRequest(_mediaReceiverRegistrar);
@@ -151,11 +176,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</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)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
{
return ProcessEventRequest(_contentDirectory);
@@ -165,11 +194,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request.
/// </summary>
/// <param name="serverId">Server UUID.</param>
+ /// <response code="200">Request processed.</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)]
+ [Produces(MediaTypeNames.Text.Xml)]
+ [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
{
return ProcessEventRequest(_connectionManager);
@@ -183,7 +216,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>Icon stream.</returns>
[HttpGet("{serverId}/icons/{fileName}")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName)
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesImageFile]
+ public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
{
return GetIconInternal(fileName);
}
@@ -194,7 +229,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="fileName">The icon filename.</param>
/// <returns>Icon stream.</returns>
[HttpGet("icons/{fileName}")]
- public ActionResult GetIcon([FromRoute] string fileName)
+ [ProducesImageFile]
+ public ActionResult GetIcon([FromRoute, Required] string fileName)
{
return GetIconInternal(fileName);
}
@@ -221,16 +257,15 @@ namespace Jellyfin.Api.Controllers
private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
{
- return service.ProcessControlRequestAsync(new ControlRequest
+ return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers)
{
- Headers = Request.Headers,
InputXml = requestStream,
TargetServerUuId = id,
RequestedUrl = GetAbsoluteUri()
});
}
- private EventSubscriptionResponse ProcessEventRequest(IEventManager eventManager)
+ private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager)
{
var subscriptionId = Request.Headers["SID"];
if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
@@ -241,17 +276,17 @@ namespace Jellyfin.Api.Controllers
if (string.IsNullOrEmpty(notificationType))
{
- return eventManager.RenewEventSubscription(
+ return dlnaEventManager.RenewEventSubscription(
subscriptionId,
notificationType,
timeoutString,
callback);
}
- return eventManager.CreateEventSubscription(notificationType, timeoutString, callback);
+ return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback);
}
- return eventManager.CancelEventSubscription(subscriptionId);
+ return dlnaEventManager.CancelEventSubscription(subscriptionId);
}
}
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index d581ab8cd2..e07690e110 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
@@ -8,12 +8,12 @@ using System.Linq;
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;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
@@ -22,7 +22,6 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
@@ -53,9 +52,9 @@ namespace Jellyfin.Api.Controllers
private readonly IConfiguration _configuration;
private readonly IDeviceManager _deviceManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly INetworkManager _networkManager;
private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper;
+ private readonly DynamicHlsHelper _dynamicHlsHelper;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
@@ -74,8 +73,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> 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="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
+ /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
public DynamicHlsController(
ILibraryManager libraryManager,
IUserManager userManager,
@@ -89,8 +88,8 @@ namespace Jellyfin.Api.Controllers
IConfiguration configuration,
IDeviceManager deviceManager,
TranscodingJobHelper transcodingJobHelper,
- INetworkManager networkManager,
- ILogger<DynamicHlsController> logger)
+ ILogger<DynamicHlsController> logger,
+ DynamicHlsHelper dynamicHlsHelper)
{
_libraryManager = libraryManager;
_userManager = userManager;
@@ -104,8 +103,8 @@ namespace Jellyfin.Api.Controllers
_configuration = configuration;
_deviceManager = deviceManager;
_transcodingJobHelper = transcodingJobHelper;
- _networkManager = networkManager;
_logger = logger;
+ _dynamicHlsHelper = dynamicHlsHelper;
_encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
}
@@ -114,7 +113,6 @@ namespace Jellyfin.Api.Controllers
/// Gets a video hls playlist 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>
@@ -168,9 +166,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Videos/{itemId}/master.m3u8")]
[HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
public async Task<ActionResult> GetMasterHlsVideoPlaylist(
- [FromRoute] Guid itemId,
- [FromRoute] string? container,
+ [FromRoute, Required] Guid itemId,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -179,7 +177,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
- [FromQuery, Required] string? mediaSourceId,
+ [FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
@@ -220,12 +218,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
- var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
- var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new HlsVideoRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -276,15 +271,13 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
- return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
- .ConfigureAwait(false);
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
}
/// <summary>
/// Gets an audio hls playlist 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>
@@ -302,6 +295,7 @@ namespace Jellyfin.Api.Controllers
/// <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>
@@ -338,9 +332,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Audio/{itemId}/master.m3u8")]
[HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesPlaylistFile]
public async Task<ActionResult> GetMasterHlsAudioPlaylist(
- [FromRoute] Guid itemId,
- [FromRoute] string? container,
+ [FromRoute, Required] Guid itemId,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -349,7 +343,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
- [FromQuery, Required] string? mediaSourceId,
+ [FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
@@ -358,6 +352,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@@ -390,12 +385,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
- var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
- var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new HlsAudioRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -413,7 +405,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
@@ -446,15 +438,13 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
- return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource)
- .ConfigureAwait(false);
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
}
/// <summary>
/// Gets a video stream using HTTP live streaming.
/// </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>
@@ -506,9 +496,9 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid itemId,
- [FromRoute] string? container,
+ [FromRoute, Required] Guid itemId,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -561,7 +551,6 @@ namespace Jellyfin.Api.Controllers
var streamingRequest = new VideoRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -619,7 +608,6 @@ namespace Jellyfin.Api.Controllers
/// Gets an audio stream using HTTP live streaming.
/// </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>
@@ -637,6 +625,7 @@ namespace Jellyfin.Api.Controllers
/// <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>
@@ -671,9 +660,9 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid itemId,
- [FromRoute] string? container,
+ [FromRoute, Required] Guid itemId,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -691,6 +680,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@@ -726,7 +716,6 @@ namespace Jellyfin.Api.Controllers
var streamingRequest = new StreamingRequestDto
{
Id = itemId,
- Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
@@ -744,7 +733,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
@@ -838,11 +827,12 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid itemId,
- [FromRoute] string playlistId,
- [FromRoute] int segmentId,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] int segmentId,
[FromRoute] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
@@ -973,6 +963,7 @@ namespace Jellyfin.Api.Controllers
/// <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>
@@ -1007,11 +998,12 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid itemId,
- [FromRoute] string playlistId,
- [FromRoute] int segmentId,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] int segmentId,
[FromRoute] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
@@ -1030,6 +1022,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@@ -1082,7 +1075,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = audioBitRate,
+ AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
@@ -1118,106 +1111,6 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false);
}
- private async Task<ActionResult> GetMasterPlaylistInternal(
- StreamingRequestDto streamingRequest,
- bool isHeadRequest,
- bool enableAdaptiveBitrateStreaming,
- CancellationTokenSource cancellationTokenSource)
- {
- using var state = await StreamingHelpers.GetStreamingState(
- streamingRequest,
- Request,
- _authContext,
- _mediaSourceManager,
- _userManager,
- _libraryManager,
- _serverConfigurationManager,
- _mediaEncoder,
- _fileSystem,
- _subtitleEncoder,
- _configuration,
- _dlnaManager,
- _deviceManager,
- _transcodingJobHelper,
- _transcodingJobType,
- cancellationTokenSource.Token)
- .ConfigureAwait(false);
-
- Response.Headers.Add(HeaderNames.Expires, "0");
- if (isHeadRequest)
- {
- return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
- }
-
- var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0;
-
- var builder = new StringBuilder();
-
- builder.AppendLine("#EXTM3U");
-
- var isLiveStream = state.IsSegmentedLiveStream;
-
- var queryString = Request.QueryString.ToString();
-
- // from universal audio service
- if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
- {
- queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
- }
-
- // from universal audio service
- if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
- {
- queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
- }
-
- // Main stream
- var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
-
- playlistUrl += queryString;
-
- var subtitleStreams = state.MediaSource
- .MediaStreams
- .Where(i => i.IsTextSubtitleStream)
- .ToList();
-
- var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
- ? "subs"
- : null;
-
- // If we're burning in subtitles then don't add additional subs to the manifest
- if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
- {
- subtitleGroup = null;
- }
-
- if (!string.IsNullOrWhiteSpace(subtitleGroup))
- {
- AddSubtitles(state, subtitleStreams, builder);
- }
-
- AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
-
- if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming))
- {
- var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
-
- // By default, vary by just 200k
- var variation = GetBitrateVariation(totalBitrate);
-
- var newBitrate = totalBitrate - variation;
- var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
- AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
-
- variation *= 2;
- newBitrate = totalBitrate - variation;
- variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
- AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
- }
-
- return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
- }
-
private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource)
{
using var state = await StreamingHelpers.GetStreamingState(
@@ -1245,30 +1138,30 @@ namespace Jellyfin.Api.Controllers
var builder = new StringBuilder();
- builder.AppendLine("#EXTM3U");
- builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
- builder.AppendLine("#EXT-X-VERSION:3");
- builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
- builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+ builder.AppendLine("#EXTM3U")
+ .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
+ .AppendLine("#EXT-X-VERSION:3")
+ .Append("#EXT-X-TARGETDURATION:")
+ .Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
+ .AppendLine()
+ .AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
- var queryString = Request.QueryString;
var index = 0;
-
var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
+ var queryString = Request.QueryString;
foreach (var length in segmentLengths)
{
- builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
- builder.AppendLine(
- string.Format(
- CultureInfo.InvariantCulture,
- "hls1/{0}/{1}{2}{3}",
- name,
- index.ToString(CultureInfo.InvariantCulture),
- segmentExtension,
- queryString));
-
- index++;
+ builder.Append("#EXTINF:")
+ .Append(length.ToString("0.0000", CultureInfo.InvariantCulture))
+ .AppendLine(", nodesc")
+ .Append("hls1/")
+ .Append(name)
+ .Append('/')
+ .Append(index++)
+ .Append(segmentExtension)
+ .Append(queryString)
+ .AppendLine();
}
builder.AppendLine("#EXT-X-ENDLIST");
@@ -1411,330 +1304,6 @@ namespace Jellyfin.Api.Controllers
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
- private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
- {
- var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
- const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
-
- foreach (var stream in subtitles)
- {
- var name = stream.DisplayTitle;
-
- var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
- var isForced = stream.IsForced;
-
- var url = string.Format(
- CultureInfo.InvariantCulture,
- "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
- state.Request.MediaSourceId,
- stream.Index.ToString(CultureInfo.InvariantCulture),
- 30.ToString(CultureInfo.InvariantCulture),
- ClaimHelpers.GetToken(Request.HttpContext.User));
-
- var line = string.Format(
- CultureInfo.InvariantCulture,
- Format,
- name,
- isDefault ? "YES" : "NO",
- isForced ? "YES" : "NO",
- url,
- stream.Language ?? "Unknown");
-
- builder.AppendLine(line);
- }
- }
-
- private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
- {
- builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
- .Append(bitrate.ToString(CultureInfo.InvariantCulture))
- .Append(",AVERAGE-BANDWIDTH=")
- .Append(bitrate.ToString(CultureInfo.InvariantCulture));
-
- AppendPlaylistCodecsField(builder, state);
-
- AppendPlaylistResolutionField(builder, state);
-
- AppendPlaylistFramerateField(builder, state);
-
- if (!string.IsNullOrWhiteSpace(subtitleGroup))
- {
- builder.Append(",SUBTITLES=\"")
- .Append(subtitleGroup)
- .Append('"');
- }
-
- builder.Append(Environment.NewLine);
- builder.AppendLine(url);
- }
-
- /// <summary>
- /// Appends a CODECS field containing formatted strings of
- /// the active streams output video and audio codecs.
- /// </summary>
- /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
- /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
- /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
- /// <param name="builder">StringBuilder to append the field to.</param>
- /// <param name="state">StreamState of the current stream.</param>
- private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
- {
- // Video
- string videoCodecs = string.Empty;
- int? videoCodecLevel = GetOutputVideoCodecLevel(state);
- if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
- {
- videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
- }
-
- // Audio
- string audioCodecs = string.Empty;
- if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
- {
- audioCodecs = GetPlaylistAudioCodecs(state);
- }
-
- StringBuilder codecs = new StringBuilder();
-
- codecs.Append(videoCodecs);
-
- if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
- {
- codecs.Append(',');
- }
-
- codecs.Append(audioCodecs);
-
- if (codecs.Length > 1)
- {
- builder.Append(",CODECS=\"")
- .Append(codecs)
- .Append('"');
- }
- }
-
- /// <summary>
- /// Appends a RESOLUTION field containing the resolution of the output stream.
- /// </summary>
- /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
- /// <param name="builder">StringBuilder to append the field to.</param>
- /// <param name="state">StreamState of the current stream.</param>
- private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
- {
- if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
- {
- builder.Append(",RESOLUTION=")
- .Append(state.OutputWidth.GetValueOrDefault())
- .Append('x')
- .Append(state.OutputHeight.GetValueOrDefault());
- }
- }
-
- /// <summary>
- /// Appends a FRAME-RATE field containing the framerate of the output stream.
- /// </summary>
- /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
- /// <param name="builder">StringBuilder to append the field to.</param>
- /// <param name="state">StreamState of the current stream.</param>
- private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
- {
- double? framerate = null;
- if (state.TargetFramerate.HasValue)
- {
- framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
- }
- else if (state.VideoStream?.RealFrameRate != null)
- {
- framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
- }
-
- if (framerate.HasValue)
- {
- builder.Append(",FRAME-RATE=")
- .Append(framerate.Value);
- }
- }
-
- private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming)
- {
- // Within the local network this will likely do more harm than good.
- var ip = RequestHelpers.NormalizeIp(Request.HttpContext.Connection.RemoteIpAddress).ToString();
- if (_networkManager.IsInLocalNetwork(ip))
- {
- return false;
- }
-
- if (!enableAdaptiveBitrateStreaming)
- {
- return false;
- }
-
- if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
- {
- // Opening live streams is so slow it's not even worth it
- return false;
- }
-
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
- {
- return false;
- }
-
- if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
- {
- return false;
- }
-
- if (!state.IsOutputVideo)
- {
- return false;
- }
-
- // Having problems in android
- return false;
- // return state.VideoRequest.VideoBitRate.HasValue;
- }
-
- /// <summary>
- /// Get the H.26X level of the output video stream.
- /// </summary>
- /// <param name="state">StreamState of the current stream.</param>
- /// <returns>H.26X level of the output video stream.</returns>
- private int? GetOutputVideoCodecLevel(StreamState state)
- {
- string? levelString;
- if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && state.VideoStream.Level.HasValue)
- {
- levelString = state.VideoStream?.Level.ToString();
- }
- else
- {
- levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
- }
-
- if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
- {
- return parsedLevel;
- }
-
- return null;
- }
-
- /// <summary>
- /// Gets a formatted string of the output audio codec, for use in the CODECS field.
- /// </summary>
- /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
- /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
- /// <param name="state">StreamState of the current stream.</param>
- /// <returns>Formatted audio codec string.</returns>
- private string GetPlaylistAudioCodecs(StreamState state)
- {
- if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
- {
- string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
- return HlsCodecStringHelpers.GetAACString(profile);
- }
-
- if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetMP3String();
- }
-
- if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetAC3String();
- }
-
- if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
- {
- return HlsCodecStringHelpers.GetEAC3String();
- }
-
- return string.Empty;
- }
-
- /// <summary>
- /// Gets a formatted string of the output video codec, for use in the CODECS field.
- /// </summary>
- /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
- /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
- /// <param name="state">StreamState of the current stream.</param>
- /// <param name="codec">Video codec.</param>
- /// <param name="level">Video level.</param>
- /// <returns>Formatted video codec string.</returns>
- private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
- {
- if (level == 0)
- {
- // This is 0 when there's no requested H.26X level in the device profile
- // and the source is not encoded in H.26X
- _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
- return string.Empty;
- }
-
- if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
- {
- string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
- return HlsCodecStringHelpers.GetH264String(profile, level);
- }
-
- if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
- {
- string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
-
- return HlsCodecStringHelpers.GetH265String(profile, level);
- }
-
- return string.Empty;
- }
-
- private int GetBitrateVariation(int bitrate)
- {
- // By default, vary by just 50k
- var variation = 50000;
-
- if (bitrate >= 10000000)
- {
- variation = 2000000;
- }
- else if (bitrate >= 5000000)
- {
- variation = 1500000;
- }
- else if (bitrate >= 3000000)
- {
- variation = 1000000;
- }
- else if (bitrate >= 2000000)
- {
- variation = 500000;
- }
- else if (bitrate >= 1000000)
- {
- variation = 300000;
- }
- else if (bitrate >= 600000)
- {
- variation = 200000;
- }
- else if (bitrate >= 400000)
- {
- variation = 100000;
- }
-
- return variation;
- }
-
- private string ReplaceBitrate(string url, int oldValue, int newValue)
- {
- return url.Replace(
- "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
- "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
- StringComparison.OrdinalIgnoreCase);
- }
-
private double[] GetSegmentLengths(StreamState state)
{
var result = new List<double>();
@@ -1786,15 +1355,20 @@ namespace Jellyfin.Api.Controllers
segmentFormat = "mpegts";
}
+ var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128
+ ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+ : "128";
+
return string.Format(
CultureInfo.InvariantCulture,
- "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
+ "{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} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
inputModifier,
_encodingHelper.GetInputArgument(state, encodingOptions),
threads,
mapArgs,
GetVideoArguments(state, encodingOptions, startNumber),
GetAudioArguments(state, encodingOptions),
+ maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
segmentFormat,
startNumberParam,
@@ -1885,7 +1459,7 @@ namespace Jellyfin.Api.Controllers
var args = "-codec:v:0 " + codec;
- // if (state.EnableMpegtsM2TsMode)
+ // if (state.EnableMpegtsM2TsMode)
// {
// args += " -mpegts_m2ts_mode 1";
// }
@@ -2089,7 +1663,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
- return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, this);
+ return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext);
}
private long GetEndPositionTicks(StreamState state, int requestedIndex)
diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs
index 64670f7d84..ce88b0b995 100644
--- a/Jellyfin.Api/Controllers/EnvironmentController.cs
+++ b/Jellyfin.Api/Controllers/EnvironmentController.cs
@@ -69,11 +69,11 @@ namespace Jellyfin.Api.Controllers
/// Validates path.
/// </summary>
/// <param name="validatePathDto">Validate request object.</param>
- /// <response code="200">Path validated.</response>
+ /// <response code="204">Path validated.</response>
/// <response code="404">Path not found.</response>
/// <returns>Validation status.</returns>
[HttpPost("ValidatePath")]
- [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto)
{
@@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers
}
}
- return Ok();
+ return NoContent();
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 55ad712007..4e47658b06 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -1,5 +1,5 @@
using System;
-using System.Globalization;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
@@ -9,6 +9,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -46,7 +47,6 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all genres 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">The search term.</param>
@@ -54,22 +54,9 @@ namespace Jellyfin.Api.Controllers
/// <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 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="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="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 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="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>
@@ -81,7 +68,6 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetGenres(
- [FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
@@ -89,22 +75,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
- [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -115,42 +88,22 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = null;
- BaseItem parentItem;
+ User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
- if (userId.HasValue && !userId.Equals(Guid.Empty))
- {
- user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
- }
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
- MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@@ -168,87 +121,20 @@ namespace Jellyfin.Api.Controllers
}
}
- // Studios
- if (!string.IsNullOrEmpty(studios))
+ 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)))
{
- query.StudioIds = studios.Split('|')
- .Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null)
- .Select(i => i!.Id)
- .ToArray();
+ result = _libraryManager.GetMusicGenres(query);
}
-
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ else
{
- 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;
- }
+ result = _libraryManager.GetGenres(query);
}
- var result = new QueryResult<(BaseItem, ItemCounts)>();
-
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, counts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = counts.ItemCount;
- dto.ProgramCount = counts.ProgramCount;
- dto.SeriesCount = counts.SeriesCount;
- dto.EpisodeCount = counts.EpisodeCount;
- dto.MovieCount = counts.MovieCount;
- dto.TrailerCount = counts.TrailerCount;
- dto.AlbumCount = counts.AlbumCount;
- dto.SongCount = counts.SongCount;
- dto.ArtistCount = counts.ArtistCount;
- }
-
- return dto;
- });
-
- return new QueryResult<BaseItemDto>
- {
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
- };
+ var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>
@@ -260,7 +146,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the genre.</returns>
[HttpGet("{genreName}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetGenre([FromRoute] string genreName, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
{
var dtoOptions = new DtoOptions()
.AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index e4a6842bce..054e586ce3 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -1,8 +1,10 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Configuration;
@@ -54,14 +56,15 @@ namespace Jellyfin.Api.Controllers
[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] string itemId, [FromRoute] string segmentId)
+ public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
{
// TODO: Deprecate with new iOS app
var file = segmentId + Path.GetExtension(Request.Path);
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
- return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, this);
+ return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
}
/// <summary>
@@ -74,8 +77,9 @@ namespace Jellyfin.Api.Controllers
[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] string itemId, [FromRoute] string playlistId)
+ public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
var file = playlistId + Path.GetExtension(Request.Path);
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
@@ -112,12 +116,13 @@ namespace Jellyfin.Api.Controllers
// [Authenticated]
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsVideoSegmentLegacy(
- [FromRoute] string itemId,
- [FromRoute] string playlistId,
- [FromRoute] string segmentId,
- [FromRoute] string segmentContainer)
+ [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();
@@ -148,7 +153,7 @@ namespace Jellyfin.Api.Controllers
return Task.CompletedTask;
});
- return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, this);
+ return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, HttpContext);
}
}
}
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index 5285905365..980c3273dd 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net.Mime;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -65,7 +66,8 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<FileStreamResult> GetGeneralImage([FromRoute, Required] string? name, [FromRoute, Required] string? type)
+ [ProducesImageFile]
+ public ActionResult GetGeneralImage([FromRoute, Required] string name, [FromRoute, Required] string type)
{
var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
? "folder"
@@ -110,9 +112,10 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<FileStreamResult> GetRatingImage(
- [FromRoute, Required] string? theme,
- [FromRoute, Required] string? name)
+ [ProducesImageFile]
+ public ActionResult GetRatingImage(
+ [FromRoute, Required] string theme,
+ [FromRoute, Required] string name)
{
return GetImageFile(_applicationPaths.RatingsPath, theme, name);
}
@@ -143,9 +146,10 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<FileStreamResult> GetMediaInfoImage(
- [FromRoute, Required] string? theme,
- [FromRoute, Required] string? name)
+ [ProducesImageFile]
+ public ActionResult GetMediaInfoImage(
+ [FromRoute, Required] string theme,
+ [FromRoute, Required] string name)
{
return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
}
@@ -157,7 +161,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="theme">Theme to search.</param>
/// <param name="name">File name to search for.</param>
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
- private ActionResult<FileStreamResult> GetImageFile(string basePath, string? theme, string? name)
+ private ActionResult GetImageFile(string basePath, string? theme, string? name)
{
var themeFolder = Path.Combine(basePath, theme);
if (Directory.Exists(themeFolder))
@@ -168,7 +172,7 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
var contentType = MimeTypes.GetMimeType(path);
- return File(System.IO.File.OpenRead(path), contentType);
+ return PhysicalFile(path, contentType);
}
}
@@ -181,7 +185,7 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
var contentType = MimeTypes.GetMimeType(path);
- return File(System.IO.File.OpenRead(path), contentType);
+ return PhysicalFile(path, contentType);
}
}
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 8f5c6beb30..4a67c1aede 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Configuration;
@@ -90,8 +92,8 @@ namespace Jellyfin.Api.Controllers
[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] Guid userId,
- [FromRoute] ImageType imageType,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
[FromRoute] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
@@ -107,13 +109,13 @@ namespace Jellyfin.Api.Controllers
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null)
{
- _userManager.ClearProfileImage(user);
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
await _providerManager
- .SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path)
+ .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@@ -136,9 +138,9 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public ActionResult DeleteUserImage(
- [FromRoute] Guid userId,
- [FromRoute] ImageType imageType,
+ public async Task<ActionResult> DeleteUserImage(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
[FromRoute] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
@@ -156,7 +158,7 @@ namespace Jellyfin.Api.Controllers
_logger.LogError(e, "Error deleting user profile image:");
}
- _userManager.ClearProfileImage(user);
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
return NoContent();
}
@@ -174,9 +176,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteItemImage(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
+ public async Task<ActionResult> DeleteItemImage(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
[FromRoute] int? imageIndex = null)
{
var item = _libraryManager.GetItemById(itemId);
@@ -185,7 +187,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- item.DeleteImage(imageType, imageIndex ?? 0);
+ await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false);
return NoContent();
}
@@ -205,8 +207,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> SetItemImage(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
[FromRoute] int? imageIndex = null)
{
var item = _libraryManager.GetItemById(itemId);
@@ -218,7 +220,7 @@ namespace Jellyfin.Api.Controllers
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
- item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -237,10 +239,10 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateItemImageIndex(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
- [FromRoute] int imageIndex,
+ public async Task<ActionResult> UpdateItemImageIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
[FromQuery] int newIndex)
{
var item = _libraryManager.GetItemById(itemId);
@@ -249,7 +251,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- item.SwapImages(imageType, imageIndex, newIndex);
+ await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false);
return NoContent();
}
@@ -264,7 +266,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<ImageInfo>> GetItemImageInfos([FromRoute] Guid itemId)
+ public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -281,7 +283,7 @@ namespace Jellyfin.Api.Controllers
return list;
}
- _libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct
+ await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes are correct
foreach (var image in itemImages)
{
@@ -331,7 +333,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</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="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>
@@ -351,17 +353,18 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetItemImage(
- [FromRoute] Guid itemId,
- [FromRoute] ImageType imageType,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] string? tag,
[FromQuery] bool? cropWhitespace,
- [FromQuery] string? format,
+ [FromQuery] ImageFormat? format,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
@@ -429,24 +432,25 @@ namespace Jellyfin.Api.Controllers
[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] Guid itemId,
- [FromRoute] ImageType imageType,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
+ [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,
- [FromRoute] string tag,
+ [FromRoute, Required] string tag,
[FromQuery] bool? cropWhitespace,
- [FromRoute] string format,
+ [FromRoute, Required] ImageFormat format,
[FromQuery] bool? addPlayedIndicator,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [FromRoute, Required] double percentPlayed,
+ [FromRoute, Required] int unplayedCount,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromRoute, Required] int imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -507,15 +511,16 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetArtistImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [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,
@@ -524,7 +529,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromRoute, Required] int imageIndex)
{
var item = _libraryManager.GetArtist(name);
if (item == null)
@@ -585,15 +590,16 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetGenreImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [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,
@@ -663,15 +669,16 @@ namespace Jellyfin.Api.Controllers
[HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetMusicGenreImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [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,
@@ -741,15 +748,16 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetPersonImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [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,
@@ -819,15 +827,16 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetStudioImage(
- [FromRoute] string name,
- [FromRoute] ImageType imageType,
- [FromRoute] string tag,
- [FromRoute] string format,
- [FromRoute] int? maxWidth,
- [FromRoute] int? maxHeight,
- [FromRoute] double? percentPlayed,
- [FromRoute] int? unplayedCount,
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] string tag,
+ [FromRoute, Required] ImageFormat format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
@@ -897,11 +906,12 @@ namespace Jellyfin.Api.Controllers
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetUserImage(
- [FromRoute] Guid userId,
- [FromRoute] ImageType imageType,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
[FromQuery] string? tag,
- [FromQuery] string? format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -1028,7 +1038,7 @@ namespace Jellyfin.Api.Controllers
ImageType imageType,
int? imageIndex,
string? tag,
- string? format,
+ ImageFormat? format,
int? maxWidth,
int? maxHeight,
double? percentPlayed,
@@ -1118,12 +1128,11 @@ namespace Jellyfin.Api.Controllers
isHeadRequest).ConfigureAwait(false);
}
- private ImageFormat[] GetOutputFormats(string? format)
+ private ImageFormat[] GetOutputFormats(ImageFormat? format)
{
- if (!string.IsNullOrWhiteSpace(format)
- && Enum.TryParse(format, true, out ImageFormat parsedFormat))
+ if (format.HasValue)
{
- return new[] { parsedFormat };
+ return new[] { format.Value };
}
return GetClientSupportedFormats();
@@ -1147,7 +1156,7 @@ namespace Jellyfin.Api.Controllers
var acceptParam = Request.Query[HeaderNames.Accept];
- var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
+ var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
if (!supportsWebP)
{
@@ -1169,7 +1178,7 @@ namespace Jellyfin.Api.Controllers
formats.Add(ImageFormat.Jpg);
formats.Add(ImageFormat.Png);
- if (SupportsFormat(supportedFormats, acceptParam, "gif", true))
+ if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
{
formats.Add(ImageFormat.Gif);
}
@@ -1177,9 +1186,10 @@ namespace Jellyfin.Api.Controllers
return formats.ToArray();
}
- private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
+ private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, ImageFormat format, bool acceptAll)
{
- var mimeType = "image/" + format;
+ var normalized = format.ToString().ToLowerInvariant();
+ var mimeType = "image/" + normalized;
if (requestAcceptTypes.Contains(mimeType))
{
@@ -1191,7 +1201,7 @@ namespace Jellyfin.Api.Controllers
return true;
}
- return string.Equals(acceptParam, format, StringComparison.OrdinalIgnoreCase);
+ return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
}
private async Task<ActionResult> GetImageResult(
@@ -1281,9 +1291,9 @@ namespace Jellyfin.Api.Controllers
Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false)));
// if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified
- if (!(dateImageModified > ifModifiedSinceHeader))
+ if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue)
{
- if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow)
+ if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow)
{
Response.StatusCode = StatusCodes.Status304NotModified;
return new ContentResult();
@@ -1297,8 +1307,7 @@ namespace Jellyfin.Api.Controllers
return NoContent();
}
- var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read);
- return File(stream, imageContentType);
+ return PhysicalFile(imagePath, imageContentType);
}
}
}
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 73bd30c4d1..7f8a2be12e 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -64,14 +65,14 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Songs/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -101,14 +102,14 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Albums/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var album = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -138,14 +139,14 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Playlists/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -175,14 +176,14 @@ namespace Jellyfin.Api.Controllers
[HttpGet("MusicGenres/{name}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
- [FromRoute, Required] string? name,
+ [FromRoute, Required] string name,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@@ -211,14 +212,14 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Artists/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -248,14 +249,14 @@ namespace Jellyfin.Api.Controllers
[HttpGet("MusicGenres/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -285,14 +286,14 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Items/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
- [FromRoute] Guid id,
+ [FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes)
+ [FromQuery] ImageType[] enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -315,9 +316,9 @@ namespace Jellyfin.Api.Controllers
TotalRecordCount = list.Count
};
- if (limit.HasValue)
+ if (limit.HasValue && limit < list.Count)
{
- list = list.Take(limit.Value).ToList();
+ list = list.GetRange(0, limit.Value);
}
var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index afde4a4336..ab73aa4286 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -7,6 +7,7 @@ using System.Net.Mime;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -18,6 +19,7 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -72,7 +74,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute] Guid itemId)
+ public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -248,6 +250,8 @@ namespace Jellyfin.Api.Controllers
/// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
/// </returns>
[HttpGet("Items/RemoteSearch/Image")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesImageFile]
public async Task<ActionResult> GetRemoteSearchImage(
[FromQuery, Required] string imageUrl,
[FromQuery, Required] string providerName)
@@ -260,8 +264,7 @@ namespace Jellyfin.Api.Controllers
var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
if (System.IO.File.Exists(contentPath))
{
- await using var fileStreamExisting = System.IO.File.OpenRead(pointerCachePath);
- return new FileStreamResult(fileStreamExisting, MediaTypeNames.Application.Octet);
+ return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath));
}
}
catch (FileNotFoundException)
@@ -274,10 +277,8 @@ namespace Jellyfin.Api.Controllers
}
await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
-
- // Read the pointer file again
- await using var fileStream = System.IO.File.OpenRead(pointerCachePath);
- return new FileStreamResult(fileStream, MediaTypeNames.Application.Octet);
+ var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
+ return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath));
}
/// <summary>
@@ -291,10 +292,11 @@ namespace Jellyfin.Api.Controllers
/// 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/{id}")]
+ [HttpPost("Items/RemoteSearch/Apply/{itemId}")]
[Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> ApplySearchCriteria(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromBody, Required] RemoteSearchResult searchResult,
[FromQuery] bool replaceAllImages = true)
{
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index 3f5d305c18..49865eb5ee 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -53,7 +54,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult Post(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
[FromQuery] bool replaceAllMetadata = false,
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 4b40c6ada9..0a6ed31ae3 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request)
+ public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers
item.OnMetadataChanged();
- item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (isLockedChanged && item.IsFolder)
{
@@ -110,7 +111,7 @@ namespace Jellyfin.Api.Controllers
foreach (var child in folder.GetRecursiveChildren())
{
child.IsLocked = newLockData;
- child.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
}
@@ -140,7 +141,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Items/{itemId}/MetadataEditor")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute] Guid itemId)
+ public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
@@ -194,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}/ContentType")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, Required] string? contentType)
+ public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 1b8b68313b..7ca5775438 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -1,13 +1,15 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
-using Jellyfin.Data.Entities;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -158,7 +160,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery] string? locationTypes,
- [FromQuery] string? excludeLocationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@@ -181,10 +183,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
- [FromQuery] string? imageTypes,
+ [FromQuery] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? isPlayed,
[FromQuery] string? genres,
@@ -193,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
@@ -266,7 +268,9 @@ namespace Jellyfin.Api.Controllers
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
// Assume all folders inside an EnabledChannel are enabled
- || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id);
+ || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id)
+ // Assume all items inside an EnabledChannel are enabled
+ || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.ChannelId);
var collectionFolders = _libraryManager.GetCollectionFolders(item);
foreach (var collectionFolder in collectionFolders)
@@ -339,7 +343,7 @@ namespace Jellyfin.Api.Controllers
PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
- ImageTypes = RequestHelpers.Split(imageTypes, ',', true).Select(v => Enum.Parse<ImageType>(v, true)).ToArray(),
+ ImageTypes = imageTypes,
VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(),
AdjacentTo = adjacentTo,
ItemIds = RequestHelpers.GetGuids(ids),
@@ -362,7 +366,7 @@ namespace Jellyfin.Api.Controllers
query.CollapseBoxSetItems = false;
}
- foreach (var filter in RequestHelpers.GetFilters(filters!))
+ foreach (var filter in filters)
{
switch (filter)
{
@@ -403,12 +407,9 @@ namespace Jellyfin.Api.Controllers
}
// ExcludeLocationTypes
- if (!string.IsNullOrEmpty(excludeLocationTypes))
+ if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
{
- if (excludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray().Contains(LocationType.Virtual))
- {
- query.IsVirtualItem = false;
- }
+ query.IsVirtualItem = false;
}
if (!string.IsNullOrEmpty(locationTypes))
@@ -527,7 +528,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Users/{userId}/Items/Resume")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
@@ -536,7 +537,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true,
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 4548e202a0..8a872ae133 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -8,6 +8,7 @@ using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -19,6 +20,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Net;
@@ -35,8 +37,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Book = MediaBrowser.Controller.Entities.Book;
-using Movie = Jellyfin.Data.Entities.Movie;
-using MusicAlbum = Jellyfin.Data.Entities.MusicAlbum;
namespace Jellyfin.Api.Controllers
{
@@ -105,7 +105,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult GetFile([FromRoute] Guid itemId)
+ [ProducesFile("video/*", "audio/*")]
+ public ActionResult GetFile([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -113,8 +114,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- using var fileStream = new FileStream(item.Path, FileMode.Open, FileAccess.Read);
- return File(fileStream, MimeTypes.GetMimeType(item.Path));
+ return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path));
}
/// <summary>
@@ -145,7 +145,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ThemeMediaResult> GetThemeSongs(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false)
{
@@ -211,7 +211,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ThemeMediaResult> GetThemeVideos(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false)
{
@@ -276,7 +276,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<AllThemeMediaResult> GetThemeMedia(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] bool inheritFromParent = false)
{
@@ -439,7 +439,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute] Guid itemId, [FromQuery] Guid? userId)
+ public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
var item = _libraryManager.GetItemById(itemId);
@@ -556,7 +556,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Library/Movies/Updated")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult PostUpdatedMovies([FromRoute] string? tmdbId, [FromRoute] string? imdbId)
+ public ActionResult PostUpdatedMovies([FromQuery] string? tmdbId, [FromQuery] string? imdbId)
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
@@ -619,7 +619,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.Download)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult GetDownload([FromRoute] Guid itemId)
+ [ProducesFile("video/*", "audio/*")]
+ public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -648,7 +649,7 @@ namespace Jellyfin.Api.Controllers
if (user != null)
{
- LogDownload(item, user, auth);
+ await LogDownloadAsync(item, user, auth).ConfigureAwait(false);
}
var path = item.Path;
@@ -688,7 +689,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] string? excludeArtistIds,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
@@ -861,17 +862,17 @@ namespace Jellyfin.Api.Controllers
: item;
}
- private void LogDownload(BaseItem item, User user, AuthorizationInfo auth)
+ private async Task LogDownloadAsync(BaseItem item, User user, AuthorizationInfo auth)
{
try
{
- _activityManager.Create(new ActivityLog(
+ await _activityManager.CreateAsync(new ActivityLog(
string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
"UserDownloadingContent",
auth.UserId)
{
ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
- });
+ }).ConfigureAwait(false);
}
catch
{
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index cdab4f356f..94995650cb 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryStructureDto;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
@@ -75,8 +76,8 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> AddVirtualFolder(
[FromQuery] string? name,
[FromQuery] string? collectionType,
- [FromQuery] string[] paths,
- [FromBody] LibraryOptionsDto? libraryOptionsDto,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
+ [FromBody] AddVirtualFolderDto? libraryOptionsDto,
[FromQuery] bool refreshLibrary = false)
{
var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions();
@@ -312,19 +313,17 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Update library options.
/// </summary>
- /// <param name="id">The library name.</param>
- /// <param name="libraryOptions">The library options.</param>
+ /// <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(
- [FromQuery] string? id,
- [FromBody] LibraryOptions? libraryOptions)
+ [FromBody] UpdateLibraryOptionsDto request)
{
- var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
+ var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
- collectionFolder.UpdateLibraryOptions(libraryOptions);
+ collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 97a88bc149..88a7542cee 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
@@ -9,6 +10,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -16,6 +18,7 @@ using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -23,6 +26,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
@@ -142,7 +146,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isDisliked,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
[FromQuery] string? sortBy,
@@ -208,7 +212,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Channels/{channelId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<BaseItemDto> GetChannel([FromRoute] Guid channelId, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@@ -259,7 +263,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool? isMovie,
@@ -346,7 +350,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
@@ -405,7 +409,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Recordings/{recordingId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult<BaseItemDto> GetRecording([FromRoute] Guid recordingId, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@@ -427,7 +431,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Tuners/{tunerId}/Reset")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.DefaultAuthorization)]
- public ActionResult ResetTuner([FromRoute] string tunerId)
+ public ActionResult ResetTuner([FromRoute, Required] string tunerId)
{
AssertUserCanManageLiveTv();
_liveTvManager.ResetTuner(tunerId, CancellationToken.None);
@@ -445,7 +449,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Timers/{timerId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)]
- public async Task<ActionResult<TimerInfoDto>> GetTimer(string timerId)
+ public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
{
return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
}
@@ -557,7 +561,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? genreIds,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] string? seriesTimerId,
[FromQuery] Guid? librarySeriesId,
@@ -588,15 +592,15 @@ namespace Jellyfin.Api.Controllers
IsKids = isKids,
IsSports = isSports,
SeriesTimerId = seriesTimerId,
- Genres = RequestHelpers.Split(genres, ',', true),
+ Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds)
};
- if (!librarySeriesId.Equals(Guid.Empty))
+ if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
{
query.IsSeries = true;
- if (_libraryManager.GetItemById(librarySeriesId ?? Guid.Empty) is Series series)
+ if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
{
query.Name = series.Name;
}
@@ -644,7 +648,7 @@ namespace Jellyfin.Api.Controllers
IsKids = body.IsKids,
IsSports = body.IsSports,
SeriesTimerId = body.SeriesTimerId,
- Genres = RequestHelpers.Split(body.Genres, ',', true),
+ Genres = RequestHelpers.Split(body.Genres, '|', true),
GenreIds = RequestHelpers.GetGuids(body.GenreIds)
};
@@ -701,7 +705,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isSports,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? genreIds,
[FromQuery] string? fields,
[FromQuery] bool? enableUserData,
@@ -743,7 +747,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<BaseItemDto>> GetProgram(
- [FromRoute] string programId,
+ [FromRoute, Required] string programId,
[FromQuery] Guid? userId)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -764,7 +768,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteRecording([FromRoute] Guid recordingId)
+ public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
{
AssertUserCanManageLiveTv();
@@ -791,7 +795,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Timers/{timerId}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CancelTimer([FromRoute] string timerId)
+ public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
{
AssertUserCanManageLiveTv();
await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
@@ -809,7 +813,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> UpdateTimer([FromRoute] string timerId, [FromBody] TimerInfoDto timerInfo)
+ public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
{
AssertUserCanManageLiveTv();
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
@@ -843,7 +847,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute] string timerId)
+ public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
{
var timer = await _liveTvManager.GetSeriesTimer(timerId, CancellationToken.None).ConfigureAwait(false);
if (timer == null)
@@ -883,7 +887,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("SeriesTimers/{timerId}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> CancelSeriesTimer([FromRoute] string timerId)
+ public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
{
AssertUserCanManageLiveTv();
await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
@@ -901,7 +905,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
- public async Task<ActionResult> UpdateSeriesTimer([FromRoute] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
+ public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
{
AssertUserCanManageLiveTv();
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
@@ -933,7 +937,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("This endpoint is obsolete.")]
- public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute] Guid? groupId)
+ public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
{
return NotFound();
}
@@ -1004,7 +1008,7 @@ namespace Jellyfin.Api.Controllers
/// <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>
- [HttpGet("ListingProviders")]
+ [HttpPost("ListingProviders")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
@@ -1014,9 +1018,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool validateListings = false,
[FromQuery] bool validateLogin = false)
{
- using var sha = SHA1.Create();
if (!string.IsNullOrEmpty(pw))
{
+ using var sha = SHA1.Create();
listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw)));
}
@@ -1067,9 +1071,10 @@ namespace Jellyfin.Api.Controllers
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile(MediaTypeNames.Application.Json)]
public async Task<ActionResult> GetSchedulesDirectCountries()
{
- var client = _httpClientFactory.CreateClient();
+ 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("https://json.schedulesdirect.org/20141201/available/countries")
@@ -1175,7 +1180,8 @@ namespace Jellyfin.Api.Controllers
[HttpGet("LiveRecordings/{recordingId}/stream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> GetLiveRecordingFile([FromRoute] string recordingId)
+ [ProducesVideoFile]
+ public async Task<ActionResult> GetLiveRecordingFile([FromRoute, Required] string recordingId)
{
var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
@@ -1205,7 +1211,8 @@ namespace Jellyfin.Api.Controllers
[HttpGet("LiveStreamFiles/{streamId}/stream.{container}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> GetLiveStreamFile([FromRoute] string streamId, [FromRoute] string container)
+ [ProducesVideoFile]
+ public async Task<ActionResult> GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
{
var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false);
if (liveStreamInfo == null)
@@ -1213,11 +1220,8 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- await using var memoryStream = new MemoryStream();
- await new ProgressiveFileCopier(liveStreamInfo, null, _transcodingJobHelper, CancellationToken.None)
- .WriteToAsync(memoryStream, CancellationToken.None)
- .ConfigureAwait(false);
- return File(memoryStream, MimeTypes.GetMimeType("file." + container));
+ var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
+ return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
}
private void AssertUserCanManageLiveTv()
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 517113074c..186024585b 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -1,30 +1,20 @@
using System;
using System.Buffers;
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
using System.Linq;
using System.Net.Mime;
-using System.Text.Json;
-using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos;
using Jellyfin.Api.Models.VideoDtos;
-using Jellyfin.Data.Entities;
-using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -42,12 +32,9 @@ namespace Jellyfin.Api.Controllers
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IDeviceManager _deviceManager;
private readonly ILibraryManager _libraryManager;
- private readonly INetworkManager _networkManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IUserManager _userManager;
private readonly IAuthorizationContext _authContext;
private readonly ILogger<MediaInfoController> _logger;
- private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly MediaInfoHelper _mediaInfoHelper;
/// <summary>
/// Initializes a new instance of the <see cref="MediaInfoController"/> class.
@@ -55,32 +42,23 @@ namespace Jellyfin.Api.Controllers
/// <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="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
- /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
- /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
public MediaInfoController(
IMediaSourceManager mediaSourceManager,
IDeviceManager deviceManager,
ILibraryManager libraryManager,
- INetworkManager networkManager,
- IMediaEncoder mediaEncoder,
- IUserManager userManager,
IAuthorizationContext authContext,
ILogger<MediaInfoController> logger,
- IServerConfigurationManager serverConfigurationManager)
+ MediaInfoHelper mediaInfoHelper)
{
_mediaSourceManager = mediaSourceManager;
_deviceManager = deviceManager;
_libraryManager = libraryManager;
- _networkManager = networkManager;
- _mediaEncoder = mediaEncoder;
- _userManager = userManager;
_authContext = authContext;
_logger = logger;
- _serverConfigurationManager = serverConfigurationManager;
+ _mediaInfoHelper = mediaInfoHelper;
}
/// <summary>
@@ -92,9 +70,12 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid itemId, [FromQuery, Required] Guid? userId)
+ public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId)
{
- return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false);
+ return await _mediaInfoHelper.GetPlaybackInfo(
+ itemId,
+ userId)
+ .ConfigureAwait(false);
}
/// <summary>
@@ -121,9 +102,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
- [FromQuery] long? maxStreamingBitrate,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] long? startTimeTicks,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
@@ -153,7 +134,12 @@ namespace Jellyfin.Api.Controllers
}
}
- var info = await GetPlaybackInfoInternal(itemId, userId, mediaSourceId, liveStreamId).ConfigureAwait(false);
+ var info = await _mediaInfoHelper.GetPlaybackInfo(
+ itemId,
+ userId,
+ mediaSourceId,
+ liveStreamId)
+ .ConfigureAwait(false);
if (profile != null)
{
@@ -162,7 +148,7 @@ namespace Jellyfin.Api.Controllers
foreach (var mediaSource in info.MediaSources)
{
- SetDeviceSpecificData(
+ _mediaInfoHelper.SetDeviceSpecificData(
item,
mediaSource,
profile,
@@ -179,10 +165,11 @@ namespace Jellyfin.Api.Controllers
enableDirectStream,
enableTranscoding,
allowVideoStreamCopy,
- allowAudioStreamCopy);
+ allowAudioStreamCopy,
+ Request.HttpContext.GetNormalizedRemoteIp());
}
- SortMediaSources(info, maxStreamingBitrate);
+ _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
}
if (autoOpenLiveStream)
@@ -191,21 +178,23 @@ namespace Jellyfin.Api.Controllers
if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
{
- var openStreamResult = await OpenMediaSource(new LiveStreamRequest
- {
- AudioStreamIndex = audioStreamIndex,
- DeviceProfile = deviceProfile?.DeviceProfile,
- EnableDirectPlay = enableDirectPlay,
- EnableDirectStream = enableDirectStream,
- 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(
+ Request,
+ new LiveStreamRequest
+ {
+ AudioStreamIndex = audioStreamIndex,
+ DeviceProfile = deviceProfile?.DeviceProfile,
+ EnableDirectPlay = enableDirectPlay,
+ EnableDirectStream = enableDirectStream,
+ 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 };
}
@@ -215,7 +204,7 @@ namespace Jellyfin.Api.Controllers
{
foreach (var mediaSource in info.MediaSources)
{
- NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
+ _mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
}
}
@@ -245,7 +234,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? openToken,
[FromQuery] Guid? userId,
[FromQuery] string? playSessionId,
- [FromQuery] long? maxStreamingBitrate,
+ [FromQuery] int? maxStreamingBitrate,
[FromQuery] long? startTimeTicks,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
@@ -271,7 +260,7 @@ namespace Jellyfin.Api.Controllers
EnableDirectStream = enableDirectStream,
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
};
- return await OpenMediaSource(request).ConfigureAwait(false);
+ return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);
}
/// <summary>
@@ -282,9 +271,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("LiveStreams/Close")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult CloseLiveStream([FromQuery, Required] string? liveStreamId)
+ public async Task<ActionResult> CloseLiveStream([FromQuery, Required] string liveStreamId)
{
- _mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult();
+ await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false);
return NoContent();
}
@@ -299,6 +288,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Produces(MediaTypeNames.Application.Octet)]
+ [ProducesFile(MediaTypeNames.Application.Octet)]
public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400)
{
const int MaxSize = 10_000_000;
@@ -324,454 +314,5 @@ namespace Jellyfin.Api.Controllers
ArrayPool<byte>.Shared.Return(buffer);
}
}
-
- private async Task<PlaybackInfoResponse> GetPlaybackInfoInternal(
- Guid id,
- Guid? userId,
- string? mediaSourceId = null,
- string? liveStreamId = null)
- {
- var user = userId.HasValue && !userId.Equals(Guid.Empty)
- ? _userManager.GetUserById(userId.Value)
- : null;
- var item = _libraryManager.GetItemById(id);
- var result = new PlaybackInfoResponse();
-
- MediaSourceInfo[] mediaSources;
- if (string.IsNullOrWhiteSpace(liveStreamId))
- {
- // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
- var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
-
- if (string.IsNullOrWhiteSpace(mediaSourceId))
- {
- mediaSources = mediaSourcesList.ToArray();
- }
- else
- {
- mediaSources = mediaSourcesList
- .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
- .ToArray();
- }
- }
- else
- {
- var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
-
- mediaSources = new[] { mediaSource };
- }
-
- if (mediaSources.Length == 0)
- {
- result.MediaSources = Array.Empty<MediaSourceInfo>();
-
- result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
- }
- else
- {
- // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
- // Should we move this directly into MediaSourceManager?
- result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
-
- result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
- }
-
- return result;
- }
-
- private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
- {
- mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
- }
-
- private void SetDeviceSpecificData(
- BaseItem item,
- MediaSourceInfo mediaSource,
- DeviceProfile profile,
- AuthorizationInfo auth,
- long? maxBitrate,
- long startTimeTicks,
- string mediaSourceId,
- int? audioStreamIndex,
- int? subtitleStreamIndex,
- int? maxAudioChannels,
- string playSessionId,
- Guid userId,
- bool enableDirectPlay,
- bool enableDirectStream,
- bool enableTranscoding,
- bool allowVideoStreamCopy,
- bool allowAudioStreamCopy)
- {
- var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
-
- var options = new VideoOptions
- {
- MediaSources = new[] { mediaSource },
- Context = EncodingContext.Streaming,
- DeviceId = auth.DeviceId,
- ItemId = item.Id,
- Profile = profile,
- MaxAudioChannels = maxAudioChannels
- };
-
- if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
- {
- options.MediaSourceId = mediaSourceId;
- options.AudioStreamIndex = audioStreamIndex;
- options.SubtitleStreamIndex = subtitleStreamIndex;
- }
-
- var user = _userManager.GetUserById(userId);
-
- if (!enableDirectPlay)
- {
- mediaSource.SupportsDirectPlay = false;
- }
-
- if (!enableDirectStream)
- {
- mediaSource.SupportsDirectStream = false;
- }
-
- if (!enableTranscoding)
- {
- mediaSource.SupportsTranscoding = false;
- }
-
- if (item is Audio)
- {
- _logger.LogInformation(
- "User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
- user.Username,
- user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
- }
- else
- {
- _logger.LogInformation(
- "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
- user.Username,
- user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
- user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
- user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
- }
-
- // Beginning of Playback Determination: Attempt DirectPlay first
- if (mediaSource.SupportsDirectPlay)
- {
- if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
- {
- mediaSource.SupportsDirectPlay = false;
- }
- else
- {
- var supportsDirectStream = mediaSource.SupportsDirectStream;
-
- // Dummy this up to fool StreamBuilder
- mediaSource.SupportsDirectStream = true;
- options.MaxBitrate = maxBitrate;
-
- if (item is Audio)
- {
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
- {
- options.ForceDirectPlay = true;
- }
- }
- else if (item is Video)
- {
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
- {
- options.ForceDirectPlay = true;
- }
- }
-
- // The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.BuildAudioItem(options)
- : streamBuilder.BuildVideoItem(options);
-
- if (streamInfo == null || !streamInfo.IsDirectStream)
- {
- mediaSource.SupportsDirectPlay = false;
- }
-
- // Set this back to what it was
- mediaSource.SupportsDirectStream = supportsDirectStream;
-
- if (streamInfo != null)
- {
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- }
- }
- }
-
- if (mediaSource.SupportsDirectStream)
- {
- if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
- {
- mediaSource.SupportsDirectStream = false;
- }
- else
- {
- options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
-
- if (item is Audio)
- {
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
- {
- options.ForceDirectStream = true;
- }
- }
- else if (item is Video)
- {
- if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
- && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
- {
- options.ForceDirectStream = true;
- }
- }
-
- // The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.BuildAudioItem(options)
- : streamBuilder.BuildVideoItem(options);
-
- if (streamInfo == null || !streamInfo.IsDirectStream)
- {
- mediaSource.SupportsDirectStream = false;
- }
-
- if (streamInfo != null)
- {
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- }
- }
- }
-
- if (mediaSource.SupportsTranscoding)
- {
- options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
-
- // The MediaSource supports direct stream, now test to see if the client supports it
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- ? streamBuilder.BuildAudioItem(options)
- : streamBuilder.BuildVideoItem(options);
-
- if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
- {
- if (streamInfo != null)
- {
- streamInfo.PlaySessionId = playSessionId;
- streamInfo.StartPositionTicks = startTimeTicks;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
- mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- mediaSource.TranscodingContainer = streamInfo.Container;
- mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
- // Do this after the above so that StartPositionTicks is set
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- }
- }
- else
- {
- if (streamInfo != null)
- {
- streamInfo.PlaySessionId = playSessionId;
-
- if (streamInfo.PlayMethod == PlayMethod.Transcode)
- {
- streamInfo.StartPositionTicks = startTimeTicks;
- mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
-
- if (!allowVideoStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
- }
-
- if (!allowAudioStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- }
-
- mediaSource.TranscodingContainer = streamInfo.Container;
- mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
- }
-
- if (!allowAudioStreamCopy)
- {
- mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
- }
-
- mediaSource.TranscodingContainer = streamInfo.Container;
- mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
-
- // Do this after the above so that StartPositionTicks is set
- SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
- }
- }
- }
-
- foreach (var attachment in mediaSource.MediaAttachments)
- {
- attachment.DeliveryUrl = string.Format(
- CultureInfo.InvariantCulture,
- "/Videos/{0}/{1}/Attachments/{2}",
- item.Id,
- mediaSource.Id,
- attachment.Index);
- }
- }
-
- private async Task<LiveStreamResponse> OpenMediaSource(LiveStreamRequest request)
- {
- var authInfo = _authContext.GetAuthorizationInfo(Request);
-
- var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
-
- var profile = request.DeviceProfile;
- if (profile == null)
- {
- var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
- if (caps != null)
- {
- profile = caps.DeviceProfile;
- }
- }
-
- if (profile != null)
- {
- var item = _libraryManager.GetItemById(request.ItemId);
-
- SetDeviceSpecificData(
- item,
- result.MediaSource,
- profile,
- authInfo,
- request.MaxStreamingBitrate,
- request.StartTimeTicks ?? 0,
- result.MediaSource.Id,
- request.AudioStreamIndex,
- request.SubtitleStreamIndex,
- request.MaxAudioChannels,
- request.PlaySessionId,
- request.UserId,
- request.EnableDirectPlay,
- request.EnableDirectStream,
- true,
- true,
- true);
- }
- else
- {
- if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
- {
- result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
- }
- }
-
- // here was a check if (result.MediaSource != null) but Rider said it will never be null
- NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
-
- return result;
- }
-
- private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
- {
- var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
- mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
-
- mediaSource.TranscodeReasons = info.TranscodeReasons;
-
- foreach (var profile in profiles)
- {
- foreach (var stream in mediaSource.MediaStreams)
- {
- if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
- {
- stream.DeliveryMethod = profile.DeliveryMethod;
-
- if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
- {
- stream.DeliveryUrl = profile.Url.TrimStart('-');
- stream.IsExternalUrl = profile.IsExternalUrl;
- }
- }
- }
- }
- }
-
- private long? GetMaxBitrate(long? clientMaxBitrate, User user)
- {
- var maxBitrate = clientMaxBitrate;
- var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
-
- if (remoteClientMaxBitrate <= 0)
- {
- remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
- }
-
- if (remoteClientMaxBitrate > 0)
- {
- var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString());
-
- _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.HttpContext.Connection.RemoteIpAddress.ToString(), isInLocalNetwork);
- if (!isInLocalNetwork)
- {
- maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
- }
- }
-
- return maxBitrate;
- }
-
- private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
- {
- var originalList = result.MediaSources.ToList();
-
- result.MediaSources = result.MediaSources.OrderBy(i =>
- {
- // Nothing beats direct playing a file
- if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
- {
- return 0;
- }
-
- return 1;
- })
- .ThenBy(i =>
- {
- // Let's assume direct streaming a file is just as desirable as direct playing a remote url
- if (i.SupportsDirectPlay || i.SupportsDirectStream)
- {
- return 0;
- }
-
- return 1;
- })
- .ThenBy(i =>
- {
- return i.Protocol switch
- {
- MediaProtocol.File => 0,
- _ => 1,
- };
- })
- .ThenBy(i =>
- {
- if (maxBitrate.HasValue && i.Bitrate.HasValue)
- {
- return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
- }
-
- return 1;
- })
- .ThenBy(originalList.IndexOf)
- .ToArray();
- }
}
}
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 148d8a18e6..afb536e747 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
@@ -84,8 +85,8 @@ namespace Jellyfin.Api.Controllers
IncludeItemTypes = new[]
{
nameof(Movie),
- // typeof(Trailer).Name,
- // typeof(LiveTvProgram).Name
+ // nameof(Trailer),
+ // nameof(LiveTvProgram)
},
// IsMovie = true
OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
@@ -181,7 +182,7 @@ namespace Jellyfin.Api.Controllers
DtoOptions dtoOptions,
RecommendationType type)
{
- var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
+ var itemTypes = new List<string> { nameof(Movie) };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(nameof(Trailer));
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 0d319137a4..e105befe8e 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -1,5 +1,5 @@
using System;
-using System.Globalization;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -46,7 +47,6 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all music genres 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">The search term.</param>
@@ -54,22 +54,9 @@ namespace Jellyfin.Api.Controllers
/// <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 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="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="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 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="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>
@@ -79,8 +66,8 @@ namespace Jellyfin.Api.Controllers
/// <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] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
@@ -88,22 +75,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
- [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -114,42 +88,22 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
- User? user = null;
- BaseItem parentItem;
+ User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
- if (userId.HasValue && !userId.Equals(Guid.Empty))
- {
- user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
- }
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
- MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@@ -167,87 +121,10 @@ namespace Jellyfin.Api.Controllers
}
}
- // Studios
- if (!string.IsNullOrEmpty(studios))
- {
- query.StudioIds = studios.Split('|')
- .Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null)
- .Select(i => i!.Id)
- .ToArray();
- }
-
- foreach (var filter in RequestHelpers.GetFilters(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.GetMusicGenres(query);
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, counts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = counts.ItemCount;
- dto.ProgramCount = counts.ProgramCount;
- dto.SeriesCount = counts.SeriesCount;
- dto.EpisodeCount = counts.EpisodeCount;
- dto.MovieCount = counts.MovieCount;
- dto.TrailerCount = counts.TrailerCount;
- dto.AlbumCount = counts.AlbumCount;
- dto.SongCount = counts.SongCount;
- dto.ArtistCount = counts.ArtistCount;
- }
-
- return dto;
- });
-
- return new QueryResult<BaseItemDto>
- {
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
- };
+ var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes);
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>
@@ -258,7 +135,7 @@ namespace Jellyfin.Api.Controllers
/// <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] string genreName, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index cf3e780b44..0ceda6815c 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using Jellyfin.Api.Constants;
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 3d6a879093..1d9de14d2d 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -44,14 +44,15 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Packages/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PackageInfo>> GetPackageInfo(
- [FromRoute] [Required] string? name,
+ [FromRoute, Required] string name,
[FromQuery] string? assemblyGuid)
{
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
var result = _installationManager.FilterPackages(
- packages,
- name,
- string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)).FirstOrDefault();
+ packages,
+ name,
+ string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid))
+ .FirstOrDefault();
return result;
}
@@ -76,6 +77,7 @@ namespace Jellyfin.Api.Controllers
/// <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>
@@ -84,16 +86,24 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.RequiresElevation)]
public async Task<ActionResult> InstallPackage(
- [FromRoute] [Required] string? name,
+ [FromRoute, Required] string name,
[FromQuery] string? assemblyGuid,
- [FromQuery] string? version)
+ [FromQuery] string? version,
+ [FromQuery] string? repositoryUrl)
{
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
+ if (!string.IsNullOrEmpty(repositoryUrl))
+ {
+ packages = packages.Where(p => p.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+ }
+
var package = _installationManager.GetCompatibleVersions(
packages,
name,
string.IsNullOrEmpty(assemblyGuid) ? Guid.Empty : Guid.Parse(assemblyGuid),
- string.IsNullOrEmpty(version) ? null : Version.Parse(version)).FirstOrDefault();
+ specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version))
+ .FirstOrDefault();
if (package == null)
{
@@ -115,7 +125,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CancelPackageInstallation(
- [FromRoute] [Required] Guid packageId)
+ [FromRoute, Required] Guid packageId)
{
_installationManager.CancelInstallation(packageId);
return NoContent();
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index b6ccec666a..1e0bdb6bc7 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -1,14 +1,16 @@
using System;
-using System.Globalization;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -25,6 +27,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataManager;
/// <summary>
/// Initializes a new instance of the <see cref="PersonsController"/> class.
@@ -32,84 +35,53 @@ namespace Jellyfin.Api.Controllers
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
public PersonsController(
ILibraryManager libraryManager,
IDtoService dtoService,
- IUserManager userManager)
+ IUserManager userManager,
+ IUserDataManager userDataManager)
{
_libraryManager = libraryManager;
_dtoService = dtoService;
_userManager = userManager;
+ _userDataManager = userDataManager;
}
/// <summary>
- /// Gets all persons from a given item, folder, or the entire library.
+ /// Gets all persons.
/// </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">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="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="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="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="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="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="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</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="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">Optional. Include total record count.</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] double? minCommunityRating,
- [FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
- [FromQuery] string? parentId,
[FromQuery] string? fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
+ [FromQuery] ImageType[] enableImageTypes,
+ [FromQuery] string? excludePersonTypes,
[FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery] string? appearsInItemId,
[FromQuery] Guid? userId,
- [FromQuery] string? nameStartsWithOrGreater,
- [FromQuery] string? nameStartsWith,
- [FromQuery] string? nameLessThan,
- [FromQuery] bool? enableImages = true,
- [FromQuery] bool enableTotalRecordCount = true)
+ [FromQuery] bool? enableImages = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
@@ -117,136 +89,28 @@ namespace Jellyfin.Api.Controllers
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
- BaseItem parentItem;
if (userId.HasValue && !userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
- var query = new InternalItemsQuery(user)
+ var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
+ var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
{
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
- StartIndex = startIndex,
- Limit = limit,
- IsFavorite = isFavorite,
- NameLessThan = nameLessThan,
- NameStartsWith = nameStartsWith,
- NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
- MinCommunityRating = minCommunityRating,
- DtoOptions = dtoOptions,
- SearchTerm = searchTerm,
- EnableTotalRecordCount = enableTotalRecordCount
- };
-
- if (!string.IsNullOrWhiteSpace(parentId))
- {
- if (parentItem is Folder)
- {
- query.AncestorIds = new[] { new Guid(parentId) };
- }
- else
- {
- query.ItemIds = new[] { new Guid(parentId) };
- }
- }
-
- // Studios
- if (!string.IsNullOrEmpty(studios))
- {
- query.StudioIds = studios.Split('|')
- .Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null)
- .Select(i => i!.Id)
- .ToArray();
- }
-
- foreach (var filter in RequestHelpers.GetFilters(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 = new QueryResult<(BaseItem, ItemCounts)>();
-
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, counts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = counts.ItemCount;
- dto.ProgramCount = counts.ProgramCount;
- dto.SeriesCount = counts.SeriesCount;
- dto.EpisodeCount = counts.EpisodeCount;
- dto.MovieCount = counts.MovieCount;
- dto.TrailerCount = counts.TrailerCount;
- dto.AlbumCount = counts.AlbumCount;
- dto.SongCount = counts.SongCount;
- dto.ArtistCount = counts.ArtistCount;
- }
-
- return dto;
+ ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true),
+ NameContains = searchTerm,
+ User = user,
+ IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
+ AppearsInItemId = string.IsNullOrEmpty(appearsInItemId) ? Guid.Empty : Guid.Parse(appearsInItemId),
+ Limit = limit ?? 0
});
return new QueryResult<BaseItemDto>
{
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
+ Items = peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray(),
+ TotalRecordCount = peopleItems.Count
};
}
@@ -262,7 +126,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<BaseItemDto> GetPerson([FromRoute] string name, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId)
{
var dtoOptions = new DtoOptions()
.AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 12c87d7c36..0419b2436b 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Playlists;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
@@ -83,12 +84,12 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpPost("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult AddToPlaylist(
- [FromRoute] string? playlistId,
+ public async Task<ActionResult> AddToPlaylist(
+ [FromRoute, Required] Guid playlistId,
[FromQuery] string? ids,
[FromQuery] Guid? userId)
{
- _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty);
+ await _playlistManager.AddToPlaylistAsync(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty).ConfigureAwait(false);
return NoContent();
}
@@ -102,12 +103,12 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult MoveItem(
- [FromRoute] string? playlistId,
- [FromRoute] string? itemId,
- [FromRoute] int newIndex)
+ public async Task<ActionResult> MoveItem(
+ [FromRoute, Required] string playlistId,
+ [FromRoute, Required] string itemId,
+ [FromRoute, Required] int newIndex)
{
- _playlistManager.MoveItem(playlistId, itemId, newIndex);
+ await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
return NoContent();
}
@@ -120,9 +121,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult RemoveFromPlaylist([FromRoute] string? playlistId, [FromQuery] string? entryIds)
+ public async Task<ActionResult> RemoveFromPlaylist([FromRoute, Required] string playlistId, [FromQuery] string? entryIds)
{
- _playlistManager.RemoveFromPlaylist(playlistId, RequestHelpers.Split(entryIds, ',', true));
+ await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false);
return NoContent();
}
@@ -143,15 +144,15 @@ namespace Jellyfin.Api.Controllers
/// <returns>The original playlist items.</returns>
[HttpGet("{playlistId}/Items")]
public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
- [FromRoute] Guid playlistId,
- [FromRoute] Guid userId,
- [FromRoute] int? startIndex,
- [FromRoute] int? limit,
- [FromRoute] string? fields,
- [FromRoute] bool? enableImages,
- [FromRoute] bool? enableUserData,
- [FromRoute] int? imageTypeLimit,
- [FromRoute] string? enableImageTypes)
+ [FromRoute, Required] Guid playlistId,
+ [FromQuery, Required] Guid userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] string? fields,
+ [FromQuery] bool? enableImages,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery] ImageType[] enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
if (playlist == null)
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 0422bfe727..5c15e9a0d7 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
@@ -71,8 +72,8 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> MarkPlayedItem(
- [FromRoute] Guid userId,
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] DateTime? datePlayed)
{
var user = _userManager.GetUserById(userId);
@@ -94,9 +95,9 @@ namespace Jellyfin.Api.Controllers
/// <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}/PlayedItem/{itemId}")]
+ [HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -195,8 +196,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackStart(
- [FromRoute] Guid userId,
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
@@ -245,8 +246,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackProgress(
- [FromRoute] Guid userId,
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] long? positionTicks,
[FromQuery] int? audioStreamIndex,
@@ -297,8 +298,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackStopped(
- [FromRoute] Guid userId,
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
[FromQuery] string? nextMediaType,
[FromQuery] long? positionTicks,
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index b2f34680b0..0f8ceba291 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult UninstallPlugin([FromRoute] Guid pluginId)
+ public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
{
var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
if (plugin == null)
@@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute] Guid pluginId)
+ public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
@@ -113,17 +113,21 @@ namespace Jellyfin.Api.Controllers
[HttpPost("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> UpdatePluginConfiguration([FromRoute] Guid pluginId)
+ public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
return NotFound();
}
- var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
+ var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
.ConfigureAwait(false);
- plugin.UpdateConfiguration(configuration);
+ if (configuration != null)
+ {
+ plugin.UpdateConfiguration(configuration);
+ }
+
return NoContent();
}
@@ -168,7 +172,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("This endpoint should not be used.")]
[HttpPost("RegistrationRecords/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute] string? name)
+ public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
{
return new MBRegistrationRecord
{
@@ -190,7 +194,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("Paid plugins are not supported")]
[HttpGet("Registrations/{name}")]
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
- public ActionResult GetRegistration([FromRoute] string? name)
+ public ActionResult GetRegistration([FromRoute, Required] string name)
{
// TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
// delete all these registration endpoints. They are only kept for compatibility.
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
new file mode 100644
index 0000000000..73da2f906a
--- /dev/null
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -0,0 +1,154 @@
+using System.ComponentModel.DataAnnotations;
+using Jellyfin.Api.Constants;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.QuickConnect;
+using MediaBrowser.Model.QuickConnect;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers
+{
+ /// <summary>
+ /// Quick connect controller.
+ /// </summary>
+ public class QuickConnectController : BaseJellyfinApiController
+ {
+ private readonly IQuickConnect _quickConnect;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="QuickConnectController"/> class.
+ /// </summary>
+ /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
+ public QuickConnectController(IQuickConnect quickConnect)
+ {
+ _quickConnect = quickConnect;
+ }
+
+ /// <summary>
+ /// Gets the current quick connect state.
+ /// </summary>
+ /// <response code="200">Quick connect state returned.</response>
+ /// <returns>The current <see cref="QuickConnectState"/>.</returns>
+ [HttpGet("Status")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QuickConnectState> GetStatus()
+ {
+ _quickConnect.ExpireRequests();
+ return _quickConnect.State;
+ }
+
+ /// <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>
+ [HttpGet("Initiate")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QuickConnectResult> Initiate()
+ {
+ return _quickConnect.TryConnect();
+ }
+
+ /// <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> Connect([FromQuery, Required] string secret)
+ {
+ try
+ {
+ return _quickConnect.CheckRequestStatus(secret);
+ }
+ catch (ResourceNotFoundException)
+ {
+ return NotFound("Unknown secret");
+ }
+ }
+
+ /// <summary>
+ /// Temporarily activates quick connect for five minutes.
+ /// </summary>
+ /// <response code="204">Quick connect has been temporarily activated.</response>
+ /// <response code="403">Quick connect is unavailable on this server.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success.</returns>
+ [HttpPost("Activate")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public ActionResult Activate()
+ {
+ if (_quickConnect.State == QuickConnectState.Unavailable)
+ {
+ return Forbid("Quick connect is unavailable");
+ }
+
+ _quickConnect.Activate();
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Enables or disables quick connect.
+ /// </summary>
+ /// <param name="status">New <see cref="QuickConnectState"/>.</param>
+ /// <response code="204">Quick connect state set successfully.</response>
+ /// <returns>An <see cref="NoContentResult"/> on success.</returns>
+ [HttpPost("Available")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available)
+ {
+ _quickConnect.SetState(status);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Authorizes a pending quick connect request.
+ /// </summary>
+ /// <param name="code">Quick connect code to authorize.</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 ActionResult<bool> Authorize([FromQuery, Required] string code)
+ {
+ var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
+ if (!userId.HasValue)
+ {
+ return Forbid("Unknown user id");
+ }
+
+ return _quickConnect.AuthorizeRequest(userId.Value, code);
+ }
+
+ /// <summary>
+ /// Deauthorize all quick connect devices for the current user.
+ /// </summary>
+ /// <response code="200">All quick connect devices were deleted.</response>
+ /// <returns>The number of devices that were deleted.</returns>
+ [HttpPost("Deauthorize")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<int> Deauthorize()
+ {
+ var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
+ if (!userId.HasValue)
+ {
+ return 0;
+ }
+
+ return _quickConnect.DeleteAllDevices(userId.Value);
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index a203c50b96..5f095443b9 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -7,8 +7,10 @@ using System.Net.Http;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -69,7 +71,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] ImageType? type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
@@ -132,7 +134,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute] Guid itemId)
+ public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute, Required] Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -154,6 +156,7 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
public async Task<ActionResult> GetRemoteImage([FromQuery, Required] string imageUrl)
{
var urlHash = imageUrl.GetMD5();
@@ -191,7 +194,7 @@ namespace Jellyfin.Api.Controllers
}
var contentType = MimeTypes.GetMimeType(contentPath);
- return File(System.IO.File.OpenRead(contentPath), contentType);
+ return PhysicalFile(contentPath, contentType);
}
/// <summary>
@@ -208,7 +211,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DownloadRemoteImage(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery, Required] ImageType type,
[FromQuery] string? imageUrl)
{
@@ -221,7 +224,7 @@ namespace Jellyfin.Api.Controllers
await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None)
.ConfigureAwait(false);
- item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -244,7 +247,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>Task.</returns>
private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
{
- var httpClient = _httpClientFactory.CreateClient();
+ var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last();
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index e672070c06..68e4f0586f 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>The list of scheduled tasks.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable<IScheduledTaskWorker> GetTasks(
+ public IEnumerable<TaskInfo> GetTasks(
[FromQuery] bool? isHidden,
[FromQuery] bool? isEnabled)
{
@@ -57,7 +57,7 @@ namespace Jellyfin.Api.Controllers
}
}
- yield return task;
+ yield return ScheduledTaskHelpers.GetTaskInfo(task);
}
}
@@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{taskId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<TaskInfo> GetTask([FromRoute, Required] string? taskId)
+ public ActionResult<TaskInfo> GetTask([FromRoute, Required] string taskId)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
@@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Running/{taskId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult StartTask([FromRoute] string? taskId)
+ public ActionResult StartTask([FromRoute, Required] string taskId)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
@@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Running/{taskId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult StopTask([FromRoute, Required] string? taskId)
+ public ActionResult StopTask([FromRoute, Required] string taskId)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateTask(
- [FromRoute, Required] string? taskId,
+ [FromRoute, Required] string taskId,
[FromBody, Required] TaskTriggerInfo[] triggerInfos)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index e159a96666..62c870cb1f 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -81,7 +81,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] Guid? userId,
- [FromQuery, Required] string? searchTerm,
+ [FromQuery, Required] string searchTerm,
[FromQuery] string? includeItemTypes,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? mediaTypes,
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 48b57bdb7f..e506ac7bf1 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CA1801
-
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -7,6 +5,7 @@ using System.Linq;
using System.Threading;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
@@ -125,10 +124,10 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DisplayContent(
- [FromRoute, Required] string? sessionId,
- [FromQuery, Required] string? itemType,
- [FromQuery, Required] string? itemId,
- [FromQuery, Required] string? itemName)
+ [FromRoute, Required] string sessionId,
+ [FromQuery, Required] string itemType,
+ [FromQuery, Required] string itemId,
+ [FromQuery, Required] string itemName)
{
var command = new BrowseRequest
{
@@ -150,30 +149,26 @@ namespace Jellyfin.Api.Controllers
/// 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="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="playRequest">The <see cref="PlayRequest"/>.</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 ActionResult Play(
- [FromRoute, Required] string? sessionId,
- [FromQuery] Guid[] itemIds,
- [FromQuery] long? startPositionTicks,
- [FromQuery] PlayCommand playCommand,
- [FromBody, Required] PlayRequest playRequest)
+ [FromRoute, Required] string sessionId,
+ [FromQuery, Required] PlayCommand playCommand,
+ [FromQuery, Required] string itemIds,
+ [FromQuery] long? startPositionTicks)
{
- if (playRequest == null)
+ var playRequest = new PlayRequest
{
- throw new ArgumentException("Request Body may not be null");
- }
-
- playRequest.ItemIds = itemIds;
- playRequest.StartPositionTicks = startPositionTicks;
- playRequest.PlayCommand = playCommand;
+ ItemIds = RequestHelpers.GetGuids(itemIds),
+ StartPositionTicks = startPositionTicks,
+ PlayCommand = playCommand
+ };
_sessionManager.SendPlayCommand(
RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
@@ -188,20 +183,29 @@ namespace Jellyfin.Api.Controllers
/// Issues a playstate command to a client.
/// </summary>
/// <param name="sessionId">The session id.</param>
- /// <param name="playstateRequest">The <see cref="PlaystateRequest"/>.</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 ActionResult SendPlaystateCommand(
- [FromRoute, Required] string? sessionId,
- [FromBody] PlaystateRequest playstateRequest)
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] PlaystateCommand command,
+ [FromQuery] long? seekPositionTicks,
+ [FromQuery] string? controllingUserId)
{
_sessionManager.SendPlaystateCommand(
RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id,
sessionId,
- playstateRequest,
+ new PlaystateRequest()
+ {
+ Command = command,
+ ControllingUserId = controllingUserId,
+ SeekPositionTicks = seekPositionTicks,
+ },
CancellationToken.None);
return NoContent();
@@ -218,19 +222,13 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendSystemCommand(
- [FromRoute, Required] string? sessionId,
- [FromRoute, Required] string? command)
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] GeneralCommandType command)
{
- var name = command;
- if (Enum.TryParse(name, true, out GeneralCommandType commandType))
- {
- name = commandType.ToString();
- }
-
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
var generalCommand = new GeneralCommand
{
- Name = name,
+ Name = command,
ControllingUserId = currentSession.UserId
};
@@ -250,8 +248,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendGeneralCommand(
- [FromRoute, Required] string? sessionId,
- [FromRoute, Required] string? command)
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] GeneralCommandType command)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -277,7 +275,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendFullGeneralCommand(
- [FromRoute, Required] string? sessionId,
+ [FromRoute, Required] string sessionId,
[FromBody, Required] GeneralCommand command)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -311,9 +309,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendMessageCommand(
- [FromRoute, Required] string? sessionId,
- [FromQuery, Required] string? text,
- [FromQuery, Required] string? header,
+ [FromRoute, Required] string sessionId,
+ [FromQuery, Required] string text,
+ [FromQuery] string? header,
[FromQuery] long? timeoutMs)
{
var command = new MessageCommand
@@ -339,8 +337,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddUserToSession(
- [FromRoute, Required] string? sessionId,
- [FromRoute] Guid userId)
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] Guid userId)
{
_sessionManager.AddAdditionalUser(sessionId, userId);
return NoContent();
@@ -357,8 +355,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveUserFromSession(
- [FromRoute] string? sessionId,
- [FromRoute] Guid userId)
+ [FromRoute, Required] string sessionId,
+ [FromRoute, Required] Guid userId)
{
_sessionManager.RemoveAdditionalUser(sessionId, userId);
return NoContent();
@@ -379,9 +377,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostCapabilities(
- [FromQuery, Required] string? id,
+ [FromQuery] string? id,
[FromQuery] string? playableMediaTypes,
- [FromQuery] string? supportedCommands,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false,
[FromQuery] bool supportsPersistentIdentifier = true)
@@ -394,7 +392,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.ReportCapabilities(id, new ClientCapabilities
{
PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true),
- SupportedCommands = RequestHelpers.Split(supportedCommands, ',', true),
+ SupportedCommands = supportedCommands,
SupportsMediaControl = supportsMediaControl,
SupportsSync = supportsSync,
SupportsPersistentIdentifier = supportsPersistentIdentifier
@@ -413,7 +411,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostFullCapabilities(
- [FromQuery, Required] string? id,
+ [FromQuery] string? id,
[FromBody, Required] ClientCapabilities capabilities)
{
if (string.IsNullOrWhiteSpace(id))
@@ -438,9 +436,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult ReportViewing(
[FromQuery] string? sessionId,
- [FromQuery] string? itemId)
+ [FromQuery, Required] string? itemId)
{
- string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
+ string session = sessionId ?? RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
_sessionManager.ReportNowViewingItem(session, itemId);
return NoContent();
@@ -480,7 +478,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <response code="200">Password reset providers retrieved.</response>
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
- [HttpGet("Auto/PasswordResetProviders")]
+ [HttpGet("Auth/PasswordResetProviders")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.RequiresElevation)]
public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index c8e3cc4f52..9c259cc198 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
@@ -64,21 +65,16 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Sets the initial startup wizard configuration.
/// </summary>
- /// <param name="uiCulture">The UI language culture.</param>
- /// <param name="metadataCountryCode">The metadata country code.</param>
- /// <param name="preferredMetadataLanguage">The preferred language for metadata.</param>
+ /// <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(
- [FromForm] string? uiCulture,
- [FromForm] string? metadataCountryCode,
- [FromForm] string? preferredMetadataLanguage)
+ public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration)
{
- _config.Configuration.UICulture = uiCulture;
- _config.Configuration.MetadataCountryCode = metadataCountryCode;
- _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage;
+ _config.Configuration.UICulture = startupConfiguration.UICulture;
+ _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode;
+ _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage;
_config.SaveConfiguration();
return NoContent();
}
@@ -86,16 +82,15 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Sets remote access and UPnP.
/// </summary>
- /// <param name="enableRemoteAccess">Enable remote access.</param>
- /// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
+ /// <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([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
+ public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto)
{
- _config.Configuration.EnableRemoteAccess = enableRemoteAccess;
- _config.Configuration.EnableUPnP = enableAutomaticPortMapping;
+ _config.Configuration.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
+ _config.Configuration.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
_config.SaveConfiguration();
return NoContent();
}
@@ -131,7 +126,7 @@ namespace Jellyfin.Api.Controllers
/// </returns>
[HttpPost("User")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> UpdateStartupUser([FromForm] StartupUserDto startupUserDto)
+ public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto)
{
var user = _userManager.Users.First();
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index 6f2787d933..c5fcfb3569 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -1,5 +1,5 @@
using System;
-using System.Linq;
+using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -44,7 +45,6 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all studios 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>
@@ -52,22 +52,10 @@ namespace Jellyfin.Api.Controllers
/// <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 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. 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="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>
@@ -79,7 +67,6 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetStudios(
- [FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
@@ -87,22 +74,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
- [FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -115,44 +90,23 @@ namespace Jellyfin.Api.Controllers
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- User? user = null;
- BaseItem parentItem;
+ User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
- if (userId.HasValue && !userId.Equals(Guid.Empty))
- {
- user = _userManager.GetUserById(userId.Value);
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
- }
- else
- {
- parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
- }
+ var parentItem = _libraryManager.GetParentItem(parentId, userId);
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
- var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = excludeItemTypesArr,
IncludeItemTypes = includeItemTypesArr,
- MediaTypes = mediaTypesArr,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
- Tags = RequestHelpers.Split(tags, ',', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
- Genres = RequestHelpers.Split(genres, ',', true),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
- Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
- MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@@ -170,84 +124,9 @@ namespace Jellyfin.Api.Controllers
}
}
- // Studios
- if (!string.IsNullOrEmpty(studios))
- {
- query.StudioIds = studios.Split('|').Select(i =>
- {
- try
- {
- return _libraryManager.GetStudio(i);
- }
- catch
- {
- return null;
- }
- }).Where(i => i != null).Select(i => i!.Id)
- .ToArray();
- }
-
- foreach (var filter in RequestHelpers.GetFilters(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 = new QueryResult<(BaseItem, ItemCounts)>();
- var dtos = result.Items.Select(i =>
- {
- var (baseItem, itemCounts) = i;
- var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
-
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
- {
- dto.ChildCount = itemCounts.ItemCount;
- dto.ProgramCount = itemCounts.ProgramCount;
- dto.SeriesCount = itemCounts.SeriesCount;
- dto.EpisodeCount = itemCounts.EpisodeCount;
- dto.MovieCount = itemCounts.MovieCount;
- dto.TrailerCount = itemCounts.TrailerCount;
- dto.AlbumCount = itemCounts.AlbumCount;
- dto.SongCount = itemCounts.SongCount;
- dto.ArtistCount = itemCounts.ArtistCount;
- }
-
- return dto;
- });
-
- return new QueryResult<BaseItemDto>
- {
- Items = dtos.ToArray(),
- TotalRecordCount = result.TotalRecordCount
- };
+ var result = _libraryManager.GetStudios(query);
+ var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
+ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>
@@ -259,7 +138,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the studio.</returns>
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<BaseItemDto> GetStudio([FromRoute] string name, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 8550a86e0c..4c9a153840 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -9,7 +9,9 @@ using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Models.SubtitleDtos;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -93,8 +95,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Task> DeleteSubtitle(
- [FromRoute] Guid itemId,
- [FromRoute] int index)
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int index)
{
var item = _libraryManager.GetItemById(itemId);
@@ -119,8 +121,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
- [FromRoute] Guid itemId,
- [FromRoute, Required] string? language,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string language,
[FromQuery] bool? isPerfectMatch)
{
var video = (Video)_libraryManager.GetItemById(itemId);
@@ -139,8 +141,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DownloadRemoteSubtitles(
- [FromRoute] Guid itemId,
- [FromRoute, Required] string? subtitleId)
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string subtitleId)
{
var video = (Video)_libraryManager.GetItemById(itemId);
@@ -169,7 +171,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Application.Octet)]
- public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string? id)
+ [ProducesFile("text/*")]
+ public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string id)
{
var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
@@ -192,11 +195,12 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile("text/*")]
public async Task<ActionResult> GetSubtitle(
[FromRoute, Required] Guid itemId,
- [FromRoute, Required] string? mediaSourceId,
+ [FromRoute, Required] string mediaSourceId,
[FromRoute, Required] int index,
- [FromRoute, Required] string? format,
+ [FromRoute, Required] string format,
[FromQuery] long? endPositionTicks,
[FromQuery] bool copyTimestamps = false,
[FromQuery] bool addVttTimeMap = false,
@@ -218,8 +222,7 @@ namespace Jellyfin.Api.Controllers
var subtitleStream = mediaSource.MediaStreams
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
- FileStream stream = new FileStream(subtitleStream.Path, FileMode.Open, FileAccess.Read);
- return File(stream, MimeTypes.GetMimeType(subtitleStream.Path));
+ return PhysicalFile(subtitleStream.Path, MimeTypes.GetMimeType(subtitleStream.Path));
}
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
@@ -258,11 +261,12 @@ namespace Jellyfin.Api.Controllers
[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] Guid itemId,
- [FromRoute] int index,
- [FromRoute] string? mediaSourceId,
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int index,
+ [FromRoute, Required] string mediaSourceId,
[FromQuery, Required] int segmentLength)
{
var item = (Video)_libraryManager.GetItemById(itemId);
@@ -285,7 +289,8 @@ namespace Jellyfin.Api.Controllers
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U")
.Append("#EXT-X-TARGETDURATION:")
- .AppendLine(segmentLength.ToString(CultureInfo.InvariantCulture))
+ .Append(segmentLength)
+ .AppendLine()
.AppendLine("#EXT-X-VERSION:3")
.AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
@@ -300,8 +305,9 @@ namespace Jellyfin.Api.Controllers
var lengthTicks = Math.Min(remaining, segmentLengthTicks);
builder.Append("#EXTINF:")
- .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture))
- .AppendLine(",");
+ .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds)
+ .Append(',')
+ .AppendLine();
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
@@ -322,6 +328,33 @@ namespace Jellyfin.Api.Controllers
}
/// <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")]
+ 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);
+ await using var memoryStream = new MemoryStream(data);
+ await _subtitleManager.UploadSubtitle(
+ video,
+ new SubtitleResponse
+ {
+ Format = body.Format,
+ Language = body.Language,
+ IsForced = body.IsForced,
+ Stream = memoryStream
+ }).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
/// Encodes a subtitle in the specified format.
/// </summary>
/// <param name="id">The media id.</param>
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index 42db6b6a11..ad64adfbad 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,5 +1,7 @@
using System;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
+using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
@@ -8,6 +10,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -17,6 +20,7 @@ namespace Jellyfin.Api.Controllers
/// The suggestions controller.
/// </summary>
[Route("")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class SuggestionsController : BaseJellyfinApiController
{
private readonly IDtoService _dtoService;
@@ -53,7 +57,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Users/{userId}/Suggestions")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromQuery] string? mediaType,
[FromQuery] string? type,
[FromQuery] int? startIndex,
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index bbfd163de5..4cb1984a2f 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -3,10 +3,13 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -176,8 +179,8 @@ namespace Jellyfin.Api.Controllers
{
return new EndPointInfo
{
- IsLocal = Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress),
- IsInNetwork = _network.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString())
+ IsLocal = HttpContext.IsLocal(),
+ IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())
};
}
@@ -190,14 +193,14 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Logs/Log")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult GetLogFile([FromQuery, Required] string? name)
+ [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);
return File(stream, "text/plain");
}
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 5157b08ae5..281c0016e9 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -1,6 +1,8 @@
using System;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -124,7 +126,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery] string? locationTypes,
- [FromQuery] string? excludeLocationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@@ -146,10 +148,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? parentId,
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
- [FromQuery] string? imageTypes,
+ [FromQuery] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? isPlayed,
[FromQuery] string? genres,
@@ -158,7 +160,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index f463ab8894..49a6c386fe 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -69,7 +70,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("NextUp")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetNextUp(
- [FromQuery, Required] Guid? userId,
+ [FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? fields,
@@ -77,7 +78,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? parentId,
[FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
{
@@ -127,14 +128,14 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Upcoming")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetUpcomingEpisodes(
- [FromQuery, Required] Guid? userId,
+ [FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] string? parentId,
[FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@@ -194,8 +195,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
- [FromRoute, Required] string? seriesId,
- [FromQuery, Required] Guid? userId,
+ [FromRoute, Required] string seriesId,
+ [FromQuery] Guid? userId,
[FromQuery] string? fields,
[FromQuery] int? season,
[FromQuery] string? seasonId,
@@ -206,7 +207,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] string? sortBy)
{
@@ -317,15 +318,15 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
- [FromRoute, Required] string? seriesId,
- [FromQuery, Required] Guid? userId,
+ [FromRoute, Required] string seriesId,
+ [FromQuery] Guid? userId,
[FromQuery] string? fields,
[FromQuery] bool? isSpecialSeason,
[FromQuery] bool? isMissing,
[FromQuery] string? adjacentTo,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 75df16aa7b..e10f1fe912 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -1,18 +1,24 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
-using System.Net.Http;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
-using Jellyfin.Api.Models.VideoDtos;
+using Jellyfin.Api.Models.StreamingDtos;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
{
@@ -23,27 +29,39 @@ namespace Jellyfin.Api.Controllers
public class UniversalAudioController : BaseJellyfinApiController
{
private readonly IAuthorizationContext _authorizationContext;
- private readonly MediaInfoController _mediaInfoController;
- private readonly DynamicHlsController _dynamicHlsController;
- private readonly AudioController _audioController;
+ private readonly IDeviceManager _deviceManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<UniversalAudioController> _logger;
+ private readonly MediaInfoHelper _mediaInfoHelper;
+ private readonly AudioHelper _audioHelper;
+ private readonly DynamicHlsHelper _dynamicHlsHelper;
/// <summary>
/// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
/// </summary>
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
- /// <param name="mediaInfoController">Instance of the <see cref="MediaInfoController"/>.</param>
- /// <param name="dynamicHlsController">Instance of the <see cref="DynamicHlsController"/>.</param>
- /// <param name="audioController">Instance of the <see cref="AudioController"/>.</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{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(
IAuthorizationContext authorizationContext,
- MediaInfoController mediaInfoController,
- DynamicHlsController dynamicHlsController,
- AudioController audioController)
+ IDeviceManager deviceManager,
+ ILibraryManager libraryManager,
+ ILogger<UniversalAudioController> logger,
+ MediaInfoHelper mediaInfoHelper,
+ AudioHelper audioHelper,
+ DynamicHlsHelper dynamicHlsHelper)
{
_authorizationContext = authorizationContext;
- _mediaInfoController = mediaInfoController;
- _dynamicHlsController = dynamicHlsController;
- _audioController = audioController;
+ _deviceManager = deviceManager;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _mediaInfoHelper = mediaInfoHelper;
+ _audioHelper = audioHelper;
+ _dynamicHlsHelper = dynamicHlsHelper;
}
/// <summary>
@@ -58,6 +76,7 @@ namespace Jellyfin.Api.Controllers
/// <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>
@@ -70,22 +89,22 @@ namespace Jellyfin.Api.Controllers
/// <response code="302">Redirected to remote audio stream.</response>
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/universal")]
- [HttpGet("Audio/{itemId}/{universal=universal}.{container?}", Name = "GetUniversalAudioStream_2")]
[HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
- [HttpHead("Audio/{itemId}/{universal=universal}.{container?}", Name = "HeadUniversalAudioStream_2")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status302Found)]
+ [ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream(
- [FromRoute] Guid itemId,
- [FromRoute] string? container,
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] string? container,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
[FromQuery] string? audioCodec,
[FromQuery] int? maxAudioChannels,
[FromQuery] int? transcodingAudioChannels,
- [FromQuery] long? maxStreamingBitrate,
+ [FromQuery] int? maxStreamingBitrate,
+ [FromQuery] int? audioBitRate,
[FromQuery] long? startTimeTicks,
[FromQuery] string? transcodingContainer,
[FromQuery] string? transcodingProtocol,
@@ -95,24 +114,68 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool breakOnNonKeyFrames,
[FromQuery] bool enableRedirection = true)
{
- bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
_authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
- var playbackInfoResult = await _mediaInfoController.GetPostedPlaybackInfo(
- itemId,
- userId,
- maxStreamingBitrate,
- startTimeTicks,
- null,
- null,
- maxAudioChannels,
- mediaSourceId,
- null,
- new DeviceProfileDto { DeviceProfile = deviceProfile })
+ var authInfo = _authorizationContext.GetAuthorizationInfo(Request);
+
+ _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
+
+ if (deviceProfile == null)
+ {
+ var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId);
+ if (clientCapabilities != null)
+ {
+ deviceProfile = clientCapabilities.DeviceProfile;
+ }
+ }
+
+ var info = await _mediaInfoHelper.GetPlaybackInfo(
+ itemId,
+ userId,
+ mediaSourceId)
.ConfigureAwait(false);
- var mediaSource = playbackInfoResult.Value.MediaSources[0];
+ if (deviceProfile != null)
+ {
+ // set device specific data
+ var item = _libraryManager.GetItemById(itemId);
+
+ foreach (var sourceInfo in info.MediaSources)
+ {
+ _mediaInfoHelper.SetDeviceSpecificData(
+ item,
+ sourceInfo,
+ deviceProfile,
+ authInfo,
+ maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
+ startTimeTicks ?? 0,
+ mediaSourceId ?? string.Empty,
+ null,
+ null,
+ maxAudioChannels,
+ info!.PlaySessionId!,
+ userId ?? Guid.Empty,
+ true,
+ true,
+ true,
+ true,
+ true,
+ Request.HttpContext.GetNormalizedRemoteIp());
+ }
+
+ _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
+ }
+
+ if (info.MediaSources != null)
+ {
+ foreach (var source in info.MediaSources)
+ {
+ _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile!, DlnaProfileType.Video);
+ }
+ }
+
+ var mediaSource = info.MediaSources![0];
if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
{
if (enableRedirection)
@@ -127,129 +190,71 @@ namespace Jellyfin.Api.Controllers
var isStatic = mediaSource.SupportsDirectStream;
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
{
- var transcodingProfile = deviceProfile.TranscodingProfiles[0];
-
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
// TODO: remove this when we switch back to the segment muxer
var supportedHlsContainers = new[] { "mpegts", "fmp4" };
- if (isHeadRequest)
+ var dynamicHlsRequestDto = new HlsAudioRequestDto
{
- _dynamicHlsController.Request.Method = HttpMethod.Head.Method;
- }
-
- return await _dynamicHlsController.GetMasterHlsAudioPlaylist(
- itemId,
- ".m3u8",
- isStatic,
- null,
- null,
- null,
- playbackInfoResult.Value.PlaySessionId,
+ Id = itemId,
+ Container = ".m3u8",
+ Static = isStatic,
+ PlaySessionId = info.PlaySessionId,
// fallback to mpegts if device reports some weird value unsupported by hls
- Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
- null,
- null,
- mediaSource.Id,
- deviceId,
- transcodingProfile.AudioCodec,
- null,
- null,
- null,
- transcodingProfile.BreakOnNonKeyFrames,
- maxAudioSampleRate,
- maxAudioBitDepth,
- null,
- isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
- maxAudioChannels,
- null,
- null,
- null,
- null,
- null,
- startTimeTicks,
- null,
- null,
- null,
- null,
- SubtitleDeliveryMethod.Hls,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
- null,
- null,
- EncodingContext.Static,
- new Dictionary<string, string>())
+ SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
+ 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 = true,
+ DeInterlace = true,
+ RequireNonAnamorphic = true,
+ EnableMpegtsM2TsMode = true,
+ TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+ Context = EncodingContext.Static,
+ StreamOptions = new Dictionary<string, string>(),
+ EnableAdaptiveBitrateStreaming = true
+ };
+
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
.ConfigureAwait(false);
}
- else
+
+ var audioStreamingDto = new StreamingRequestDto
{
- if (isHeadRequest)
- {
- _audioController.Request.Method = HttpMethod.Head.Method;
- }
+ 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 ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = maxAudioChannels,
+ CopyTimestamps = true,
+ StartTimeTicks = startTimeTicks,
+ SubtitleMethod = SubtitleDeliveryMethod.Embed,
+ TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
+ Context = EncodingContext.Static
+ };
- return await _audioController.GetAudioStream(
- itemId,
- isStatic ? null : ("." + mediaSource.TranscodingContainer),
- isStatic,
- null,
- null,
- null,
- playbackInfoResult.Value.PlaySessionId,
- null,
- null,
- null,
- mediaSource.Id,
- deviceId,
- audioCodec,
- null,
- null,
- null,
- breakOnNonKeyFrames,
- maxAudioSampleRate,
- maxAudioBitDepth,
- isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
- null,
- maxAudioChannels,
- null,
- null,
- null,
- null,
- null,
- startTimeTicks,
- null,
- null,
- null,
- null,
- SubtitleDeliveryMethod.Embed,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
- null,
- null,
- null,
- null)
- .ConfigureAwait(false);
- }
+ return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false);
}
private DeviceProfile GetDeviceProfile(
@@ -265,20 +270,24 @@ namespace Jellyfin.Api.Controllers
{
var deviceProfile = new DeviceProfile();
- var directPlayProfiles = new List<DirectPlayProfile>();
-
var containers = RequestHelpers.Split(container, ',', true);
-
- foreach (var cont in containers)
+ int len = containers.Length;
+ var directPlayProfiles = new DirectPlayProfile[len];
+ for (int i = 0; i < len; i++)
{
- var parts = RequestHelpers.Split(cont, ',', true);
+ var parts = RequestHelpers.Split(containers[i], '|', true);
- var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
+ var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
- directPlayProfiles.Add(new DirectPlayProfile { Type = DlnaProfileType.Audio, Container = parts[0], AudioCodec = audioCodecs });
+ directPlayProfiles[i] = new DirectPlayProfile
+ {
+ Type = DlnaProfileType.Audio,
+ Container = parts[0],
+ AudioCodec = audioCodecs
+ };
}
- deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
+ deviceProfile.DirectPlayProfiles = directPlayProfiles;
deviceProfile.TranscodingProfiles = new[]
{
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 2723125224..0f7c25d0e4 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -7,6 +7,7 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.UserDtos;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
@@ -108,7 +109,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.IgnoreParentalControl)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<UserDto> GetUserById([FromRoute] Guid userId)
+ public ActionResult<UserDto> GetUserById([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
@@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers
return NotFound("User not found");
}
- var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString());
+ var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp());
return result;
}
@@ -125,14 +126,14 @@ namespace Jellyfin.Api.Controllers
/// Deletes a user.
/// </summary>
/// <param name="userId">The user id.</param>
- /// <response code="200">User deleted.</response>
+ /// <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 ActionResult DeleteUser([FromRoute] Guid userId)
+ public ActionResult DeleteUser([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
_sessionManager.RevokeUserTokens(user.Id, null);
@@ -156,7 +157,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
[FromRoute, Required] Guid userId,
- [FromQuery, Required] string? pw,
+ [FromQuery, Required] string pw,
[FromQuery] string? password)
{
var user = _userManager.GetUserById(userId);
@@ -203,7 +204,7 @@ namespace Jellyfin.Api.Controllers
DeviceName = auth.Device,
Password = request.Pw,
PasswordSha1 = request.Password,
- RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(),
+ RemoteEndPoint = HttpContext.GetNormalizedRemoteIp(),
Username = request.Username
}).ConfigureAwait(false);
@@ -212,7 +213,41 @@ namespace Jellyfin.Api.Controllers
catch (SecurityException e)
{
// rethrow adding IP address to message
- throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e);
+ 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 async Task<ActionResult<AuthenticationResult>> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
+ {
+ var auth = _authContext.GetAuthorizationInfo(Request);
+
+ try
+ {
+ var authRequest = new AuthenticationRequest
+ {
+ App = auth.Client,
+ AppVersion = auth.Version,
+ DeviceId = auth.DeviceId,
+ DeviceName = auth.Device,
+ };
+
+ return await _sessionManager.AuthenticateQuickConnect(
+ authRequest,
+ request.Token).ConfigureAwait(false);
+ }
+ catch (SecurityException e)
+ {
+ // rethrow adding IP address to message
+ throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
}
}
@@ -221,7 +256,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="request">The <see cref="UpdateUserPassword"/> request.</param>
- /// <response code="200">Password successfully reset.</response>
+ /// <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>
@@ -231,7 +266,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateUserPassword(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromBody] UpdateUserPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
@@ -256,7 +291,7 @@ namespace Jellyfin.Api.Controllers
user.Username,
request.CurrentPw,
request.CurrentPw,
- HttpContext.Connection.RemoteIpAddress.ToString(),
+ HttpContext.GetNormalizedRemoteIp(),
false).ConfigureAwait(false);
if (success == null)
@@ -279,7 +314,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param>
- /// <response code="200">Password successfully reset.</response>
+ /// <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>
@@ -289,7 +324,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateUserEasyPassword(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromBody] UpdateUserEasyPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
@@ -331,7 +366,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUser(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromBody] UserDto updateUser)
{
if (updateUser == null)
@@ -346,17 +381,13 @@ namespace Jellyfin.Api.Controllers
var user = _userManager.GetUserById(userId);
- if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
- {
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
- _userManager.UpdateConfiguration(user.Id, updateUser.Configuration);
- }
- else
+ if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
{
await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
- _userManager.UpdateConfiguration(updateUser.Id, updateUser.Configuration);
}
+ await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false);
+
return NoContent();
}
@@ -374,8 +405,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public ActionResult UpdateUserPolicy(
- [FromRoute] Guid userId,
+ public async Task<ActionResult> UpdateUserPolicy(
+ [FromRoute, Required] Guid userId,
[FromBody] UserPolicy newPolicy)
{
if (newPolicy == null)
@@ -412,7 +443,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.RevokeUserTokens(user.Id, currentToken);
}
- _userManager.UpdatePolicy(userId, newPolicy);
+ await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
return NoContent();
}
@@ -429,8 +460,8 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public ActionResult UpdateUserConfiguration(
- [FromRoute] Guid userId,
+ public async Task<ActionResult> UpdateUserConfiguration(
+ [FromRoute, Required] Guid userId,
[FromBody] UserConfiguration userConfig)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
@@ -438,7 +469,7 @@ namespace Jellyfin.Api.Controllers
return Forbid("User configuration update not allowed");
}
- _userManager.UpdateConfiguration(userId, userConfig);
+ await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
return NoContent();
}
@@ -462,7 +493,7 @@ namespace Jellyfin.Api.Controllers
await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
}
- var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString());
+ var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp());
return result;
}
@@ -470,17 +501,17 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Initiates the forgot password process for a local user.
/// </summary>
- /// <param name="enteredUsername">The entered username.</param>
+ /// <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] string? enteredUsername)
+ public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
{
- var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress)
- || _networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString());
+ var isLocal = HttpContext.IsLocal()
+ || _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp());
- var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false);
+ var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);
return result;
}
@@ -499,6 +530,33 @@ namespace Jellyfin.Api.Controllers
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(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public ActionResult<UserDto> GetCurrentUser()
+ {
+ var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
+ if (userId == null)
+ {
+ return BadRequest();
+ }
+
+ var user = _userManager.GetUserById(userId.Value);
+ if (user == null)
+ {
+ return BadRequest();
+ }
+
+ return _userManager.GetUserDto(user);
+ }
+
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
{
var users = _userManager.Users;
@@ -525,7 +583,7 @@ namespace Jellyfin.Api.Controllers
if (filterByNetwork)
{
- if (!_networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString()))
+ if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()))
{
users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
}
@@ -533,7 +591,7 @@ namespace Jellyfin.Api.Controllers
var result = users
.OrderBy(u => u.Username)
- .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString()));
+ .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp()));
return result;
}
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index f55ff6f3d5..a52af17815 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -70,7 +71,7 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid userId, [FromRoute] Guid itemId)
+ public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
@@ -93,7 +94,7 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid userId)
+ public ActionResult<BaseItemDto> GetRootFolder([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
var item = _libraryManager.GetUserRootFolder();
@@ -110,7 +111,7 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid userId, [FromRoute] Guid itemId)
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
@@ -138,7 +139,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("Users/{userId}/FavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
return MarkFavorite(userId, itemId, true);
}
@@ -152,7 +153,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("Users/{userId}/FavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
return MarkFavorite(userId, itemId, false);
}
@@ -166,7 +167,7 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
return UpdateUserItemRatingInternal(userId, itemId, null);
}
@@ -181,7 +182,7 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid userId, [FromRoute] Guid itemId, [FromQuery] bool? likes)
+ public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId, [FromQuery] bool? likes)
{
return UpdateUserItemRatingInternal(userId, itemId, likes);
}
@@ -195,7 +196,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>The items local trailers.</returns>
[HttpGet("Users/{userId}/Items/{itemId}/LocalTrailers")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
@@ -230,7 +231,7 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid userId, [FromRoute] Guid itemId)
+ public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
{
var user = _userManager.GetUserById(userId);
@@ -264,14 +265,14 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Users/{userId}/Items/Latest")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromQuery] Guid? parentId,
[FromQuery] string? fields,
[FromQuery] string? includeItemTypes,
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int limit = 20,
[FromQuery] bool groupItems = true)
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index 6df7cc779f..d575bfc3b8 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Extensions;
@@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Users/{userId}/Views")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
- [FromRoute] Guid userId,
+ [FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
[FromQuery] string? presetViews,
[FromQuery] bool includeHidden = false)
@@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Users/{userId}/GroupingOptions")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromRoute] Guid userId)
+ public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
if (user == null)
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index 76188f46d6..d7bcf79c1f 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
@@ -161,8 +163,9 @@ namespace Jellyfin.Api.Controllers
/// <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] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromQuery] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
@@ -368,7 +371,7 @@ namespace Jellyfin.Api.Controllers
var baseUrlParam = string.Format(
CultureInfo.InvariantCulture,
- "\"hls{0}\"",
+ "\"hls/{0}/\"",
Path.GetFileNameWithoutExtension(outputPath));
return string.Format(
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index ae32dbf035..4de7aac71d 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -6,11 +6,13 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
@@ -114,7 +116,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{itemId}/AdditionalParts")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute] Guid itemId, [FromQuery] Guid? userId)
+ public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@@ -159,9 +161,9 @@ namespace Jellyfin.Api.Controllers
/// <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.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteAlternateSources([FromRoute] Guid itemId)
+ public async Task<ActionResult> DeleteAlternateSources([FromRoute, Required] Guid itemId)
{
var video = (Video)_libraryManager.GetItemById(itemId);
@@ -180,12 +182,12 @@ namespace Jellyfin.Api.Controllers
link.SetPrimaryVersionId(null);
link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- link.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ await link.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
video.LinkedAlternateVersions = Array.Empty<LinkedChild>();
video.SetPrimaryVersionId(null);
- video.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ await video.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -201,7 +203,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public ActionResult MergeVersions([FromQuery, Required] string? itemIds)
+ public async Task<ActionResult> MergeVersions([FromQuery, Required] string itemIds)
{
var items = RequestHelpers.Split(itemIds, ',', true)
.Select(i => _libraryManager.GetItemById(i))
@@ -233,37 +235,40 @@ namespace Jellyfin.Api.Controllers
.First();
}
- var list = primaryVersion.LinkedAlternateVersions.ToList();
+ var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList();
foreach (var item in items.Where(i => i.Id != primaryVersion.Id))
{
item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture));
- item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- list.Add(new LinkedChild
+ if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase)))
{
- Path = item.Path,
- ItemId = item.Id
- });
+ alternateVersionsOfPrimary.Add(new LinkedChild
+ {
+ Path = item.Path,
+ ItemId = item.Id
+ });
+ }
foreach (var linkedItem in item.LinkedAlternateVersions)
{
- if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
+ if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase)))
{
- list.Add(linkedItem);
+ alternateVersionsOfPrimary.Add(linkedItem);
}
}
if (item.LinkedAlternateVersions.Length > 0)
{
item.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
}
- primaryVersion.LinkedAlternateVersions = list.ToArray();
- primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ primaryVersion.LinkedAlternateVersions = alternateVersionsOfPrimary.ToArray();
+ await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -321,13 +326,14 @@ namespace Jellyfin.Api.Controllers
/// <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=stream}.{container?}", Name = "GetVideoStream_2")]
+ [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")]
[HttpGet("{itemId}/stream")]
- [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")]
+ [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")]
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
public async Task<ActionResult> GetVideoStream(
- [FromRoute] Guid itemId,
+ [FromRoute, Required] Guid itemId,
[FromRoute] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
@@ -470,8 +476,8 @@ namespace Jellyfin.Api.Controllers
{
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
- var httpClient = _httpClientFactory.CreateClient();
- return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
+ var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
+ return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false);
}
if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
@@ -507,7 +513,7 @@ namespace Jellyfin.Api.Controllers
state.MediaPath,
contentType,
isHeadRequest,
- this);
+ HttpContext);
}
// Need to start ffmpeg (because media can't be returned directly)
@@ -517,10 +523,9 @@ namespace Jellyfin.Api.Controllers
return await FileStreamResponseHelpers.GetTranscodedFile(
state,
isHeadRequest,
- this,
+ HttpContext,
_transcodingJobHelper,
ffmpegCommandLineArguments,
- Request,
_transcodingJobType,
cancellationTokenSource).ConfigureAwait(false);
}
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index eb91ac23e9..2c685309a6 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
@@ -9,6 +10,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -76,7 +78,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
- [FromQuery] string? enableImageTypes,
+ [FromQuery] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] bool recursive = true,
[FromQuery] bool? enableImages = true)
@@ -179,7 +181,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{year}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<BaseItemDto> GetYear([FromRoute] int year, [FromQuery] Guid? userId)
+ public ActionResult<BaseItemDto> GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId)
{
var item = _libraryManager.GetYear(year);
if (item == null)