aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api/Controllers')
-rw-r--r--Jellyfin.Api/Controllers/ApiKeyController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs145
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs181
-rw-r--r--Jellyfin.Api/Controllers/BrandingController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs10
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs19
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs56
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs295
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs76
-rw-r--r--Jellyfin.Api/Controllers/GenresController.cs20
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs28
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs700
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs6
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs460
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs13
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs36
-rw-r--r--Jellyfin.Api/Controllers/LocalizationController.cs2
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs60
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs6
-rw-r--r--Jellyfin.Api/Controllers/MusicGenresController.cs20
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs10
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs14
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs15
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs5
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs4
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs15
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs13
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs14
-rw-r--r--Jellyfin.Api/Controllers/StudiosController.cs23
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs42
-rw-r--r--Jellyfin.Api/Controllers/SuggestionsController.cs11
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs315
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs10
-rw-r--r--Jellyfin.Api/Controllers/TimeSyncController.cs16
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs93
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs28
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs17
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs42
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs8
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs9
-rw-r--r--Jellyfin.Api/Controllers/VideoAttachmentsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/VideoHlsController.cs183
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs182
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs29
49 files changed, 2432 insertions, 816 deletions
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index e8d6ccdf2..8c43d786a 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Jellyfin.Api.Constants;
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index 9bad206e0..fed7ed3e5 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -1,9 +1,8 @@
-using System;
+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;
@@ -87,26 +86,26 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -119,64 +118,55 @@ namespace Jellyfin.Api.Controllers
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
- BaseItem parentItem;
+ BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
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 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,
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ MediaTypes = mediaTypes,
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),
+ Tags = tags,
+ OfficialRatings = officialRatings,
+ Genres = genres,
+ GenreIds = genreIds,
+ StudioIds = studioIds,
Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+ PersonIds = personIds,
+ PersonTypes = personTypes,
+ Years = years,
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
};
- if (!string.IsNullOrWhiteSpace(parentId))
+ if (parentId.HasValue)
{
if (parentItem is Folder)
{
- query.AncestorIds = new[] { new Guid(parentId) };
+ query.AncestorIds = new[] { parentId.Value };
}
else
{
- query.ItemIds = new[] { new Guid(parentId) };
+ query.ItemIds = new[] { parentId.Value };
}
}
// Studios
- if (!string.IsNullOrEmpty(studios))
+ if (studios.Length != 0)
{
- query.StudioIds = studios.Split('|').Select(i =>
+ query.StudioIds = studios.Select(i =>
{
try
{
@@ -230,7 +220,7 @@ namespace Jellyfin.Api.Controllers
var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
+ if (includeItemTypes.Length != 0)
{
dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount;
@@ -295,26 +285,26 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@@ -327,64 +317,55 @@ namespace Jellyfin.Api.Controllers
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
- BaseItem parentItem;
+ BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
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 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,
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ MediaTypes = mediaTypes,
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),
+ Tags = tags,
+ OfficialRatings = officialRatings,
+ Genres = genres,
+ GenreIds = genreIds,
+ StudioIds = studioIds,
Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+ PersonIds = personIds,
+ PersonTypes = personTypes,
+ Years = years,
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
};
- if (!string.IsNullOrWhiteSpace(parentId))
+ if (parentId.HasValue)
{
if (parentItem is Folder)
{
- query.AncestorIds = new[] { new Guid(parentId) };
+ query.AncestorIds = new[] { parentId.Value };
}
else
{
- query.ItemIds = new[] { new Guid(parentId) };
+ query.ItemIds = new[] { parentId.Value };
}
}
// Studios
- if (!string.IsNullOrEmpty(studios))
+ if (studios.Length != 0)
{
- query.StudioIds = studios.Split('|').Select(i =>
+ query.StudioIds = studios.Select(i =>
{
try
{
@@ -438,7 +419,7 @@ namespace Jellyfin.Api.Controllers
var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
- if (!string.IsNullOrWhiteSpace(includeItemTypes))
+ if (includeItemTypes.Length != 0)
{
dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount;
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index d4c6e4af9..616fe5b91 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
@@ -42,7 +42,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -71,29 +71,192 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Audio stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("{itemId}/stream.{container:required}", Name = "GetAudioStreamByContainer")]
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
- [HttpHead("{itemId}/stream.{container:required}", Name = "HeadAudioStreamByContainer")]
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStream(
[FromRoute, Required] Guid itemId,
- [FromRoute] string? container,
+ [FromQuery] string? container,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext? context,
+ [FromQuery] Dictionary<string, string>? streamOptions)
+ {
+ StreamingRequestDto streamingRequest = new StreamingRequestDto
+ {
+ Id = itemId,
+ Container = container,
+ Static = @static ?? true,
+ Params = @params,
+ Tag = tag,
+ DeviceProfileId = deviceProfileId,
+ PlaySessionId = playSessionId,
+ SegmentContainer = segmentContainer,
+ SegmentLength = segmentLength,
+ MinSegments = minSegments,
+ MediaSourceId = mediaSourceId,
+ DeviceId = deviceId,
+ AudioCodec = audioCodec,
+ EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
+ AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
+ AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
+ BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
+ AudioSampleRate = audioSampleRate,
+ MaxAudioChannels = maxAudioChannels,
+ AudioBitRate = audioBitRate,
+ MaxAudioBitDepth = maxAudioBitDepth,
+ AudioChannels = audioChannels,
+ Profile = profile,
+ Level = level,
+ Framerate = framerate,
+ MaxFramerate = maxFramerate,
+ CopyTimestamps = copyTimestamps ?? true,
+ StartTimeTicks = startTimeTicks,
+ Width = width,
+ Height = height,
+ VideoBitRate = videoBitRate,
+ SubtitleStreamIndex = subtitleStreamIndex,
+ SubtitleMethod = subtitleMethod,
+ MaxRefFrames = maxRefFrames,
+ MaxVideoBitDepth = maxVideoBitDepth,
+ RequireAvc = requireAvc ?? true,
+ DeInterlace = deInterlace ?? true,
+ RequireNonAnamorphic = requireNonAnamorphic ?? true,
+ TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
+ CpuCoreLimit = cpuCoreLimit,
+ LiveStreamId = liveStreamId,
+ EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
+ VideoCodec = videoCodec,
+ SubtitleCodec = subtitleCodec,
+ TranscodeReasons = transcodeReasons,
+ AudioStreamIndex = audioStreamIndex,
+ VideoStreamIndex = videoStreamIndex,
+ Context = context ?? EncodingContext.Static,
+ StreamOptions = streamOptions
+ };
+
+ return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Gets an audio stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The audio container.</param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <param name="segmentContainer">The segment container.</param>
+ /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="minSegments">The minimum number of segments.</param>
+ /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+ /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+ /// <param name="requireAvc">Optional. Whether to require avc.</param>
+ /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+ /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+ /// <param name="liveStreamId">The live stream id.</param>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <response code="200">Audio stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
+ [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
+ public async Task<ActionResult> GetAudioStreamByContainer(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -136,7 +299,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodingReasons,
+ [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
@@ -188,7 +351,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodingReasons,
+ TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Static,
diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs
index 1d4836f27..d3ea41201 100644
--- a/Jellyfin.Api/Controllers/BrandingController.cs
+++ b/Jellyfin.Api/Controllers/BrandingController.cs
@@ -1,4 +1,4 @@
-using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Branding;
using Microsoft.AspNetCore.Http;
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index ec9d7cdce..b70c76e80 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -92,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, Required] string channelId)
+ public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] Guid channelId)
{
return _channelManager.GetChannelFeatures(channelId);
}
@@ -198,7 +198,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? channelIds)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@@ -208,11 +208,7 @@ namespace Jellyfin.Api.Controllers
{
Limit = limit,
StartIndex = startIndex,
- ChannelIds = (channelIds ?? string.Empty)
- .Split(',')
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .Select(i => new Guid(i))
- .ToArray(),
+ ChannelIds = channelIds,
DtoOptions = new DtoOptions { Fields = fields }
};
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index eae06b767..2a7b2b5c6 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -1,9 +1,10 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Net;
@@ -54,7 +55,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
[FromQuery] string? name,
- [FromQuery] string? ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
[FromQuery] Guid? parentId,
[FromQuery] bool isLocked = false)
{
@@ -65,7 +66,7 @@ namespace Jellyfin.Api.Controllers
IsLocked = isLocked,
Name = name,
ParentId = parentId,
- ItemIdList = RequestHelpers.Split(ids, ',', true),
+ ItemIdList = ids,
UserIds = new[] { userId }
}).ConfigureAwait(false);
@@ -88,9 +89,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
+ public async Task<ActionResult> AddToCollection(
+ [FromRoute, Required] Guid collectionId,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
- await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true);
+ await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
return NoContent();
}
@@ -103,9 +106,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
+ public async Task<ActionResult> RemoveFromCollection(
+ [FromRoute, Required] Guid collectionId,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
- await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false);
+ await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index a859ac114..ccc81dfc5 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 76f5717e3..8b8f63015 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -6,6 +6,7 @@ using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authorization;
@@ -47,13 +48,19 @@ namespace Jellyfin.Api.Controllers
[FromQuery, Required] Guid userId,
[FromQuery, Required] string client)
{
- var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
- var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
+ if (!Guid.TryParse(displayPreferencesId, out var itemId))
+ {
+ itemId = displayPreferencesId.GetMD5();
+ }
+
+ var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
+ var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
+ itemPreferences.ItemId = itemId;
var dto = new DisplayPreferencesDto
{
Client = displayPreferences.Client,
- Id = displayPreferences.UserId.ToString(),
+ Id = displayPreferences.ItemId.ToString(),
ViewType = itemPreferences.ViewType.ToString(),
SortBy = itemPreferences.SortBy,
SortOrder = itemPreferences.SortOrder,
@@ -81,6 +88,16 @@ namespace Jellyfin.Api.Controllers
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
+ // Load all custom display preferences
+ var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
+ if (customDisplayPreferences != null)
+ {
+ foreach (var (key, value) in customDisplayPreferences)
+ {
+ dto.CustomPrefs.TryAdd(key, value);
+ }
+ }
+
// This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
_displayPreferencesManager.SaveChanges();
@@ -115,7 +132,12 @@ namespace Jellyfin.Api.Controllers
HomeSectionType.LatestMedia, HomeSectionType.None,
};
- var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
+ if (!Guid.TryParse(displayPreferencesId, out var itemId))
+ {
+ itemId = displayPreferencesId.GetMD5();
+ }
+
+ var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
@@ -124,21 +146,33 @@ namespace Jellyfin.Api.Controllers
existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
: ChromecastVersion.Stable;
+ displayPreferences.CustomPrefs.Remove("chromecastVersion");
+
existingDisplayPreferences.EnableNextVideoInfoOverlay = displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
? bool.Parse(enableNextVideoInfoOverlay)
: true;
+ displayPreferences.CustomPrefs.Remove("enableNextVideoInfoOverlay");
+
existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength)
? int.Parse(skipBackLength, CultureInfo.InvariantCulture)
: 10000;
+ displayPreferences.CustomPrefs.Remove("skipBackLength");
+
existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength)
? int.Parse(skipForwardLength, CultureInfo.InvariantCulture)
: 30000;
+ displayPreferences.CustomPrefs.Remove("skipForwardLength");
+
existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme)
? theme
: string.Empty;
+ displayPreferences.CustomPrefs.Remove("dashboardTheme");
+
existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home)
? home
: string.Empty;
+ displayPreferences.CustomPrefs.Remove("tvhome");
+
existingDisplayPreferences.HomeSections.Clear();
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase)))
@@ -149,26 +183,34 @@ namespace Jellyfin.Api.Controllers
type = order < 7 ? defaults[order] : HomeSectionType.None;
}
+ displayPreferences.CustomPrefs.Remove(key);
existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
}
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{
- var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Parse(key.Substring("landing-".Length)), existingDisplayPreferences.Client);
- itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
+ if (Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var preferenceId))
+ {
+ var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, preferenceId, existingDisplayPreferences.Client);
+ itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
+ displayPreferences.CustomPrefs.Remove(key);
+ }
}
- var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client);
+ var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, itemId, existingDisplayPreferences.Client);
itemPrefs.SortBy = displayPreferences.SortBy;
itemPrefs.SortOrder = displayPreferences.SortOrder;
itemPrefs.RememberIndexing = displayPreferences.RememberIndexing;
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
+ itemPrefs.ItemId = itemId;
if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
{
itemPrefs.ViewType = viewType;
}
+ // Set all remaining custom preferences.
+ _displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
_displayPreferencesManager.SaveChanges();
return NoContent();
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 4e6455eaa..4fd9c2fbf 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -252,7 +252,7 @@ namespace Jellyfin.Api.Controllers
private string GetAbsoluteUri()
{
- return $"{Request.Scheme}://{Request.Host}{Request.Path}";
+ return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
}
private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 6e59da798..6e85737d2 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -41,6 +42,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
public class DynamicHlsController : BaseJellyfinApiController
{
+ private const string DefaultEncoderPreset = "veryfast";
+ private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
+
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager;
@@ -56,8 +60,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper;
private readonly DynamicHlsHelper _dynamicHlsHelper;
-
- private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
+ private readonly EncodingOptions _encodingOptions;
/// <summary>
/// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
@@ -92,6 +95,8 @@ namespace Jellyfin.Api.Controllers
ILogger<DynamicHlsController> logger,
DynamicHlsHelper dynamicHlsHelper)
{
+ _encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
+
_libraryManager = libraryManager;
_userManager = userManager;
_dlnaManager = dlnaManager;
@@ -106,8 +111,7 @@ namespace Jellyfin.Api.Controllers
_transcodingJobHelper = transcodingJobHelper;
_logger = logger;
_dynamicHlsHelper = dynamicHlsHelper;
-
- _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
+ _encodingOptions = serverConfigurationManager.GetEncodingOptions();
}
/// <summary>
@@ -120,7 +124,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -149,14 +153,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@@ -212,7 +216,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodingReasons,
+ [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
@@ -264,7 +268,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodingReasons,
+ TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
@@ -272,7 +276,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
- return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
}
/// <summary>
@@ -285,7 +289,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -315,14 +319,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@@ -379,7 +383,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodingReasons,
+ [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
@@ -431,7 +435,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodingReasons,
+ TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
@@ -439,7 +443,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
- return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
+ return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
}
/// <summary>
@@ -452,7 +456,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -481,14 +485,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@@ -542,7 +546,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodingReasons,
+ [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
@@ -594,7 +598,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodingReasons,
+ TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
@@ -615,7 +619,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -645,14 +649,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@@ -707,7 +711,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodingReasons,
+ [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
@@ -759,7 +763,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodingReasons,
+ TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
@@ -812,14 +816,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@@ -834,7 +838,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
- [FromRoute] string container,
+ [FromRoute, Required] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -877,7 +881,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodingReasons,
+ [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
@@ -929,7 +933,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodingReasons,
+ TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
@@ -953,7 +957,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -983,14 +987,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@@ -1005,7 +1009,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
- [FromRoute] string container,
+ [FromRoute, Required] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -1049,7 +1053,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodingReasons,
+ [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
@@ -1101,7 +1105,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodingReasons,
+ TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
@@ -1129,7 +1133,7 @@ namespace Jellyfin.Api.Controllers
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
- _transcodingJobType,
+ TranscodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@@ -1137,11 +1141,19 @@ namespace Jellyfin.Api.Controllers
var segmentLengths = GetSegmentLengths(state);
+ var segmentContainer = state.Request.SegmentContainer ?? "ts";
+
+ // http://ffmpeg.org/ffmpeg-all.html#toc-hls-2
+ var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase);
+ var hlsVersion = isHlsInFmp4 ? "7" : "3";
+
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
- .AppendLine("#EXT-X-VERSION:3")
+ .Append("#EXT-X-VERSION:")
+ .Append(hlsVersion)
+ .AppendLine()
.Append("#EXT-X-TARGETDURATION:")
.Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
.AppendLine()
@@ -1151,6 +1163,18 @@ namespace Jellyfin.Api.Controllers
var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
var queryString = Request.QueryString;
+ if (isHlsInFmp4)
+ {
+ builder.Append("#EXT-X-MAP:URI=\"")
+ .Append("hls1/")
+ .Append(name)
+ .Append("/-1")
+ .Append(segmentExtension)
+ .Append(queryString)
+ .Append('"')
+ .AppendLine();
+ }
+
foreach (var length in segmentLengths)
{
builder.Append("#EXTINF:")
@@ -1194,7 +1218,7 @@ namespace Jellyfin.Api.Controllers
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
- _transcodingJobType,
+ TranscodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@@ -1208,7 +1232,7 @@ namespace Jellyfin.Api.Controllers
if (System.IO.File.Exists(segmentPath))
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+ job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
_logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
@@ -1222,7 +1246,7 @@ namespace Jellyfin.Api.Controllers
{
if (System.IO.File.Exists(segmentPath))
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+ job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
transcodingLock.Release();
released = true;
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
@@ -1233,7 +1257,13 @@ namespace Jellyfin.Api.Controllers
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
- if (currentTranscodingIndex == null)
+ if (segmentId == -1)
+ {
+ _logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
+ startTranscoding = true;
+ segmentId = 0;
+ }
+ else if (currentTranscodingIndex == null)
{
_logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
startTranscoding = true;
@@ -1265,13 +1295,12 @@ namespace Jellyfin.Api.Controllers
streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId);
state.WaitForPath = segmentPath;
- var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
job = await _transcodingJobHelper.StartFfMpeg(
state,
playlistPath,
- GetCommandLineArguments(playlistPath, encodingOptions, state, true, segmentId),
+ GetCommandLineArguments(playlistPath, state, true, segmentId),
Request,
- _transcodingJobType,
+ TranscodingJobType,
cancellationTokenSource).ConfigureAwait(false);
}
catch
@@ -1284,7 +1313,7 @@ namespace Jellyfin.Api.Controllers
}
else
{
- job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+ job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job?.TranscodingThrottler != null)
{
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
@@ -1301,7 +1330,7 @@ namespace Jellyfin.Api.Controllers
}
_logger.LogDebug("returning {0} [general case]", segmentPath);
- job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
+ job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
@@ -1325,11 +1354,10 @@ namespace Jellyfin.Api.Controllers
return result.ToArray();
}
- private string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding, int startNumber)
+ private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber)
{
- var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
-
- var threads = _encodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
+ var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+ var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
if (state.BaseRequest.BreakOnNonKeyFrames)
{
@@ -1341,36 +1369,57 @@ namespace Jellyfin.Api.Controllers
state.BaseRequest.BreakOnNonKeyFrames = false;
}
- var inputModifier = _encodingHelper.GetInputModifier(state, encodingOptions);
-
// If isEncoding is true we're actually starting ffmpeg
var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
-
+ var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+ var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
+ var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
+ var outputExtension = GetSegmentFileExtension(state.Request.SegmentContainer);
+ var outputTsArg = outputPrefix + "%d" + outputExtension;
- var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
-
- var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
+ var segmentFormat = outputExtension.TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{
segmentFormat = "mpegts";
}
+ else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ var outputFmp4HeaderArg = string.Empty;
+ var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ if (isWindows)
+ {
+ // on Windows, the path of fmp4 header file needs to be configured
+ outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
+ }
+ else
+ {
+ // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
+ outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
+ }
+
+ segmentFormat = "fmp4" + outputFmp4HeaderArg;
+ }
+ else
+ {
+ _logger.LogError("Invalid HLS segment container: " + segmentFormat);
+ }
- var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128
- ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+ 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 -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}\"",
+ "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
inputModifier,
- _encodingHelper.GetInputArgument(state, encodingOptions),
+ _encodingHelper.GetInputArgument(state, _encodingOptions),
threads,
mapArgs,
- GetVideoArguments(state, encodingOptions, startNumber),
- GetAudioArguments(state, encodingOptions),
+ GetVideoArguments(state, startNumber),
+ GetAudioArguments(state),
maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
segmentFormat,
@@ -1379,50 +1428,63 @@ namespace Jellyfin.Api.Controllers
outputPath).Trim();
}
- private string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
+ /// <summary>
+ /// Gets the audio arguments for transcoding.
+ /// </summary>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <returns>The command line arguments for audio transcoding.</returns>
+ private string GetAudioArguments(StreamState state)
{
+ if (state.AudioStream == null)
+ {
+ return string.Empty;
+ }
+
var audioCodec = _encodingHelper.GetAudioEncoder(state);
if (!state.IsOutputVideo)
{
if (EncodingHelper.IsCopyCodec(audioCodec))
{
- return "-acodec copy";
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+
+ return "-acodec copy -strict -2" + bitStreamArgs;
}
- var audioTranscodeParams = new List<string>();
+ var audioTranscodeParams = string.Empty;
- audioTranscodeParams.Add("-acodec " + audioCodec);
+ audioTranscodeParams += "-acodec " + audioCodec;
if (state.OutputAudioBitrate.HasValue)
{
- audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
+ audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioChannels.HasValue)
{
- audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
+ audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioSampleRate.HasValue)
{
- audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
+ audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- audioTranscodeParams.Add("-vn");
- return string.Join(' ', audioTranscodeParams);
+ audioTranscodeParams += " -vn";
+ return audioTranscodeParams;
}
if (EncodingHelper.IsCopyCodec(audioCodec))
{
- var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
+ var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
{
- return "-codec:a:0 copy -copypriorss:a:0 0";
+ return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0" + bitStreamArgs;
}
- return "-codec:a:0 copy";
+ return "-codec:a:0 copy -strict -2" + bitStreamArgs;
}
var args = "-codec:a:0 " + audioCodec;
@@ -1446,94 +1508,89 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- args += " " + _encodingHelper.GetAudioFilterParam(state, encodingOptions, true);
+ args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
return args;
}
- private string GetVideoArguments(StreamState state, EncodingOptions encodingOptions, int startNumber)
+ /// <summary>
+ /// Gets the video arguments for transcoding.
+ /// </summary>
+ /// <param name="state">The <see cref="StreamState"/>.</param>
+ /// <param name="startNumber">The first number in the hls sequence.</param>
+ /// <returns>The command line arguments for video transcoding.</returns>
+ private string GetVideoArguments(StreamState state, int startNumber)
{
+ if (state.VideoStream == null)
+ {
+ return string.Empty;
+ }
+
if (!state.IsOutputVideo)
{
return string.Empty;
}
- var codec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
+ var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var args = "-codec:v:0 " + codec;
+ // Prefer hvc1 to hev1.
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ args += " -tag:v:0 hvc1";
+ }
+
// if (state.EnableMpegtsM2TsMode)
// {
// args += " -mpegts_m2ts_mode 1";
// }
- // See if we can save come cpu cycles by avoiding encoding
+ // See if we can save come cpu cycles by avoiding encoding.
if (EncodingHelper.IsCopyCodec(codec))
{
if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
- string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
+ string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
if (!string.IsNullOrEmpty(bitStreamArgs))
{
args += " " + bitStreamArgs;
}
}
+ args += " -start_at_zero";
+
// args += " -flags -global_header";
}
else
{
- var gopArg = string.Empty;
- var keyFrameArg = string.Format(
- CultureInfo.InvariantCulture,
- " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
- startNumber * state.SegmentLength,
- state.SegmentLength);
+ args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
- var framerate = state.VideoStream?.RealFrameRate;
+ // Set the key frame params for video encoding to match the hls segment time.
+ args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, false, startNumber);
- if (framerate.HasValue)
+ // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
+ if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{
- // This is to make sure keyframe interval is limited to our segment,
- // as forcing keyframes is not enough.
- // Example: we encoded half of desired length, then codec detected
- // scene cut and inserted a keyframe; next forced keyframe would
- // be created outside of segment, which breaks seeking
- // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
- gopArg = string.Format(
- CultureInfo.InvariantCulture,
- " -g {0} -keyint_min {0} -sc_threshold 0",
- Math.Ceiling(state.SegmentLength * framerate.Value));
- }
-
- args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "veryfast");
-
- // Unable to force key frames using these hw encoders, set key frames by GOP
- if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase))
- {
- args += " " + gopArg;
- }
- else
- {
- args += " " + keyFrameArg + gopArg;
+ args += " -bf 0";
}
// args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
- // This is for graphical subs
if (hasGraphicalSubs)
{
- args += _encodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
+ // Graphical subs overlay and resolution params.
+ args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
}
-
- // Add resolution params, if specified
else
{
- args += _encodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
+ // Resolution params.
+ args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
}
// -start_at_zero is necessary to use with -ss when seeking,
@@ -1693,7 +1750,7 @@ namespace Jellyfin.Api.Controllers
private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
{
- var job = _transcodingJobHelper.GetTranscodingJob(playlist, _transcodingJobType);
+ var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
if (job == null || job.HasExited)
{
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 008bb58d1..9220b988f 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -1,6 +1,7 @@
-using System;
+using System;
using System.Linq;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -49,37 +50,29 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
[FromQuery] Guid? userId,
- [FromQuery] string? parentId,
- [FromQuery] string? includeItemTypes,
- [FromQuery] string? mediaTypes)
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
{
- var parentItem = string.IsNullOrEmpty(parentId)
- ? null
- : _libraryManager.GetItemById(parentId);
-
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
: null;
- if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+ BaseItem? item = null;
+ if (includeItemTypes.Length != 1
+ || !(string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
{
- parentItem = null;
+ item = _libraryManager.GetParentItem(parentId, user?.Id);
}
- var item = string.IsNullOrEmpty(parentId)
- ? user == null
- ? _libraryManager.RootFolder
- : _libraryManager.GetUserRootFolder()
- : parentItem;
-
var query = new InternalItemsQuery
{
User = user,
- MediaTypes = (mediaTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
- IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
+ MediaTypes = mediaTypes,
+ IncludeItemTypes = includeItemTypes,
Recursive = true,
EnableTotalRecordCount = false,
DtoOptions = new DtoOptions
@@ -90,7 +83,12 @@ namespace Jellyfin.Api.Controllers
}
};
- var itemList = ((Folder)item!).GetItemList(query);
+ if (item is not Folder folder)
+ {
+ return new QueryFiltersLegacy();
+ }
+
+ var itemList = folder.GetItemList(query);
return new QueryFiltersLegacy
{
Years = itemList.Select(i => i.ProductionYear ?? -1)
@@ -138,8 +136,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryFilters> GetQueryFilters(
[FromQuery] Guid? userId,
- [FromQuery] string? parentId,
- [FromQuery] string? includeItemTypes,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isAiring,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSports,
@@ -148,27 +146,28 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isSeries,
[FromQuery] bool? recursive)
{
- var parentItem = string.IsNullOrEmpty(parentId)
- ? null
- : _libraryManager.GetItemById(parentId);
-
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
: null;
- if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
+ BaseItem? parentItem = null;
+ if (includeItemTypes.Length == 1
+ && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
{
parentItem = null;
}
+ else if (parentId.HasValue)
+ {
+ parentItem = _libraryManager.GetItemById(parentId.Value);
+ }
var filters = new QueryFilters();
var genreQuery = new InternalItemsQuery(user)
{
- IncludeItemTypes =
- (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
+ IncludeItemTypes = includeItemTypes,
DtoOptions = new DtoOptions
{
Fields = Array.Empty<ItemFields>(),
@@ -192,10 +191,11 @@ namespace Jellyfin.Api.Controllers
genreQuery.Parent = parentItem;
}
- if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase))
+ if (includeItemTypes.Length == 1
+ && (string.Equals(includeItemTypes[0], nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
+ || string.Equals(includeItemTypes[0], nameof(Audio), StringComparison.OrdinalIgnoreCase)))
{
filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
{
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index 9c222135d..b6755ed5e 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
@@ -72,10 +72,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
@@ -109,15 +109,15 @@ namespace Jellyfin.Api.Controllers
EnableTotalRecordCount = enableTotalRecordCount
};
- if (!string.IsNullOrWhiteSpace(parentId))
+ if (parentId.HasValue)
{
if (parentItem is Folder)
{
- query.AncestorIds = new[] { new Guid(parentId) };
+ query.AncestorIds = new[] { parentId.Value };
}
else
{
- query.ItemIds = new[] { new Guid(parentId) };
+ query.ItemIds = new[] { parentId.Value };
}
}
@@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers
result = _libraryManager.GetGenres(query);
}
- var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
+ var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index 3b75e8d43..f51987732 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.IO;
@@ -112,11 +112,13 @@ namespace Jellyfin.Api.Controllers
/// <param name="segmentId">The segment id.</param>
/// <param name="segmentContainer">The segment container.</param>
/// <response code="200">Hls video segment returned.</response>
+ /// <response code="404">Hls segment not found.</response>
/// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
// [Authenticated]
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsVideoSegmentLegacy(
@@ -132,13 +134,25 @@ namespace Jellyfin.Api.Controllers
var normalizedPlaylistId = playlistId;
- var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
- .FirstOrDefault(i =>
- string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase)
- && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
- ?? throw new ResourceNotFoundException($"Provided path ({transcodeFolderPath}) is not valid.");
+ var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
+ // Add . to start of segment container for future use.
+ segmentContainer = segmentContainer.Insert(0, ".");
+ string? playlistPath = null;
+ foreach (var path in filePaths)
+ {
+ var pathExtension = Path.GetExtension(path);
+ if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
+ && path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ playlistPath = path;
+ break;
+ }
+ }
- return GetFileResult(file, playlistPath);
+ return playlistPath == null
+ ? NotFound("Hls segment not found.")
+ : GetFileResult(file, playlistPath);
}
private ActionResult GetFileResult(string path, string playlistPath)
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 76e53b9a5..e828a0801 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
@@ -86,7 +86,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}")]
- [HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
@@ -95,11 +94,57 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> PostUserImage(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
- [FromRoute] int? index = null)
+ [FromQuery] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to update the image.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
+ }
+
+ var user = _userManager.GetUserById(userId);
+ await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
+
+ // Handle image/png; charset=utf-8
+ var mimeType = Request.ContentType.Split(';').FirstOrDefault();
+ var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+ if (user.ProfileImage != null)
+ {
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+ }
+
+ user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
+
+ await _providerManager
+ .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .ConfigureAwait(false);
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Sets the user image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image updated.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Users/{userId}/Images/{imageType}/{index}")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> PostUserImageByIndex(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int index)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
var user = _userManager.GetUserById(userId);
@@ -132,8 +177,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="204">Image deleted.</response>
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
- [HttpDelete("Users/{userId}/Images/{itemType}")]
- [HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
+ [HttpDelete("Users/{userId}/Images/{imageType}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@@ -142,11 +186,50 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> DeleteUserImage(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
- [FromRoute] int? index = null)
+ [FromQuery] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to delete the image.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
+ }
+
+ var user = _userManager.GetUserById(userId);
+ try
+ {
+ System.IO.File.Delete(user.ProfileImage.Path);
+ }
+ catch (IOException e)
+ {
+ _logger.LogError(e, "Error deleting user profile image:");
+ }
+
+ await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Delete the user's image.
+ /// </summary>
+ /// <param name="userId">User Id.</param>
+ /// <param name="imageType">(Unused) Image type.</param>
+ /// <param name="index">(Unused) Image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="403">User does not have permission to delete the image.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult> DeleteUserImageByIndex(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int index)
+ {
+ if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
var user = _userManager.GetUserById(userId);
@@ -173,14 +256,13 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpDelete("Items/{itemId}/Images/{imageType}")]
- [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteItemImage(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -193,24 +275,82 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
+ /// Delete an item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">The image index.</param>
+ /// <response code="204">Image deleted.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteItemImageByIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
/// Set item image.
/// </summary>
/// <param name="itemId">Item id.</param>
/// <param name="imageType">Image type.</param>
- /// <param name="imageIndex">(Unused) Image index.</param>
/// <response code="204">Image saved.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpPost("Items/{itemId}/Images/{imageType}")]
- [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> SetItemImage(
[FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ // 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);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
+
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Set item image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">(Unused) Image index.</param>
+ /// <response code="204">Image saved.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
+ [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
+ public async Task<ActionResult> SetItemImageByIndex(
+ [FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType,
- [FromRoute] int? imageIndex = null)
+ [FromRoute] int imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -350,8 +490,6 @@ namespace Jellyfin.Api.Controllers
/// </returns>
[HttpGet("Items/{itemId}/Images/{imageType}")]
[HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
- [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")]
- [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
@@ -372,7 +510,86 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ itemId,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Gets the item's image.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetItemImageByIndex(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute] int imageIndex,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] string? tag,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -508,8 +725,8 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
+ [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
@@ -587,8 +804,8 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
+ [HttpGet("Genres/{name}/Images/{imageType}")]
+ [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
@@ -609,7 +826,86 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetGenre(name);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Get genre image by name.
+ /// </summary>
+ /// <param name="name">Genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetGenreImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
{
var item = _libraryManager.GetGenre(name);
if (item == null)
@@ -666,8 +962,8 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
+ [HttpGet("MusicGenres/{name}/Images/{imageType}")]
+ [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
@@ -688,7 +984,86 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetMusicGenre(name);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Get music genre image by name.
+ /// </summary>
+ /// <param name="name">Music genre name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetMusicGenreImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
{
var item = _libraryManager.GetMusicGenre(name);
if (item == null)
@@ -745,8 +1120,8 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
+ [HttpGet("Persons/{name}/Images/{imageType}")]
+ [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
@@ -767,7 +1142,86 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetPerson(name);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Get person image by name.
+ /// </summary>
+ /// <param name="name">Person name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetPersonImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
{
var item = _libraryManager.GetPerson(name);
if (item == null)
@@ -824,16 +1278,16 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
+ [HttpGet("Studios/{name}/Images/{imageType}")]
+ [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetStudioImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
- [FromRoute, Required] string tag,
- [FromRoute, Required] ImageFormat format,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -846,7 +1300,86 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
+ {
+ var item = _libraryManager.GetStudio(name);
+ if (item == null)
+ {
+ return NotFound();
+ }
+
+ return await GetImageInternal(
+ item.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ item,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
+ .ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Get studio image by name.
+ /// </summary>
+ /// <param name="name">Studio name.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetStudioImageByIndex(
+ [FromRoute, Required] string name,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
{
var item = _libraryManager.GetStudio(name);
if (item == null)
@@ -903,8 +1436,8 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
- [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex?}")]
- [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
+ [HttpGet("Users/{userId}/Images/{imageType}")]
+ [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
@@ -925,7 +1458,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer,
- [FromRoute] int? imageIndex = null)
+ [FromQuery] int? imageIndex)
{
var user = _userManager.GetUserById(userId);
if (user == null)
@@ -974,6 +1507,103 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false);
}
+ /// <summary>
+ /// Get user profile image.
+ /// </summary>
+ /// <param name="userId">User id.</param>
+ /// <param name="imageType">Image type.</param>
+ /// <param name="imageIndex">Image index.</param>
+ /// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
+ /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="maxWidth">The maximum image width to return.</param>
+ /// <param name="maxHeight">The maximum image height to return.</param>
+ /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
+ /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
+ /// <param name="width">The fixed image width to return.</param>
+ /// <param name="height">The fixed image height to return.</param>
+ /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
+ /// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
+ /// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
+ /// <param name="blur">Optional. Blur image.</param>
+ /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
+ /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
+ /// <response code="200">Image stream returned.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>
+ /// A <see cref="FileStreamResult"/> containing the file stream on success,
+ /// or a <see cref="NotFoundResult"/> if item not found.
+ /// </returns>
+ [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
+ [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public async Task<ActionResult> GetUserImageByIndex(
+ [FromRoute, Required] Guid userId,
+ [FromRoute, Required] ImageType imageType,
+ [FromRoute, Required] int imageIndex,
+ [FromQuery] string? tag,
+ [FromQuery] ImageFormat? format,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] double? percentPlayed,
+ [FromQuery] int? unplayedCount,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? quality,
+ [FromQuery] bool? cropWhitespace,
+ [FromQuery] bool? addPlayedIndicator,
+ [FromQuery] int? blur,
+ [FromQuery] string? backgroundColor,
+ [FromQuery] string? foregroundLayer)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user?.ProfileImage == null)
+ {
+ return NotFound();
+ }
+
+ var info = new ItemImageInfo
+ {
+ Path = user.ProfileImage.Path,
+ Type = ImageType.Profile,
+ DateModified = user.ProfileImage.LastModified
+ };
+
+ if (width.HasValue)
+ {
+ info.Width = width.Value;
+ }
+
+ if (height.HasValue)
+ {
+ info.Height = height.Value;
+ }
+
+ return await GetImageInternal(
+ user.Id,
+ imageType,
+ imageIndex,
+ tag,
+ format,
+ maxWidth,
+ maxHeight,
+ percentPlayed,
+ unplayedCount,
+ width,
+ height,
+ quality,
+ cropWhitespace,
+ addPlayedIndicator,
+ blur,
+ backgroundColor,
+ foregroundLayer,
+ null,
+ Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase),
+ info)
+ .ConfigureAwait(false);
+ }
+
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
{
using var reader = new StreamReader(inputStream);
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index d17a26db4..244625752 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
- [HttpGet("Artists/InstantMix")]
+ [HttpGet("Artists/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
[FromRoute, Required] Guid id,
@@ -242,7 +242,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
- [HttpGet("MusicGenres/InstantMix")]
+ [HttpGet("MusicGenres/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
[FromRoute, Required] Guid id,
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index a7c1a6388..6c38f77ce 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 0a6ed31ae..9e1a39853 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index d8d371ebc..7e9035f80 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
@@ -60,7 +60,6 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets items based on a query.
/// </summary>
- /// <param name="uId">The user id supplied in the /Users/{uid}/Items.</param>
/// <param name="userId">The user id supplied as query parameter.</param>
/// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
/// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
@@ -73,8 +72,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
/// <param name="isHd">Optional filter by items that are HD or not.</param>
/// <param name="is4K">Optional filter by items that are 4K or not.</param>
- /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
- /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
+ /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
@@ -87,42 +86,42 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
- /// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
+ /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="isPlayed">Optional filter by items that are played, or not.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param>
- /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
- /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
+ /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+ /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
+ /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
- /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
- /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
+ /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
+ /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
- /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
+ /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
/// <param name="isLocked">Optional filter by items that are locked.</param>
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
@@ -133,20 +132,18 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
/// <param name="is3D">Optional filter by items that are 3D, or not.</param>
- /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
+ /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
+ /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
[HttpGet("Items")]
- [HttpGet("Users/{uId}/Items", Name = "GetItems_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetItems(
- [FromRoute] Guid? uId,
[FromQuery] Guid? userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
@@ -159,7 +156,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
- [FromQuery] string? locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
@@ -173,42 +170,42 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId,
- [FromQuery] string? excludeItemIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? recursive,
[FromQuery] string? searchTerm,
[FromQuery] string? sortOrder,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? isPlayed,
- [FromQuery] string? genres,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? artists,
- [FromQuery] string? excludeArtistIds,
- [FromQuery] string? artistIds,
- [FromQuery] string? albumArtistIds,
- [FromQuery] string? contributingArtistIds,
- [FromQuery] string? albums,
- [FromQuery] string? albumIds,
- [FromQuery] string? ids,
- [FromQuery] string? videoTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder,
@@ -219,18 +216,15 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? is3D,
- [FromQuery] string? seriesStatus,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery] string? studioIds,
- [FromQuery] string? genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
- // use user id route parameter over query parameter
- userId = uId ?? userId;
-
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
: null;
@@ -238,20 +232,15 @@ namespace Jellyfin.Api.Controllers
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)
- || string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
+ if (includeItemTypes.Length == 1
+ && (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase)
+ || includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase)))
{
parentId = null;
}
- BaseItem? item = null;
+ var item = _libraryManager.GetParentItem(parentId, userId);
QueryResult<BaseItem> result;
- if (!string.IsNullOrEmpty(parentId))
- {
- item = _libraryManager.GetItemById(parentId);
- }
-
- item ??= _libraryManager.GetUserRootFolder();
if (!(item is Folder folder))
{
@@ -262,7 +251,7 @@ namespace Jellyfin.Api.Controllers
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{
recursive = true;
- includeItemTypes = "Playlist";
+ includeItemTypes = new[] { "Playlist" };
}
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
@@ -291,14 +280,14 @@ namespace Jellyfin.Api.Controllers
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
}
- if ((recursive.HasValue && recursive.Value) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder))
+ if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder))
{
var query = new InternalItemsQuery(user!)
{
IsPlayed = isPlayed,
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+ MediaTypes = mediaTypes,
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
Recursive = recursive ?? false,
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
IsFavorite = isFavorite,
@@ -330,28 +319,28 @@ namespace Jellyfin.Api.Controllers
HasTrailer = hasTrailer,
IsHD = isHd,
Is4K = is4K,
- Tags = RequestHelpers.Split(tags, '|', true),
- OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
- Genres = RequestHelpers.Split(genres, '|', true),
- ArtistIds = RequestHelpers.GetGuids(artistIds),
- AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds),
- ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds),
- GenreIds = RequestHelpers.GetGuids(genreIds),
- StudioIds = RequestHelpers.GetGuids(studioIds),
+ Tags = tags,
+ OfficialRatings = officialRatings,
+ Genres = genres,
+ ArtistIds = artistIds,
+ AlbumArtistIds = albumArtistIds,
+ ContributingArtistIds = contributingArtistIds,
+ GenreIds = genreIds,
+ StudioIds = studioIds,
Person = person,
- PersonIds = RequestHelpers.GetGuids(personIds),
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
+ PersonIds = personIds,
+ PersonTypes = personTypes,
+ Years = years,
ImageTypes = imageTypes,
- VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(),
+ VideoTypes = videoTypes,
AdjacentTo = adjacentTo,
- ItemIds = RequestHelpers.GetGuids(ids),
+ ItemIds = ids,
MinCommunityRating = minCommunityRating,
MinCriticRating = minCriticRating,
- ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId),
+ ParentId = parentId ?? Guid.Empty,
ParentIndexNumber = parentIndexNumber,
EnableTotalRecordCount = enableTotalRecordCount,
- ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds),
+ ExcludeItemIds = excludeItemIds,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
@@ -360,7 +349,7 @@ namespace Jellyfin.Api.Controllers
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
};
- if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm))
+ if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
{
query.CollapseBoxSetItems = false;
}
@@ -400,9 +389,9 @@ namespace Jellyfin.Api.Controllers
}
// Filter by Series Status
- if (!string.IsNullOrEmpty(seriesStatus))
+ if (seriesStatus.Length != 0)
{
- query.SeriesStatuses = seriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray();
+ query.SeriesStatuses = seriesStatus;
}
// ExcludeLocationTypes
@@ -411,13 +400,9 @@ namespace Jellyfin.Api.Controllers
query.IsVirtualItem = false;
}
- if (!string.IsNullOrEmpty(locationTypes))
+ if (locationTypes.Length > 0 && locationTypes.Length < 4)
{
- var requestedLocationTypes = locationTypes.Split(',');
- if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
- {
- query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
- }
+ query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
}
// Min official rating
@@ -433,9 +418,9 @@ namespace Jellyfin.Api.Controllers
}
// Artists
- if (!string.IsNullOrEmpty(artists))
+ if (artists.Length != 0)
{
- query.ArtistIds = artists.Split('|').Select(i =>
+ query.ArtistIds = artists.Select(i =>
{
try
{
@@ -449,29 +434,29 @@ namespace Jellyfin.Api.Controllers
}
// ExcludeArtistIds
- if (!string.IsNullOrWhiteSpace(excludeArtistIds))
+ if (excludeArtistIds.Length != 0)
{
- query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
+ query.ExcludeArtistIds = excludeArtistIds;
}
- if (!string.IsNullOrWhiteSpace(albumIds))
+ if (albumIds.Length != 0)
{
- query.AlbumIds = RequestHelpers.GetGuids(albumIds);
+ query.AlbumIds = albumIds;
}
// Albums
- if (!string.IsNullOrEmpty(albums))
+ if (albums.Length != 0)
{
- query.AlbumIds = albums.Split('|').SelectMany(i =>
+ query.AlbumIds = albums.SelectMany(i =>
{
return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
}).ToArray();
}
// Studios
- if (!string.IsNullOrEmpty(studios))
+ if (studios.Length != 0)
{
- query.StudioIds = studios.Split('|').Select(i =>
+ query.StudioIds = studios.Select(i =>
{
try
{
@@ -508,18 +493,269 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets items based on a query.
/// </summary>
+ /// <param name="userId">The user id supplied as query parameter.</param>
+ /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
+ /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
+ /// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
+ /// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
+ /// <param name="hasTrailer">Optional filter by items with trailers.</param>
+ /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="parentIndexNumber">Optional filter by parent index number.</param>
+ /// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
+ /// <param name="isHd">Optional filter by items that are HD or not.</param>
+ /// <param name="is4K">Optional filter by items that are 4K or not.</param>
+ /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
+ /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
+ /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
+ /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
+ /// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
+ /// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
+ /// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
+ /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
+ /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
+ /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
+ /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
+ /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
+ /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
+ /// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
+ /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
+ /// <param name="searchTerm">Optional. Filter based on a search term.</param>
+ /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+ /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
+ /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
+ /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="isPlayed">Optional filter by items that are played, or not.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</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 delimeted.</param>
+ /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
+ /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
+ /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
+ /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
+ /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
+ /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
+ /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
+ /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
+ /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
+ /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
+ /// <param name="isLocked">Optional filter by items that are locked.</param>
+ /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
+ /// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
+ /// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
+ /// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
+ /// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
+ /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
+ /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
+ /// <param name="is3D">Optional filter by items that are 3D, or not.</param>
+ /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
+ /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
+ /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
+ /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
+ /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
+ /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
+ /// <param name="enableImages">Optional, include image information in output.</param>
+ /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
+ [HttpGet("Users/{userId}/Items")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId(
+ [FromRoute] Guid userId,
+ [FromQuery] string? maxOfficialRating,
+ [FromQuery] bool? hasThemeSong,
+ [FromQuery] bool? hasThemeVideo,
+ [FromQuery] bool? hasSubtitles,
+ [FromQuery] bool? hasSpecialFeature,
+ [FromQuery] bool? hasTrailer,
+ [FromQuery] string? adjacentTo,
+ [FromQuery] int? parentIndexNumber,
+ [FromQuery] bool? hasParentalRating,
+ [FromQuery] bool? isHd,
+ [FromQuery] bool? is4K,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
+ [FromQuery] bool? isMissing,
+ [FromQuery] bool? isUnaired,
+ [FromQuery] double? minCommunityRating,
+ [FromQuery] double? minCriticRating,
+ [FromQuery] DateTime? minPremiereDate,
+ [FromQuery] DateTime? minDateLastSaved,
+ [FromQuery] DateTime? minDateLastSavedForUser,
+ [FromQuery] DateTime? maxPremiereDate,
+ [FromQuery] bool? hasOverview,
+ [FromQuery] bool? hasImdbId,
+ [FromQuery] bool? hasTmdbId,
+ [FromQuery] bool? hasTvdbId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? recursive,
+ [FromQuery] string? searchTerm,
+ [FromQuery] string? sortOrder,
+ [FromQuery] Guid? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery] bool? isFavorite,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
+ [FromQuery] string? sortBy,
+ [FromQuery] bool? isPlayed,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
+ [FromQuery] bool? enableUserData,
+ [FromQuery] int? imageTypeLimit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
+ [FromQuery] string? person,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
+ [FromQuery] string? minOfficialRating,
+ [FromQuery] bool? isLocked,
+ [FromQuery] bool? isPlaceHolder,
+ [FromQuery] bool? hasOfficialRating,
+ [FromQuery] bool? collapseBoxSetItems,
+ [FromQuery] int? minWidth,
+ [FromQuery] int? minHeight,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
+ [FromQuery] bool? is3D,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
+ [FromQuery] string? nameStartsWithOrGreater,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool? enableImages = true)
+ {
+ return GetItems(
+ userId,
+ maxOfficialRating,
+ hasThemeSong,
+ hasThemeVideo,
+ hasSubtitles,
+ hasSpecialFeature,
+ hasTrailer,
+ adjacentTo,
+ parentIndexNumber,
+ hasParentalRating,
+ isHd,
+ is4K,
+ locationTypes,
+ excludeLocationTypes,
+ isMissing,
+ isUnaired,
+ minCommunityRating,
+ minCriticRating,
+ minPremiereDate,
+ minDateLastSaved,
+ minDateLastSavedForUser,
+ maxPremiereDate,
+ hasOverview,
+ hasImdbId,
+ hasTmdbId,
+ hasTvdbId,
+ excludeItemIds,
+ startIndex,
+ limit,
+ recursive,
+ searchTerm,
+ sortOrder,
+ parentId,
+ fields,
+ excludeItemTypes,
+ includeItemTypes,
+ filters,
+ isFavorite,
+ mediaTypes,
+ imageTypes,
+ sortBy,
+ isPlayed,
+ genres,
+ officialRatings,
+ tags,
+ years,
+ enableUserData,
+ imageTypeLimit,
+ enableImageTypes,
+ person,
+ personIds,
+ personTypes,
+ studios,
+ artists,
+ excludeArtistIds,
+ artistIds,
+ albumArtistIds,
+ contributingArtistIds,
+ albums,
+ albumIds,
+ ids,
+ videoTypes,
+ minOfficialRating,
+ isLocked,
+ isPlaceHolder,
+ hasOfficialRating,
+ collapseBoxSetItems,
+ minWidth,
+ minHeight,
+ maxWidth,
+ maxHeight,
+ is3D,
+ seriesStatus,
+ nameStartsWithOrGreater,
+ nameStartsWith,
+ nameLessThan,
+ studioIds,
+ genreIds,
+ enableTotalRecordCount,
+ enableImages);
+ }
+
+ /// <summary>
+ /// Gets items based on a query.
+ /// </summary>
/// <param name="userId">The user id.</param>
/// <param name="startIndex">The start index.</param>
/// <param name="limit">The item limit.</param>
/// <param name="searchTerm">The search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <response code="200">Items returned.</response>
@@ -531,19 +767,19 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
var user = _userManager.GetUserById(userId);
- var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
+ var parentIdGuid = parentId ?? Guid.Empty;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -569,13 +805,13 @@ namespace Jellyfin.Api.Controllers
ParentId = parentIdGuid,
Recursive = true,
DtoOptions = dtoOptions,
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+ MediaTypes = mediaTypes,
IsVirtualItem = false,
CollapseBoxSetItems = false,
EnableTotalRecordCount = enableTotalRecordCount,
AncestorIds = ancestorIds,
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
SearchTerm = searchTerm
});
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 1b115d800..184843b39 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -362,15 +362,14 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
- public ActionResult DeleteItems([FromQuery] string? ids)
+ public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
- if (string.IsNullOrEmpty(ids))
+ if (ids.Length == 0)
{
return NoContent();
}
- var itemIds = RequestHelpers.Split(ids, ',', true);
- foreach (var i in itemIds)
+ foreach (var i in ids)
{
var item = _libraryManager.GetItemById(i);
var auth = _authContext.GetAuthorizationInfo(Request);
@@ -691,7 +690,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
[FromRoute, Required] Guid itemId,
- [FromQuery] string? excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
@@ -753,9 +752,9 @@ namespace Jellyfin.Api.Controllers
};
// ExcludeArtistIds
- if (!string.IsNullOrEmpty(excludeArtistIds))
+ if (excludeArtistIds.Length != 0)
{
- query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
+ query.ExcludeArtistIds = excludeArtistIds;
}
List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 29c0f1df4..56d4b3933 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -17,7 +17,6 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Dto;
@@ -150,7 +149,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
- [FromQuery] string? sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] SortOrder? sortOrder,
[FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true)
@@ -175,7 +174,7 @@ namespace Jellyfin.Api.Controllers
IsNews = isNews,
IsKids = isKids,
IsSports = isSports,
- SortBy = RequestHelpers.Split(sortBy, ',', true),
+ SortBy = sortBy,
SortOrder = sortOrder ?? SortOrder.Ascending,
AddCurrentProgram = addCurrentProgram
},
@@ -539,7 +538,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
- [FromQuery] string? channelIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
[FromQuery] Guid? userId,
[FromQuery] DateTime? minStartDate,
[FromQuery] bool? hasAired,
@@ -556,8 +555,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery] string? sortBy,
[FromQuery] string? sortOrder,
- [FromQuery] string? genres,
- [FromQuery] string? genreIds,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -573,8 +572,7 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ChannelIds = RequestHelpers.Split(channelIds, ',', true)
- .Select(i => new Guid(i)).ToArray(),
+ ChannelIds = channelIds,
HasAired = hasAired,
IsAiring = isAiring,
EnableTotalRecordCount = enableTotalRecordCount,
@@ -591,8 +589,8 @@ namespace Jellyfin.Api.Controllers
IsKids = isKids,
IsSports = isSports,
SeriesTimerId = seriesTimerId,
- Genres = RequestHelpers.Split(genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(genreIds)
+ Genres = genres,
+ GenreIds = genreIds
};
if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
@@ -628,8 +626,7 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true)
- .Select(i => new Guid(i)).ToArray(),
+ ChannelIds = body.ChannelIds,
HasAired = body.HasAired,
IsAiring = body.IsAiring,
EnableTotalRecordCount = body.EnableTotalRecordCount,
@@ -646,8 +643,8 @@ namespace Jellyfin.Api.Controllers
IsKids = body.IsKids,
IsSports = body.IsSports,
SeriesTimerId = body.SeriesTimerId,
- Genres = RequestHelpers.Split(body.Genres, '|', true),
- GenreIds = RequestHelpers.GetGuids(body.GenreIds)
+ Genres = body.Genres,
+ GenreIds = body.GenreIds
};
if (!body.LibrarySeriesId.Equals(Guid.Empty))
@@ -703,7 +700,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
@@ -723,7 +720,7 @@ namespace Jellyfin.Api.Controllers
IsNews = isNews,
IsSports = isSports,
EnableTotalRecordCount = enableTotalRecordCount,
- GenreIds = RequestHelpers.GetGuids(genreIds)
+ GenreIds = genreIds
};
var dtoOptions = new DtoOptions { Fields = fields }
@@ -1017,7 +1014,9 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(pw))
{
using var sha = SHA1.Create();
- listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw)));
+ // TODO: remove ToLower when Convert.ToHexString supports lowercase
+ // Schedules Direct requires the hex to be lowercase
+ listingsProviderInfo.Password = Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
}
return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
@@ -1155,7 +1154,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="newDevicesOnly">Only discover new tuners.</param>
/// <response code="200">Tuners returned.</response>
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
- [HttpGet("Tuners/Discvover")]
+ [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
+ [HttpGet("Tuners/Discover")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs
index ef2e7e8b1..3d8b9e0ca 100644
--- a/Jellyfin.Api/Controllers/LocalizationController.cs
+++ b/Jellyfin.Api/Controllers/LocalizationController.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using Jellyfin.Api.Constants;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 186024585..a76dc057a 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Buffers;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@@ -8,7 +8,6 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos;
-using Jellyfin.Api.Models.VideoDtos;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
@@ -81,6 +80,9 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets live playback media info for an item.
/// </summary>
+ /// <remarks>
+ /// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
+ /// </remarks>
/// <param name="itemId">The item id.</param>
/// <param name="userId">The user id.</param>
/// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
@@ -90,13 +92,13 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxAudioChannels">The maximum number of audio channels.</param>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="liveStreamId">The livestream id.</param>
- /// <param name="deviceProfile">The device profile.</param>
/// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param>
/// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
/// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
/// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param>
/// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param>
/// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
+ /// <param name="playbackInfoDto">The playback info.</param>
/// <response code="200">Playback info returned.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
[HttpPost("Items/{itemId}/PlaybackInfo")]
@@ -111,18 +113,17 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxAudioChannels,
[FromQuery] string? mediaSourceId,
[FromQuery] string? liveStreamId,
- [FromBody] DeviceProfileDto? deviceProfile,
- [FromQuery] bool autoOpenLiveStream = false,
- [FromQuery] bool enableDirectPlay = true,
- [FromQuery] bool enableDirectStream = true,
- [FromQuery] bool enableTranscoding = true,
- [FromQuery] bool allowVideoStreamCopy = true,
- [FromQuery] bool allowAudioStreamCopy = true)
+ [FromQuery] bool? autoOpenLiveStream,
+ [FromQuery] bool? enableDirectPlay,
+ [FromQuery] bool? enableDirectStream,
+ [FromQuery] bool? enableTranscoding,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromBody] PlaybackInfoDto? playbackInfoDto)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
- var profile = deviceProfile?.DeviceProfile;
-
+ var profile = playbackInfoDto?.DeviceProfile;
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
if (profile == null)
@@ -134,6 +135,23 @@ namespace Jellyfin.Api.Controllers
}
}
+ // Copy params from posted body
+ // TODO clean up when breaking API compatibility.
+ userId ??= playbackInfoDto?.UserId;
+ maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate;
+ startTimeTicks ??= playbackInfoDto?.StartTimeTicks;
+ audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex;
+ subtitleStreamIndex ??= playbackInfoDto?.SubtitleStreamIndex;
+ maxAudioChannels ??= playbackInfoDto?.MaxAudioChannels;
+ mediaSourceId ??= playbackInfoDto?.MediaSourceId;
+ liveStreamId ??= playbackInfoDto?.LiveStreamId;
+ autoOpenLiveStream ??= playbackInfoDto?.AutoOpenLiveStream ?? false;
+ enableDirectPlay ??= playbackInfoDto?.EnableDirectPlay ?? true;
+ enableDirectStream ??= playbackInfoDto?.EnableDirectStream ?? true;
+ enableTranscoding ??= playbackInfoDto?.EnableTranscoding ?? true;
+ allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true;
+ allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? true;
+
var info = await _mediaInfoHelper.GetPlaybackInfo(
itemId,
userId,
@@ -161,18 +179,18 @@ namespace Jellyfin.Api.Controllers
maxAudioChannels,
info!.PlaySessionId!,
userId ?? Guid.Empty,
- enableDirectPlay,
- enableDirectStream,
- enableTranscoding,
- allowVideoStreamCopy,
- allowAudioStreamCopy,
+ enableDirectPlay.Value,
+ enableDirectStream.Value,
+ enableTranscoding.Value,
+ allowVideoStreamCopy.Value,
+ allowAudioStreamCopy.Value,
Request.HttpContext.GetNormalizedRemoteIp());
}
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
}
- if (autoOpenLiveStream)
+ if (autoOpenLiveStream.Value)
{
var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal));
@@ -183,9 +201,9 @@ namespace Jellyfin.Api.Controllers
new LiveStreamRequest
{
AudioStreamIndex = audioStreamIndex,
- DeviceProfile = deviceProfile?.DeviceProfile,
- EnableDirectPlay = enableDirectPlay,
- EnableDirectStream = enableDirectStream,
+ DeviceProfile = playbackInfoDto?.DeviceProfile,
+ EnableDirectPlay = enableDirectPlay.Value,
+ EnableDirectStream = enableDirectStream.Value,
ItemId = itemId,
MaxAudioChannels = maxAudioChannels,
MaxStreamingBitrate = maxStreamingBitrate,
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index ebc148fe5..4d788ad7d 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Recommendations")]
public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
[FromQuery] Guid? userId,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] int categoryLimit = 5,
[FromQuery] int itemLimit = 8)
@@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers
var categories = new List<RecommendationDto>();
- var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
+ var parentIdGuid = parentId ?? Guid.Empty;
var query = new InternalItemsQuery(user)
{
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 229d9ff02..2608a9cd0 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
@@ -72,10 +72,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
@@ -109,21 +109,21 @@ namespace Jellyfin.Api.Controllers
EnableTotalRecordCount = enableTotalRecordCount
};
- if (!string.IsNullOrWhiteSpace(parentId))
+ if (parentId.HasValue)
{
if (parentItem is Folder)
{
- query.AncestorIds = new[] { new Guid(parentId) };
+ query.AncestorIds = new[] { parentId.Value };
}
else
{
- query.ItemIds = new[] { new Guid(parentId) };
+ query.ItemIds = new[] { parentId.Value };
}
}
var result = _libraryManager.GetMusicGenres(query);
- var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes);
+ var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 1f797d6bc..6295dfc05 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -45,13 +45,13 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PackageInfo>> GetPackageInfo(
[FromRoute, Required] string name,
- [FromQuery] string? assemblyGuid)
+ [FromQuery] Guid? assemblyGuid)
{
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
var result = _installationManager.FilterPackages(
packages,
name,
- string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid))
+ assemblyGuid ?? default)
.FirstOrDefault();
if (result == null)
@@ -92,21 +92,21 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
public async Task<ActionResult> InstallPackage(
[FromRoute, Required] string name,
- [FromQuery] string? assemblyGuid,
+ [FromQuery] Guid? assemblyGuid,
[FromQuery] string? version,
[FromQuery] string? repositoryUrl)
{
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
if (!string.IsNullOrEmpty(repositoryUrl))
{
- packages = packages.Where(p => p.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase))
+ packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any())
.ToList();
}
var package = _installationManager.GetCompatibleVersions(
packages,
name,
- string.IsNullOrEmpty(assemblyGuid) ? Guid.Empty : Guid.Parse(assemblyGuid),
+ assemblyGuid ?? Guid.Empty,
specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version))
.FirstOrDefault();
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 6ac3e6417..17e631197 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
@@ -77,9 +77,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
- [FromQuery] string? excludePersonTypes,
- [FromQuery] string? personTypes,
- [FromQuery] string? appearsInItemId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery] Guid? appearsInItemId,
[FromQuery] Guid? userId,
[FromQuery] bool? enableImages = true)
{
@@ -97,12 +97,12 @@ namespace Jellyfin.Api.Controllers
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
{
- PersonTypes = RequestHelpers.Split(personTypes, ',', true),
- ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true),
+ PersonTypes = personTypes,
+ ExcludePersonTypes = excludePersonTypes,
NameContains = searchTerm,
User = user,
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
- AppearsInItemId = string.IsNullOrEmpty(appearsInItemId) ? Guid.Empty : Guid.Parse(appearsInItemId),
+ AppearsInItemId = appearsInItemId ?? Guid.Empty,
Limit = limit ?? 0
});
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 4b3d8d3d3..3e55434c0 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
@@ -63,11 +63,10 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
[FromBody, Required] CreatePlaylistDto createPlaylistRequest)
{
- Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{
Name = createPlaylistRequest.Name,
- ItemIdList = idGuidArray,
+ ItemIdList = createPlaylistRequest.Ids,
UserId = createPlaylistRequest.UserId,
MediaType = createPlaylistRequest.MediaType
}).ConfigureAwait(false);
@@ -87,10 +86,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddToPlaylist(
[FromRoute, Required] Guid playlistId,
- [FromQuery] string? ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] Guid? userId)
{
- await _playlistManager.AddToPlaylistAsync(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty).ConfigureAwait(false);
+ await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false);
return NoContent();
}
@@ -122,9 +121,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task<ActionResult> RemoveFromPlaylist([FromRoute, Required] string playlistId, [FromQuery] string? entryIds)
+ public async Task<ActionResult> RemoveFromPlaylist(
+ [FromRoute, Required] string playlistId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
{
- await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false);
+ await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 5c15e9a0d..ec7b84ff6 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -1,9 +1,10 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
@@ -74,7 +75,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<UserItemDataDto> MarkPlayedItem(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
- [FromQuery] DateTime? datePlayed)
+ [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
{
var user = _userManager.GetUserById(userId);
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 0f8ceba29..98f1bc2d2 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 73da2f906..4ac849181 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
{
if (_quickConnect.State == QuickConnectState.Unavailable)
{
- return Forbid("Quick connect is unavailable");
+ return StatusCode(StatusCodes.Status403Forbidden, "Quick connect is unavailable");
}
_quickConnect.Activate();
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
if (!userId.HasValue)
{
- return Forbid("Unknown user id");
+ return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id");
}
return _quickConnect.AuthorizeRequest(userId.Value, code);
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index e75f0d06b..08255ff8f 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -82,10 +83,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery] Guid? userId,
[FromQuery, Required] string searchTerm,
- [FromQuery] string? includeItemTypes,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? mediaTypes,
- [FromQuery] string? parentId,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery] Guid? parentId,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
[FromQuery] bool? isNews,
@@ -108,9 +109,9 @@ namespace Jellyfin.Api.Controllers
IncludeStudios = includeStudios,
StartIndex = startIndex,
UserId = userId ?? Guid.Empty,
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
- ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
- MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
+ IncludeItemTypes = includeItemTypes,
+ ExcludeItemTypes = excludeItemTypes,
+ MediaTypes = mediaTypes,
ParentId = parentId,
IsKids = isKids,
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index e506ac7bf..e2269a2ce 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -6,6 +6,7 @@ using System.Threading;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
+using Jellyfin.Api.Models.SessionDtos;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
@@ -160,12 +161,12 @@ namespace Jellyfin.Api.Controllers
public ActionResult Play(
[FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand,
- [FromQuery, Required] string itemIds,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
[FromQuery] long? startPositionTicks)
{
var playRequest = new PlayRequest
{
- ItemIds = RequestHelpers.GetGuids(itemIds),
+ ItemIds = itemIds,
StartPositionTicks = startPositionTicks,
PlayCommand = playCommand
};
@@ -378,7 +379,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostCapabilities(
[FromQuery] string? id,
- [FromQuery] string? playableMediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false,
@@ -391,7 +392,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.ReportCapabilities(id, new ClientCapabilities
{
- PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true),
+ PlayableMediaTypes = playableMediaTypes,
SupportedCommands = supportedCommands,
SupportsMediaControl = supportsMediaControl,
SupportsSync = supportsSync,
@@ -412,14 +413,14 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostFullCapabilities(
[FromQuery] string? id,
- [FromBody, Required] ClientCapabilities capabilities)
+ [FromBody, Required] ClientCapabilitiesDto capabilities)
{
if (string.IsNullOrWhiteSpace(id))
{
id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
}
- _sessionManager.ReportCapabilities(id, capabilities);
+ _sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 9c259cc19..d9cb34557 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.StartupDtos;
+using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
@@ -72,9 +73,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration)
{
- _config.Configuration.UICulture = startupConfiguration.UICulture;
- _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode;
- _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage;
+ _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty;
+ _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty;
+ _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty;
_config.SaveConfiguration();
return NoContent();
}
@@ -89,9 +90,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto)
{
- _config.Configuration.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
- _config.Configuration.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
- _config.SaveConfiguration();
+ NetworkConfiguration settings = _config.GetNetworkConfiguration();
+ settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
+ settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
+ _config.SaveConfiguration("network", settings);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index 27dcd51bc..bb54c59f6 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
@@ -71,10 +71,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
@@ -94,13 +94,10 @@ namespace Jellyfin.Api.Controllers
var parentItem = _libraryManager.GetParentItem(parentId, userId);
- var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
- var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
-
var query = new InternalItemsQuery(user)
{
- ExcludeItemTypes = excludeItemTypesArr,
- IncludeItemTypes = includeItemTypesArr,
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
@@ -112,20 +109,20 @@ namespace Jellyfin.Api.Controllers
EnableTotalRecordCount = enableTotalRecordCount
};
- if (!string.IsNullOrWhiteSpace(parentId))
+ if (parentId.HasValue)
{
if (parentItem is Folder)
{
- query.AncestorIds = new[] { new Guid(parentId) };
+ query.AncestorIds = new[] { parentId.Value };
}
else
{
- query.ItemIds = new[] { new Guid(parentId) };
+ query.ItemIds = new[] { parentId.Value };
}
}
var result = _libraryManager.GetStudios(query);
- var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
+ var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index a01ae31a0..dcb8e803b 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -193,7 +193,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
[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(
@@ -204,7 +203,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] long? endPositionTicks,
[FromQuery] bool copyTimestamps = false,
[FromQuery] bool addVttTimeMap = false,
- [FromRoute] long startPositionTicks = 0)
+ [FromQuery] long startPositionTicks = 0)
{
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
{
@@ -250,6 +249,43 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
+ /// Gets subtitles in a specified format.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="mediaSourceId">The media source id.</param>
+ /// <param name="index">The subtitle stream index.</param>
+ /// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
+ /// <param name="format">The format of the returned subtitle.</param>
+ /// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
+ /// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
+ /// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
+ /// <response code="200">File returned.</response>
+ /// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
+ [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile("text/*")]
+ public Task<ActionResult> GetSubtitleWithTicks(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string mediaSourceId,
+ [FromRoute, Required] int index,
+ [FromRoute, Required] long startPositionTicks,
+ [FromRoute, Required] string format,
+ [FromQuery] long? endPositionTicks,
+ [FromQuery] bool copyTimestamps = false,
+ [FromQuery] bool addVttTimeMap = false)
+ {
+ return GetSubtitle(
+ itemId,
+ mediaSourceId,
+ index,
+ format,
+ endPositionTicks,
+ copyTimestamps,
+ addVttTimeMap,
+ startPositionTicks);
+ }
+
+ /// <summary>
/// Gets an HLS subtitle playlist.
/// </summary>
/// <param name="itemId">The item id.</param>
@@ -335,6 +371,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="204">Subtitle uploaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Videos/{itemId}/Subtitles")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UploadSubtitle(
[FromRoute, Required] Guid itemId,
[FromBody, Required] UploadSubtitleDto body)
@@ -446,6 +483,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("FallbackFont/Fonts/{name}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile("font/*")]
public ActionResult GetFallbackFont([FromRoute, Required] string name)
{
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index ad64adfba..9f1dec712 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -1,9 +1,10 @@
-using System;
+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.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -58,8 +59,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
[FromRoute, Required] Guid userId,
- [FromQuery] string? mediaType,
- [FromQuery] string? type,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false)
@@ -70,8 +71,8 @@ namespace Jellyfin.Api.Controllers
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
{
OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
- MediaTypes = RequestHelpers.Split(mediaType!, ',', true),
- IncludeItemTypes = RequestHelpers.Split(type!, ',', true),
+ MediaTypes = mediaType,
+ IncludeItemTypes = type,
IsVirtualItem = false,
StartIndex = startIndex,
Limit = limit,
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index e16a10ba4..471c9180d 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -1,12 +1,15 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.Models.SyncPlayDtos;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
+using MediaBrowser.Controller.SyncPlay.Requests;
using MediaBrowser.Model.SyncPlay;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -17,7 +20,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// The sync play controller.
/// </summary>
- [Authorize(Policy = Policies.DefaultAuthorization)]
+ [Authorize(Policy = Policies.SyncPlayAccess)]
public class SyncPlayController : BaseJellyfinApiController
{
private readonly ISessionManager _sessionManager;
@@ -43,35 +46,36 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Create a new SyncPlay group.
/// </summary>
+ /// <param name="requestData">The settings of the new group.</param>
/// <response code="204">New group created.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("New")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlayCreateGroup()
+ [Authorize(Policy = Policies.SyncPlayCreateGroupAccess)]
+ public ActionResult SyncPlayCreateGroup(
+ [FromBody, Required] NewGroupRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- _syncPlayManager.NewGroup(currentSession, CancellationToken.None);
+ var syncPlayRequest = new NewGroupRequest(requestData.GroupName);
+ _syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Join an existing SyncPlay group.
/// </summary>
- /// <param name="groupId">The sync play group id.</param>
+ /// <param name="requestData">The group to join.</param>
/// <response code="204">Group join successful.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Join")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlayJoinGroup([FromQuery, Required] Guid groupId)
+ [Authorize(Policy = Policies.SyncPlayAccess)]
+ public ActionResult SyncPlayJoinGroup(
+ [FromBody, Required] JoinGroupRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
-
- var joinRequest = new JoinGroupRequest()
- {
- GroupId = groupId
- };
-
- _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None);
+ var syncPlayRequest = new JoinGroupRequest(requestData.GroupId);
+ _syncPlayManager.JoinGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
@@ -85,38 +89,125 @@ namespace Jellyfin.Api.Controllers
public ActionResult SyncPlayLeaveGroup()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None);
+ var syncPlayRequest = new LeaveGroupRequest();
+ _syncPlayManager.LeaveGroup(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
/// Gets all SyncPlay groups.
/// </summary>
- /// <param name="filterItemId">Optional. Filter by item id.</param>
/// <response code="200">Groups returned.</response>
/// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
[HttpGet("List")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<GroupInfoView>> SyncPlayGetGroups([FromQuery] Guid? filterItemId)
+ [Authorize(Policy = Policies.SyncPlayAccess)]
+ public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new ListGroupsRequest();
+ return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest));
+ }
+
+ /// <summary>
+ /// Request to set new playlist in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new playlist to play in the group.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetNewQueue")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlaySetNewQueue(
+ [FromBody, Required] PlayRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty));
+ var syncPlayRequest = new PlayGroupRequest(
+ requestData.PlayingQueue,
+ requestData.PlayingItemPosition,
+ requestData.StartPositionTicks);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
}
/// <summary>
- /// Request play in SyncPlay group.
+ /// Request to change playlist item in SyncPlay group.
/// </summary>
- /// <response code="204">Play request sent to all group members.</response>
+ /// <param name="requestData">The new item to play.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
- [HttpPost("Play")]
+ [HttpPost("SetPlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlayPlay()
+ public ActionResult SyncPlaySetPlaylistItem(
+ [FromBody, Required] SetPlaylistItemRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = PlaybackRequestType.Play
- };
+ var syncPlayRequest = new SetPlaylistItemGroupRequest(requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request to remove items from the playlist in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The items to remove.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("RemoveFromPlaylist")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayRemoveFromPlaylist(
+ [FromBody, Required] RemoveFromPlaylistRequestDto requestData)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new RemoveFromPlaylistGroupRequest(requestData.PlaylistItemIds);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request to move an item in the playlist in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new position for the item.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("MovePlaylistItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayMovePlaylistItem(
+ [FromBody, Required] MovePlaylistItemRequestDto requestData)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new MovePlaylistItemGroupRequest(requestData.PlaylistItemId, requestData.NewIndex);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request to queue items to the playlist of a SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The items to add.</param>
+ /// <response code="204">Queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Queue")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayQueue(
+ [FromBody, Required] QueueRequestDto requestData)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new QueueGroupRequest(requestData.ItemIds, requestData.Mode);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request unpause in SyncPlay group.
+ /// </summary>
+ /// <response code="204">Unpause update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Unpause")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayUnpause()
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new UnpauseGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
@@ -124,17 +215,29 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Request pause in SyncPlay group.
/// </summary>
- /// <response code="204">Pause request sent to all group members.</response>
+ /// <response code="204">Pause update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Pause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SyncPlayPause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = PlaybackRequestType.Pause
- };
+ var syncPlayRequest = new PauseGroupRequest();
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request stop in SyncPlay group.
+ /// </summary>
+ /// <response code="204">Stop update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Stop")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayStop()
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new StopGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
@@ -142,42 +245,143 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Request seek in SyncPlay group.
/// </summary>
- /// <param name="positionTicks">The playback position in ticks.</param>
- /// <response code="204">Seek request sent to all group members.</response>
+ /// <param name="requestData">The new playback position.</param>
+ /// <response code="204">Seek update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Seek")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlaySeek([FromQuery] long positionTicks)
+ public ActionResult SyncPlaySeek(
+ [FromBody, Required] SeekRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = PlaybackRequestType.Seek,
- PositionTicks = positionTicks
- };
+ var syncPlayRequest = new SeekGroupRequest(requestData.PositionTicks);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
/// <summary>
- /// Request group wait in SyncPlay group while buffering.
+ /// Notify SyncPlay group that member is buffering.
/// </summary>
- /// <param name="when">When the request has been made by the client.</param>
- /// <param name="positionTicks">The playback position in ticks.</param>
- /// <param name="bufferingDone">Whether the buffering is done.</param>
- /// <response code="204">Buffering request sent to all group members.</response>
+ /// <param name="requestData">The player status.</param>
+ /// <response code="204">Group state update sent to all group members.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Buffering")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlayBuffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone)
+ public ActionResult SyncPlayBuffering(
+ [FromBody, Required] BufferRequestDto requestData)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new BufferGroupRequest(
+ requestData.When,
+ requestData.PositionTicks,
+ requestData.IsPlaying,
+ requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Notify SyncPlay group that member is ready for playback.
+ /// </summary>
+ /// <param name="requestData">The player status.</param>
+ /// <response code="204">Group state update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("Ready")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayReady(
+ [FromBody, Required] ReadyRequestDto requestData)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new ReadyGroupRequest(
+ requestData.When,
+ requestData.PositionTicks,
+ requestData.IsPlaying,
+ requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request SyncPlay group to ignore member during group-wait.
+ /// </summary>
+ /// <param name="requestData">The settings to set.</param>
+ /// <response code="204">Member state updated.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetIgnoreWait")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlaySetIgnoreWait(
+ [FromBody, Required] IgnoreWaitRequestDto requestData)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new IgnoreWaitGroupRequest(requestData.IgnoreWait);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request next item in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The current item information.</param>
+ /// <response code="204">Next item update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("NextItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayNextItem(
+ [FromBody, Required] NextItemRequestDto requestData)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new NextItemGroupRequest(requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request previous item in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The current item information.</param>
+ /// <response code="204">Previous item update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("PreviousItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayPreviousItem(
+ [FromBody, Required] PreviousItemRequestDto requestData)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new PreviousItemGroupRequest(requestData.PlaylistItemId);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request to set repeat mode in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new repeat mode.</param>
+ /// <response code="204">Play queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetRepeatMode")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlaySetRepeatMode(
+ [FromBody, Required] SetRepeatModeRequestDto requestData)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new SetRepeatModeGroupRequest(requestData.Mode);
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Request to set shuffle mode in SyncPlay group.
+ /// </summary>
+ /// <param name="requestData">The new shuffle mode.</param>
+ /// <response code="204">Play queue update sent to all group members.</response>
+ /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
+ [HttpPost("SetShuffleMode")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlaySetShuffleMode(
+ [FromBody, Required] SetShuffleModeRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = bufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer,
- When = when,
- PositionTicks = positionTicks
- };
+ var syncPlayRequest = new SetShuffleModeGroupRequest(requestData.Mode);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
@@ -185,19 +389,16 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Update session ping.
/// </summary>
- /// <param name="ping">The ping.</param>
+ /// <param name="requestData">The new ping.</param>
/// <response code="204">Ping updated.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Ping")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlayPing([FromQuery] double ping)
+ public ActionResult SyncPlayPing(
+ [FromBody, Required] PingRequestDto requestData)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = PlaybackRequestType.Ping,
- Ping = Convert.ToInt64(ping)
- };
+ var syncPlayRequest = new PingGroupRequest(requestData.Ping);
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 4cb1984a2..7784e8a11 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
@@ -64,9 +64,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Info")]
[Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<SystemInfo>> GetSystemInfo()
+ public ActionResult<SystemInfo> GetSystemInfo()
{
- return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false);
+ return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
}
/// <summary>
@@ -76,9 +76,9 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns>
[HttpGet("Info/Public")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<PublicSystemInfo>> GetPublicSystemInfo()
+ public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
{
- return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false);
+ return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs
index 2dc744e7c..c730ac12b 100644
--- a/Jellyfin.Api/Controllers/TimeSyncController.cs
+++ b/Jellyfin.Api/Controllers/TimeSyncController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Globalization;
using MediaBrowser.Model.SyncPlay;
using Microsoft.AspNetCore.Http;
@@ -13,7 +13,7 @@ namespace Jellyfin.Api.Controllers
public class TimeSyncController : BaseJellyfinApiController
{
/// <summary>
- /// Gets the current utc time.
+ /// Gets the current UTC time.
/// </summary>
/// <response code="200">Time returned.</response>
/// <returns>An <see cref="UtcTimeResponse"/> to sync the client and server time.</returns>
@@ -22,18 +22,14 @@ namespace Jellyfin.Api.Controllers
public ActionResult<UtcTimeResponse> GetUtcTime()
{
// Important to keep the following line at the beginning
- var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
+ var requestReceptionTime = DateTime.UtcNow.ToUniversalTime();
- var response = new UtcTimeResponse();
- response.RequestReceptionTime = requestReceptionTime;
-
- // Important to keep the following two lines at the end
- var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
- response.ResponseTransmissionTime = responseTransmissionTime;
+ // Important to keep the following line at the end
+ var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime();
// Implementing NTP on such a high level results in this useless
// information being sent. On the other hand it enables future additions.
- return response;
+ return new UtcTimeResponse(requestReceptionTime, responseTransmissionTime);
}
}
}
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index d78adcbcd..8e9ece14f 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Model.Dto;
@@ -42,8 +42,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
/// <param name="isHd">Optional filter by items that are HD or not.</param>
/// <param name="is4K">Optional filter by items that are 4K or not.</param>
- /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
- /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
+ /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
+ /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
@@ -56,41 +56,41 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
- /// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
+ /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
- /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
- /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
+ /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
+ /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="isPlayed">Optional filter by items that are played, or not.</param>
- /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
- /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
- /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
- /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param>
+ /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
+ /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
+ /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
+ /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
- /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param>
- /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
- /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
+ /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
+ /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
+ /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
- /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
- /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
+ /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
+ /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
- /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
+ /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
/// <param name="isLocked">Optional filter by items that are locked.</param>
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
@@ -101,12 +101,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
/// <param name="is3D">Optional filter by items that are 3D, or not.</param>
- /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
+ /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
- /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
- /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
+ /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
+ /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
@@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
- [FromQuery] string? locationTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
@@ -139,41 +139,41 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId,
- [FromQuery] string? excludeItemIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? recursive,
[FromQuery] string? searchTerm,
[FromQuery] string? sortOrder,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery] string? mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? isPlayed,
- [FromQuery] string? genres,
- [FromQuery] string? officialRatings,
- [FromQuery] string? tags,
- [FromQuery] string? years,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
+ [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
- [FromQuery] string? personIds,
- [FromQuery] string? personTypes,
- [FromQuery] string? studios,
- [FromQuery] string? artists,
- [FromQuery] string? excludeArtistIds,
- [FromQuery] string? artistIds,
- [FromQuery] string? albumArtistIds,
- [FromQuery] string? contributingArtistIds,
- [FromQuery] string? albums,
- [FromQuery] string? albumIds,
- [FromQuery] string? ids,
- [FromQuery] string? videoTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder,
@@ -184,21 +184,20 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? is3D,
- [FromQuery] string? seriesStatus,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery] string? studioIds,
- [FromQuery] string? genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
- var includeItemTypes = "Trailer";
+ var includeItemTypes = new[] { "Trailer" };
return _itemsController
.GetItems(
userId,
- userId,
maxOfficialRating,
hasThemeSong,
hasThemeVideo,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 6fd154836..03fd1846d 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? seriesId,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -132,7 +132,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1);
- var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
+ var parentIdGuid = parentId ?? Guid.Empty;
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
@@ -176,7 +176,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="seriesId">The series id.</param>
/// <param name="userId">The user id.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="season">Optional filter by season number.</param>
/// <param name="seasonId">Optional. Filter by season id.</param>
/// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
@@ -188,20 +188,20 @@ namespace Jellyfin.Api.Controllers
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
- /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
[HttpGet("{seriesId}/Episodes")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
- [FromRoute, Required] string seriesId,
+ [FromRoute, Required] Guid seriesId,
[FromQuery] Guid? userId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] int? season,
- [FromQuery] string? seasonId,
+ [FromQuery] Guid? seasonId,
[FromQuery] bool? isMissing,
[FromQuery] string? adjacentTo,
- [FromQuery] string? startItemId,
+ [FromQuery] Guid? startItemId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? enableImages,
@@ -220,9 +220,9 @@ namespace Jellyfin.Api.Controllers
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
- if (!string.IsNullOrWhiteSpace(seasonId)) // Season id was supplied. Get episodes by season id.
+ if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
{
- var item = _libraryManager.GetItemById(new Guid(seasonId));
+ var item = _libraryManager.GetItemById(seasonId.Value);
if (!(item is Season seasonItem))
{
return NotFound("No season exists with Id " + seasonId);
@@ -264,10 +264,10 @@ namespace Jellyfin.Api.Controllers
.ToList();
}
- if (!string.IsNullOrWhiteSpace(startItemId))
+ if (startItemId.HasValue)
{
episodes = episodes
- .SkipWhile(i => !string.Equals(i.Id.ToString("N", CultureInfo.InvariantCulture), startItemId, StringComparison.OrdinalIgnoreCase))
+ .SkipWhile(i => startItemId.Value.Equals(i.Id))
.ToList();
}
@@ -303,7 +303,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="seriesId">The series id.</param>
/// <param name="userId">The user id.</param>
- /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="isSpecialSeason">Optional. Filter by special season.</param>
/// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
/// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
@@ -316,7 +316,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
- [FromRoute, Required] string seriesId,
+ [FromRoute, Required] Guid seriesId,
[FromQuery] Guid? userId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? isSpecialSeason,
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index e10f1fe91..34c9f32fa 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
@@ -96,7 +97,7 @@ namespace Jellyfin.Api.Controllers
[ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId,
- [FromQuery] string? container,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
@@ -191,8 +192,11 @@ namespace Jellyfin.Api.Controllers
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
{
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
+ // ffmpeg option -> file extension
+ // mpegts -> ts
+ // fmp4 -> mp4
// TODO: remove this when we switch back to the segment muxer
- var supportedHlsContainers = new[] { "mpegts", "fmp4" };
+ var supportedHlsContainers = new[] { "ts", "mp4" };
var dynamicHlsRequestDto = new HlsAudioRequestDto
{
@@ -201,7 +205,7 @@ namespace Jellyfin.Api.Controllers
Static = isStatic,
PlaySessionId = info.PlaySessionId,
// fallback to mpegts if device reports some weird value unsupported by hls
- SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
+ SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts",
MediaSourceId = mediaSourceId,
DeviceId = deviceId,
AudioCodec = audioCodec,
@@ -258,7 +262,7 @@ namespace Jellyfin.Api.Controllers
}
private DeviceProfile GetDeviceProfile(
- string? container,
+ string[] containers,
string? transcodingContainer,
string? audioCodec,
string? transcodingProtocol,
@@ -270,7 +274,6 @@ namespace Jellyfin.Api.Controllers
{
var deviceProfile = new DeviceProfile();
- var containers = RequestHelpers.Split(container, ',', true);
int len = containers.Length;
var directPlayProfiles = new DirectPlayProfile[len];
for (int i = 0; i < len; i++)
@@ -327,7 +330,7 @@ namespace Jellyfin.Api.Controllers
if (conditions.Count > 0)
{
// codec profile
- codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = container, Conditions = conditions.ToArray() });
+ codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = string.Join(',', containers), Conditions = conditions.ToArray() });
}
deviceProfile.CodecProfiles = codecProfiles.ToArray();
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index d61417480..0f0bee4bc 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw))
{
- return Forbid("Only sha1 password is not allowed.");
+ return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed.");
}
// Password should always be null
@@ -267,11 +267,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateUserPassword(
[FromRoute, Required] Guid userId,
- [FromBody] UpdateUserPassword request)
+ [FromBody, Required] UpdateUserPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to update the password.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
}
var user = _userManager.GetUserById(userId);
@@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
if (success == null)
{
- return Forbid("Invalid user or password entered.");
+ return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
}
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
@@ -325,11 +325,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateUserEasyPassword(
[FromRoute, Required] Guid userId,
- [FromBody] UpdateUserEasyPassword request)
+ [FromBody, Required] UpdateUserEasyPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
- return Forbid("User is not allowed to update the easy password.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
}
var user = _userManager.GetUserById(userId);
@@ -367,16 +367,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUser(
[FromRoute, Required] Guid userId,
- [FromBody] UserDto updateUser)
+ [FromBody, Required] UserDto updateUser)
{
- if (updateUser == null)
- {
- return BadRequest();
- }
-
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
{
- return Forbid("User update not allowed.");
+ return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
}
var user = _userManager.GetUserById(userId);
@@ -407,13 +402,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUserPolicy(
[FromRoute, Required] Guid userId,
- [FromBody] UserPolicy newPolicy)
+ [FromBody, Required] UserPolicy newPolicy)
{
- if (newPolicy == null)
- {
- return BadRequest();
- }
-
var user = _userManager.GetUserById(userId);
// If removing admin access
@@ -421,14 +411,14 @@ namespace Jellyfin.Api.Controllers
{
if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
{
- return Forbid("There must be at least one user in the system with administrative access.");
+ return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
}
}
// If disabling
if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
{
- return Forbid("Administrators cannot be disabled.");
+ return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled.");
}
// If disabling
@@ -436,7 +426,7 @@ namespace Jellyfin.Api.Controllers
{
if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
{
- return Forbid("There must be at least one enabled user in the system.");
+ return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
}
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
@@ -462,11 +452,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUserConfiguration(
[FromRoute, Required] Guid userId,
- [FromBody] UserConfiguration userConfig)
+ [FromBody, Required] UserConfiguration userConfig)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
{
- return Forbid("User configuration update not allowed");
+ return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
}
await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
@@ -483,7 +473,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("New")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] CreateUserByName request)
+ public async Task<ActionResult<UserDto>> CreateUserByName([FromBody, Required] CreateUserByName request)
{
var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index cfd851129..0e65591cc 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@@ -253,7 +253,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="userId">User id.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
- /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
+ /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="isPlayed">Filter by items that are played, or not.</param>
/// <param name="enableImages">Optional. include image information in output.</param>
/// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
@@ -269,7 +269,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
@@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
new LatestItemsQuery
{
GroupItems = groupItems,
- IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
+ IncludeItemTypes = includeItemTypes,
IsPlayed = isPlayed,
Limit = limit,
ParentId = parentId ?? Guid.Empty,
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index d575bfc3b..e1483ce9d 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -1,10 +1,11 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
[FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
- [FromQuery] string? presetViews,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
[FromQuery] bool includeHidden = false)
{
var query = new UserViewQuery
@@ -81,9 +82,9 @@ namespace Jellyfin.Api.Controllers
query.IncludeExternalContent = includeExternalContent.Value;
}
- if (!string.IsNullOrWhiteSpace(presetViews))
+ if (presetViews.Length != 0)
{
- query.PresetViews = RequestHelpers.Split(presetViews, ',', true);
+ query.PresetViews = presetViews;
}
var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 418c0c123..c2bb0dfff 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -43,7 +44,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Video or attachment not found.</response>
/// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
[HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")]
- [Produces(MediaTypeNames.Application.Octet)]
+ [ProducesFile(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetAttachment(
diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs
index 389dc8a08..7e743ee0c 100644
--- a/Jellyfin.Api/Controllers/VideoHlsController.cs
+++ b/Jellyfin.Api/Controllers/VideoHlsController.cs
@@ -1,8 +1,9 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -145,14 +146,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@@ -210,7 +211,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodingReasons,
+ [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
@@ -265,7 +266,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodingReasons,
+ TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
@@ -296,23 +297,23 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false);
TranscodingJobDto? job = null;
- var playlist = state.OutputFilePath;
+ var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
- if (!System.IO.File.Exists(playlist))
+ if (!System.IO.File.Exists(playlistPath))
{
- var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlist);
+ var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{
- if (!System.IO.File.Exists(playlist))
+ if (!System.IO.File.Exists(playlistPath))
{
// If the playlist doesn't already exist, startup ffmpeg
try
{
job = await _transcodingJobHelper.StartFfMpeg(
state,
- playlist,
- GetCommandLineArguments(playlist, state),
+ playlistPath,
+ GetCommandLineArguments(playlistPath, state),
Request,
TranscodingJobType,
cancellationTokenSource)
@@ -328,7 +329,7 @@ namespace Jellyfin.Api.Controllers
minSegments = state.MinSegments;
if (minSegments > 0)
{
- await HlsHelpers.WaitForMinimumSegmentCount(playlist, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
+ await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
}
}
}
@@ -338,14 +339,14 @@ namespace Jellyfin.Api.Controllers
}
}
- job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlist, TranscodingJobType);
+ job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job != null)
{
_transcodingJobHelper.OnTranscodeEndRequest(job);
}
- var playlistText = HlsHelpers.GetLivePlaylistText(playlist, state.SegmentLength);
+ var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
}
@@ -359,17 +360,46 @@ namespace Jellyfin.Api.Controllers
private string GetCommandLineArguments(string outputPath, StreamState state)
{
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
- var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
+ var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); // GetNumberOfThreads is static.
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
- var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts";
+ var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
+
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
- var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
+ var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
+ var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
+ var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
+ var outputTsArg = outputPrefix + "%d" + outputExtension;
- var segmentFormat = format.TrimStart('.');
+ var segmentFormat = outputExtension.TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{
segmentFormat = "mpegts";
}
+ else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ var outputFmp4HeaderArg = string.Empty;
+ var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ if (isWindows)
+ {
+ // on Windows, the path of fmp4 header file needs to be configured
+ outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
+ }
+ else
+ {
+ // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
+ outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
+ }
+
+ segmentFormat = "fmp4" + outputFmp4HeaderArg;
+ }
+ else
+ {
+ _logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat);
+ }
+
+ var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
+ ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
+ : "128";
var baseUrlParam = string.Format(
CultureInfo.InvariantCulture,
@@ -378,20 +408,19 @@ namespace Jellyfin.Api.Controllers
return string.Format(
CultureInfo.InvariantCulture,
- "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {7} -individual_header_trailer 0 -segment_format {8} -segment_list_entry_prefix {9} -segment_list_type m3u8 -segment_start_number 0 -segment_list \"{10}\" -y \"{11}\"",
+ "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number 0 -hls_base_url {9} -hls_playlist_type event -hls_segment_filename \"{10}\" -y \"{11}\"",
inputModifier,
_encodingHelper.GetInputArgument(state, _encodingOptions),
threads,
- _encodingHelper.GetMapArgs(state),
+ mapArgs,
GetVideoArguments(state),
GetAudioArguments(state),
+ maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
- string.Empty,
segmentFormat,
baseUrlParam,
- outputPath,
- outputTsArg)
- .Trim();
+ outputTsArg,
+ outputPath).Trim();
}
/// <summary>
@@ -401,14 +430,53 @@ namespace Jellyfin.Api.Controllers
/// <returns>The command line arguments for audio transcoding.</returns>
private string GetAudioArguments(StreamState state)
{
- var codec = _encodingHelper.GetAudioEncoder(state);
+ if (state.AudioStream == null)
+ {
+ return string.Empty;
+ }
- if (EncodingHelper.IsCopyCodec(codec))
+ var audioCodec = _encodingHelper.GetAudioEncoder(state);
+
+ if (!state.IsOutputVideo)
+ {
+ if (EncodingHelper.IsCopyCodec(audioCodec))
+ {
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+
+ return "-acodec copy -strict -2" + bitStreamArgs;
+ }
+
+ var audioTranscodeParams = string.Empty;
+
+ audioTranscodeParams += "-acodec " + audioCodec;
+
+ if (state.OutputAudioBitrate.HasValue)
+ {
+ audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ if (state.OutputAudioChannels.HasValue)
+ {
+ audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ if (state.OutputAudioSampleRate.HasValue)
+ {
+ audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ audioTranscodeParams += " -vn";
+ return audioTranscodeParams;
+ }
+
+ if (EncodingHelper.IsCopyCodec(audioCodec))
{
- return "-codec:a:0 copy";
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+
+ return "-acodec copy -strict -2" + bitStreamArgs;
}
- var args = "-codec:a:0 " + codec;
+ var args = "-codec:a:0 " + audioCodec;
var channels = state.OutputAudioChannels;
@@ -429,7 +497,7 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
+ args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
return args;
}
@@ -441,6 +509,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>The command line arguments for video transcoding.</returns>
private string GetVideoArguments(StreamState state)
{
+ if (state.VideoStream == null)
+ {
+ return string.Empty;
+ }
+
if (!state.IsOutputVideo)
{
return string.Empty;
@@ -450,47 +523,65 @@ namespace Jellyfin.Api.Controllers
var args = "-codec:v:0 " + codec;
+ // Prefer hvc1 to hev1.
+ if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
+ {
+ args += " -tag:v:0 hvc1";
+ }
+
// if (state.EnableMpegtsM2TsMode)
// {
// args += " -mpegts_m2ts_mode 1";
// }
- // See if we can save come cpu cycles by avoiding encoding
- if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
+ // See if we can save come cpu cycles by avoiding encoding.
+ if (EncodingHelper.IsCopyCodec(codec))
{
- // if h264_mp4toannexb is ever added, do not use it for live tv
- if (state.VideoStream != null &&
- !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+ // If h264_mp4toannexb is ever added, do not use it for live tv.
+ if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
- string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
+ string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
if (!string.IsNullOrEmpty(bitStreamArgs))
{
args += " " + bitStreamArgs;
}
}
+
+ args += " -start_at_zero";
}
else
{
- var keyFrameArg = string.Format(
- CultureInfo.InvariantCulture,
- " -force_key_frames \"expr:gte(t,n_forced*{0})\"",
- state.SegmentLength.ToString(CultureInfo.InvariantCulture));
+ args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
- var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ // Set the key frame params for video encoding to match the hls segment time.
+ args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, true, null);
- args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset) + keyFrameArg;
-
- // Add resolution params, if specified
- if (!hasGraphicalSubs)
+ // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
+ if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{
- args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
+ args += " -bf 0";
}
- // This is for internal graphical subs
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+
if (hasGraphicalSubs)
{
+ // Graphical subs overlay and resolution params.
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
}
+ else
+ {
+ // Resolution params.
+ args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
+ }
+
+ if (state.SubtitleStream == null || !state.SubtitleStream.IsExternal || state.SubtitleStream.IsTextSubtitleStream)
+ {
+ args += " -start_at_zero";
+ }
}
args += " -flags -global_header";
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 4de7aac71..44dc63952 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -10,6 +10,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
@@ -195,7 +196,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Merges videos into a single record.
/// </summary>
- /// <param name="itemIds">Item id list. This allows multiple, comma delimited.</param>
+ /// <param name="ids">Item id list. This allows multiple, comma delimited.</param>
/// <response code="204">Videos merged.</response>
/// <response code="400">Supply at least 2 video ids.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns>
@@ -203,9 +204,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task<ActionResult> MergeVersions([FromQuery, Required] string itemIds)
+ public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
- var items = RequestHelpers.Split(itemIds, ',', true)
+ var items = ids
.Select(i => _libraryManager.GetItemById(i))
.OfType<Video>()
.OrderBy(i => i.Id)
@@ -283,7 +284,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
- /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -312,29 +313,27 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
- /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
- /// <param name="transcodingReasons">Optional. The transcoding reason.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
- [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")]
[HttpGet("{itemId}/stream")]
- [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")]
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesVideoFile]
public async Task<ActionResult> GetVideoStream(
[FromRoute, Required] Guid itemId,
- [FromRoute] string? container,
+ [FromQuery] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
@@ -377,7 +376,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
- [FromQuery] string? transcodingReasons,
+ [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
@@ -431,7 +430,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
- TranscodeReasons = transcodingReasons,
+ TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
@@ -529,5 +528,166 @@ namespace Jellyfin.Api.Controllers
_transcodingJobType,
cancellationTokenSource).ConfigureAwait(false);
}
+
+ /// <summary>
+ /// Gets a video stream.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
+ /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
+ /// <param name="params">The streaming parameters.</param>
+ /// <param name="tag">The tag.</param>
+ /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
+ /// <param name="playSessionId">The play session id.</param>
+ /// <param name="segmentContainer">The segment container.</param>
+ /// <param name="segmentLength">The segment lenght.</param>
+ /// <param name="minSegments">The minimum number of segments.</param>
+ /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
+ /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
+ /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
+ /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
+ /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
+ /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
+ /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
+ /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
+ /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
+ /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
+ /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
+ /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
+ /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
+ /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
+ /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
+ /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
+ /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
+ /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
+ /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
+ /// <param name="maxRefFrames">Optional.</param>
+ /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
+ /// <param name="requireAvc">Optional. Whether to require avc.</param>
+ /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
+ /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
+ /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
+ /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
+ /// <param name="liveStreamId">The live stream id.</param>
+ /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
+ /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
+ /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
+ /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
+ /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
+ /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
+ /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
+ /// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <response code="200">Video stream returned.</response>
+ /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
+ [HttpGet("{itemId}/{stream=stream}.{container}")]
+ [HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesVideoFile]
+ public Task<ActionResult> GetVideoStreamByContainer(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string container,
+ [FromQuery] bool? @static,
+ [FromQuery] string? @params,
+ [FromQuery] string? tag,
+ [FromQuery] string? deviceProfileId,
+ [FromQuery] string? playSessionId,
+ [FromQuery] string? segmentContainer,
+ [FromQuery] int? segmentLength,
+ [FromQuery] int? minSegments,
+ [FromQuery] string? mediaSourceId,
+ [FromQuery] string? deviceId,
+ [FromQuery] string? audioCodec,
+ [FromQuery] bool? enableAutoStreamCopy,
+ [FromQuery] bool? allowVideoStreamCopy,
+ [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery] bool? breakOnNonKeyFrames,
+ [FromQuery] int? audioSampleRate,
+ [FromQuery] int? maxAudioBitDepth,
+ [FromQuery] int? audioBitRate,
+ [FromQuery] int? audioChannels,
+ [FromQuery] int? maxAudioChannels,
+ [FromQuery] string? profile,
+ [FromQuery] string? level,
+ [FromQuery] float? framerate,
+ [FromQuery] float? maxFramerate,
+ [FromQuery] bool? copyTimestamps,
+ [FromQuery] long? startTimeTicks,
+ [FromQuery] int? width,
+ [FromQuery] int? height,
+ [FromQuery] int? videoBitRate,
+ [FromQuery] int? subtitleStreamIndex,
+ [FromQuery] SubtitleDeliveryMethod subtitleMethod,
+ [FromQuery] int? maxRefFrames,
+ [FromQuery] int? maxVideoBitDepth,
+ [FromQuery] bool? requireAvc,
+ [FromQuery] bool? deInterlace,
+ [FromQuery] bool? requireNonAnamorphic,
+ [FromQuery] int? transcodingMaxAudioChannels,
+ [FromQuery] int? cpuCoreLimit,
+ [FromQuery] string? liveStreamId,
+ [FromQuery] bool? enableMpegtsM2TsMode,
+ [FromQuery] string? videoCodec,
+ [FromQuery] string? subtitleCodec,
+ [FromQuery] string? transcodeReasons,
+ [FromQuery] int? audioStreamIndex,
+ [FromQuery] int? videoStreamIndex,
+ [FromQuery] EncodingContext context,
+ [FromQuery] Dictionary<string, string> streamOptions)
+ {
+ return GetVideoStream(
+ itemId,
+ container,
+ @static,
+ @params,
+ tag,
+ deviceProfileId,
+ playSessionId,
+ segmentContainer,
+ segmentLength,
+ minSegments,
+ mediaSourceId,
+ deviceId,
+ audioCodec,
+ enableAutoStreamCopy,
+ allowVideoStreamCopy,
+ allowAudioStreamCopy,
+ breakOnNonKeyFrames,
+ audioSampleRate,
+ maxAudioBitDepth,
+ audioBitRate,
+ audioChannels,
+ maxAudioChannels,
+ profile,
+ level,
+ framerate,
+ maxFramerate,
+ copyTimestamps,
+ startTimeTicks,
+ width,
+ height,
+ videoBitRate,
+ subtitleStreamIndex,
+ subtitleMethod,
+ maxRefFrames,
+ maxVideoBitDepth,
+ requireAvc,
+ deInterlace,
+ requireNonAnamorphic,
+ transcodingMaxAudioChannels,
+ cpuCoreLimit,
+ liveStreamId,
+ enableMpegtsM2TsMode,
+ videoCodec,
+ subtitleCodec,
+ transcodeReasons,
+ audioStreamIndex,
+ videoStreamIndex,
+ context,
+ streamOptions);
+ }
}
}
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index 1b38e399d..48c639b08 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@@ -71,11 +71,11 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? sortOrder,
- [FromQuery] string? parentId,
+ [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? excludeItemTypes,
- [FromQuery] string? includeItemTypes,
- [FromQuery] string? mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
@@ -89,33 +89,24 @@ namespace Jellyfin.Api.Controllers
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
- BaseItem parentItem;
+ BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
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);
}
IList<BaseItem> items;
- 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,
+ ExcludeItemTypes = excludeItemTypes,
+ IncludeItemTypes = includeItemTypes,
+ MediaTypes = mediaTypes,
DtoOptions = dtoOptions
};
- bool Filter(BaseItem i) => FilterItem(i, excludeItemTypesArr, includeItemTypesArr, mediaTypesArr);
+ bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
if (parentItem.IsFolder)
{