aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api
diff options
context:
space:
mode:
authorMarc Brooks <IDisposable@gmail.com>2025-02-03 19:48:59 -0600
committerGitHub <noreply@github.com>2025-02-03 19:48:59 -0600
commite8cbcde02ebd930a5eeb6c95e0875a9e30acb3e8 (patch)
tree2ecd43f232012c8f037f4cd6fee4168e46d01aa3 /Jellyfin.Api
parent6dc61a430ba3a8480399309f277e5debfd6403ba (diff)
parentd376b5fbc7cf3ae7440a606a9e885d70605956bd (diff)
Merge branch 'master' into sort-nfo-data
Diffstat (limited to 'Jellyfin.Api')
-rw-r--r--Jellyfin.Api/Auth/CustomAuthenticationHandler.cs2
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs10
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs78
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs16
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs7
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs8
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs14
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs3
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs4
-rw-r--r--Jellyfin.Api/Controllers/MediaSegmentsController.cs4
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs3
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs56
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs10
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs85
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs1
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs2
-rw-r--r--Jellyfin.Api/Controllers/TrickplayController.cs2
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs2
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs34
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs7
-rw-r--r--Jellyfin.Api/Formatters/CssOutputFormatter.cs24
-rw-r--r--Jellyfin.Api/Formatters/XmlOutputFormatter.cs15
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs12
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs9
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs17
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs5
-rw-r--r--Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs2
-rw-r--r--Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs2
-rw-r--r--Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs83
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs2
33 files changed, 212 insertions, 313 deletions
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index 2853e69b01..c2398f71b2 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -57,7 +57,7 @@ namespace Jellyfin.Api.Auth
var claims = new[]
{
- new Claim(ClaimTypes.Name, authorizationInfo.User?.Username ?? string.Empty),
+ new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
new Claim(ClaimTypes.Role, role),
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index a47c604737..272b4034ed 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -238,7 +238,7 @@ public class AudioController : BaseJellyfinApiController
/// <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>
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 2a2ab4ad16..50050262f0 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -1,15 +1,13 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Dtos;
-using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Queries;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -47,7 +45,7 @@ public class DevicesController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] Guid? userId)
+ public ActionResult<QueryResult<DeviceInfoDto>> GetDevices([FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
return _deviceManager.GetDevicesForUser(userId);
@@ -63,7 +61,7 @@ public class DevicesController : BaseJellyfinApiController
[HttpGet("Info")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
+ public ActionResult<DeviceInfoDto> GetDeviceInfo([FromQuery, Required] string id)
{
var deviceInfo = _deviceManager.GetDevice(id);
if (deviceInfo is null)
@@ -84,7 +82,7 @@ public class DevicesController : BaseJellyfinApiController
[HttpGet("Options")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
+ public ActionResult<DeviceOptionsDto> GetDeviceOptions([FromQuery, Required] string id)
{
var deviceInfo = _deviceManager.GetDeviceOptions(id);
if (deviceInfo is null)
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index db1d866985..ca8ab0ef75 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -158,6 +158,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="maxHeight">Optional. The max height.</param>
/// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param>
/// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
+ /// <param name="alwaysBurnInSubtitleWhenTranscoding">Whether to always burn in subtitles when transcoding.</param>
/// <response code="200">Hls live stream retrieved.</response>
/// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
[HttpGet("Videos/{itemId}/live.m3u8")]
@@ -216,7 +217,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? enableSubtitlesInManifest,
- [FromQuery] bool enableAudioVbrEncoding = true)
+ [FromQuery] bool enableAudioVbrEncoding = true,
+ [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false)
{
VideoRequestDto streamingRequest = new VideoRequestDto
{
@@ -251,7 +253,7 @@ public class DynamicHlsController : BaseJellyfinApiController
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -271,7 +273,8 @@ public class DynamicHlsController : BaseJellyfinApiController
MaxHeight = maxHeight,
MaxWidth = maxWidth,
EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding
};
// CTS lifecycle is managed internally.
@@ -398,6 +401,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
/// <param name="enableTrickplay">Enable trickplay image playlists being added to master playlist.</param>
/// <param name="enableAudioVbrEncoding">Whether to enable Audio Encoding.</param>
+ /// <param name="alwaysBurnInSubtitleWhenTranscoding">Whether to always burn in subtitles when transcoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
[HttpGet("Videos/{itemId}/master.m3u8")]
@@ -455,9 +459,10 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true,
+ [FromQuery] bool enableAdaptiveBitrateStreaming = false,
[FromQuery] bool enableTrickplay = true,
- [FromQuery] bool enableAudioVbrEncoding = true)
+ [FromQuery] bool enableAudioVbrEncoding = true,
+ [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false)
{
var streamingRequest = new HlsVideoRequestDto
{
@@ -493,7 +498,7 @@ public class DynamicHlsController : BaseJellyfinApiController
MaxHeight = maxHeight,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -512,7 +517,8 @@ public class DynamicHlsController : BaseJellyfinApiController
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
EnableTrickplay = enableTrickplay,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -628,7 +634,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true,
+ [FromQuery] bool enableAdaptiveBitrateStreaming = false,
[FromQuery] bool enableAudioVbrEncoding = true)
{
var streamingRequest = new HlsAudioRequestDto
@@ -663,7 +669,7 @@ public class DynamicHlsController : BaseJellyfinApiController
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -681,7 +687,8 @@ public class DynamicHlsController : BaseJellyfinApiController
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = false
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -741,6 +748,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
+ /// <param name="alwaysBurnInSubtitleWhenTranscoding">Whether to always burn in subtitles when transcoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Videos/{itemId}/main.m3u8")]
@@ -797,7 +805,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAudioVbrEncoding = true)
+ [FromQuery] bool enableAudioVbrEncoding = true,
+ [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false)
{
using var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
@@ -834,7 +843,7 @@ public class DynamicHlsController : BaseJellyfinApiController
MaxHeight = maxHeight,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -851,7 +860,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding
};
return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
@@ -1001,7 +1011,7 @@ public class DynamicHlsController : BaseJellyfinApiController
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -1018,7 +1028,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = false
};
return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
@@ -1084,6 +1095,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
+ /// <param name="alwaysBurnInSubtitleWhenTranscoding">Whether to always burn in subtitles when transcoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
@@ -1146,7 +1158,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAudioVbrEncoding = true)
+ [FromQuery] bool enableAudioVbrEncoding = true,
+ [FromQuery] bool alwaysBurnInSubtitleWhenTranscoding = false)
{
var streamingRequest = new VideoRequestDto
{
@@ -1185,7 +1198,7 @@ public class DynamicHlsController : BaseJellyfinApiController
MaxHeight = maxHeight,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -1202,7 +1215,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = alwaysBurnInSubtitleWhenTranscoding
};
return await GetDynamicSegment(streamingRequest, segmentId)
@@ -1365,7 +1379,7 @@ public class DynamicHlsController : BaseJellyfinApiController
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
- SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
+ SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.External,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? false,
@@ -1382,7 +1396,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAudioVbrEncoding = enableAudioVbrEncoding
+ EnableAudioVbrEncoding = enableAudioVbrEncoding,
+ AlwaysBurnInSubtitleWhenTranscoding = false
};
return await GetDynamicSegment(streamingRequest, segmentId)
@@ -1763,7 +1778,7 @@ public class DynamicHlsController : BaseJellyfinApiController
}
else if (state.AudioStream?.CodecTag is not null && state.AudioStream.CodecTag.Equals("ac-4", StringComparison.Ordinal))
{
- // ac-4 audio tends to hava a super weird sample rate that will fail most encoders
+ // ac-4 audio tends to have a super weird sample rate that will fail most encoders
// force resample it to 48KHz
args += " -ar 48000";
}
@@ -1804,16 +1819,13 @@ public class DynamicHlsController : BaseJellyfinApiController
if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1)
{
var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
- var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
- var requestHasDOVIWithHDR10 = requestedRange.Contains(VideoRangeType.DOVIWithHDR10.ToString(), StringComparison.OrdinalIgnoreCase);
- var requestHasDOVIWithHLG = requestedRange.Contains(VideoRangeType.DOVIWithHLG.ToString(), StringComparison.OrdinalIgnoreCase);
- var requestHasDOVIWithSDR = requestedRange.Contains(VideoRangeType.DOVIWithSDR.ToString(), StringComparison.OrdinalIgnoreCase);
+ // Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer.
+ // Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks.
+ var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
+ var videoIsDoVi = state.VideoStream.VideoRangeType is VideoRangeType.DOVI or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG or VideoRangeType.DOVIWithSDR;
if (EncodingHelper.IsCopyCodec(codec)
- && ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI && requestHasDOVI)
- || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 && requestHasDOVIWithHDR10)
- || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG)
- || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR)))
+ && (videoIsDoVi && clientSupportsDoVi))
{
if (isActualOutputVideoCodecHevc)
{
@@ -1893,7 +1905,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (!string.IsNullOrEmpty(state.OutputVideoSync))
{
- args += " -vsync " + state.OutputVideoSync;
+ args += EncodingHelper.GetVideoSyncOption(state.OutputVideoSync, _mediaEncoder.EncoderVersion);
}
args += _encodingHelper.GetOutputFFlags(state);
@@ -2044,16 +2056,16 @@ public class DynamicHlsController : BaseJellyfinApiController
}
}
- private Task DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
+ private async Task DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
{
var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
if (file is null)
{
- return Task.CompletedTask;
+ return;
}
- return DeleteFile(file.FullName, retryCount);
+ await DeleteFile(file.FullName, retryCount).ConfigureAwait(false);
}
private async Task DeleteFile(string path, int retryCount)
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index dcbacf1d78..87a856d38e 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
+using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -389,23 +391,19 @@ public class InstantMixController : BaseJellyfinApiController
return GetResult(items, user, limit, dtoOptions);
}
- private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
+ private QueryResult<BaseItemDto> GetResult(IReadOnlyList<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
{
- var list = items;
+ var totalCount = items.Count;
- var totalCount = list.Count;
-
- if (limit.HasValue && limit < list.Count)
+ if (limit.HasValue && limit < items.Count)
{
- list = list.GetRange(0, limit.Value);
+ items = items.Take(limit.Value).ToArray();
}
- var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
-
var result = new QueryResult<BaseItemDto>(
0,
totalCount,
- returnList);
+ _dtoService.GetBaseItemDtos(items, dtoOptions, user));
return result;
}
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index d7a8c37c4b..7effe61e49 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -50,6 +50,7 @@ public class ItemRefreshController : BaseJellyfinApiController
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
+ /// <param name="regenerateTrickplay">(Optional) Determines if trickplay images should be replaced. Only applicable if mode is FullRefresh.</param>
/// <response code="204">Item metadata refresh queued.</response>
/// <response code="404">Item to refresh not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
@@ -62,7 +63,8 @@ public class ItemRefreshController : BaseJellyfinApiController
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
[FromQuery] bool replaceAllMetadata = false,
- [FromQuery] bool replaceAllImages = false)
+ [FromQuery] bool replaceAllImages = false,
+ [FromQuery] bool regenerateTrickplay = false)
{
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
@@ -81,7 +83,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|| replaceAllImages
|| replaceAllMetadata,
IsAutomated = false,
- RemoveOldMetadata = replaceAllMetadata
+ RemoveOldMetadata = replaceAllMetadata,
+ RegenerateTrickplay = regenerateTrickplay
};
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 4001a6addb..d49e0753ee 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -457,7 +457,7 @@ public class ItemUpdateController : BaseJellyfinApiController
return null;
}
- return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
+ return Enum.Parse<SeriesStatus>(item.Status, true);
}
private DateTime NormalizeDateTime(DateTime val)
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 828bd51740..775d723b0b 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -967,7 +967,7 @@ public class ItemsController : BaseJellyfinApiController
[HttpGet("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<UserItemDataDto> GetItemUserData(
+ public ActionResult<UserItemDataDto?> GetItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
@@ -1005,7 +1005,7 @@ public class ItemsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public ActionResult<UserItemDataDto> GetItemUserDataLegacy(
+ public ActionResult<UserItemDataDto?> GetItemUserDataLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> GetItemUserData(userId, itemId);
@@ -1022,7 +1022,7 @@ public class ItemsController : BaseJellyfinApiController
[HttpPost("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult<UserItemDataDto> UpdateItemUserData(
+ public ActionResult<UserItemDataDto?> UpdateItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[FromBody, Required] UpdateUserItemDataDto userDataDto)
@@ -1064,7 +1064,7 @@ public class ItemsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public ActionResult<UserItemDataDto> UpdateItemUserDataLegacy(
+ public ActionResult<UserItemDataDto?> UpdateItemUserDataLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromBody, Required] UpdateUserItemDataDto userDataDto)
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index afc93c3a8d..0b2d4b0325 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -780,11 +780,9 @@ public class LibraryController : BaseJellyfinApiController
Genres = item.Genres,
Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(),
- SimilarTo = item,
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true,
EnableGroupByMetadataKey = isMovie ?? false,
- MinSimilarityScore = 2 // A remnant from album/artist scoring
};
// ExcludeArtistIds
@@ -793,7 +791,7 @@ public class LibraryController : BaseJellyfinApiController
query.ExcludeArtistIds = excludeArtistIds;
}
- List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
+ var itemsResult = _libraryManager.GetItemList(query);
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
@@ -867,6 +865,16 @@ public class LibraryController : BaseJellyfinApiController
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
+ result.MediaSegmentProviders = plugins
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.MediaSegmentProvider))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = true
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
var typeOptions = new List<LibraryTypeOptionsDto>();
foreach (var type in types)
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 93c2393f33..55000fc91e 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -99,6 +99,7 @@ public class LibraryStructureController : BaseJellyfinApiController
/// <param name="name">The name of the folder.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <response code="204">Folder removed.</response>
+ /// <response code="404">Folder not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
@@ -106,7 +107,9 @@ public class LibraryStructureController : BaseJellyfinApiController
[FromQuery] string name,
[FromQuery] bool refreshLibrary = false)
{
+ // TODO: refactor! this relies on an FileNotFound exception to return NotFound when attempting to remove a library that does not exist.
await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
+
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 0ae8baa671..421f23fa1e 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -962,9 +962,9 @@ public class LiveTvController : BaseJellyfinApiController
}
/// <summary>
- /// Get guid info.
+ /// Get guide info.
/// </summary>
- /// <response code="200">Guid info returned.</response>
+ /// <response code="200">Guide info returned.</response>
/// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
[HttpGet("GuideInfo")]
[Authorize(Policy = Policies.LiveTvAccess)]
diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
index e97704d48d..2d1d4e2c8a 100644
--- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs
+++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
@@ -45,7 +45,7 @@ public class MediaSegmentsController : BaseJellyfinApiController
[HttpGet("{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<QueryResult<MediaSegmentDto>>> GetSegmentsAsync(
+ public async Task<ActionResult<QueryResult<MediaSegmentDto>>> GetItemSegments(
[FromRoute, Required] Guid itemId,
[FromQuery] IEnumerable<MediaSegmentType>? includeSegmentTypes = null)
{
@@ -55,7 +55,7 @@ public class MediaSegmentsController : BaseJellyfinApiController
return NotFound();
}
- var items = await _mediaSegmentManager.GetSegmentsAsync(item.Id, includeSegmentTypes).ConfigureAwait(false);
+ var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes).ConfigureAwait(false);
return Ok(new QueryResult<MediaSegmentDto>(items.ToArray()));
}
}
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 471bcd096e..2d917d61fb 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -120,7 +120,7 @@ public class MoviesController : BaseJellyfinApiController
DtoOptions = dtoOptions
});
- var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6));
+ var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)).ToList();
// Get recently played directors
var recentDirectors = GetDirectors(mostRecentMovies)
.ToList();
@@ -276,7 +276,6 @@ public class MoviesController : BaseJellyfinApiController
Limit = itemLimit,
IncludeItemTypes = itemTypes.ToArray(),
IsMovie = true,
- SimilarTo = item,
EnableGroupByMetadataKey = true,
DtoOptions = dtoOptions
});
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 63d6e1cc30..1ab36ccc64 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -150,6 +151,37 @@ public class PlaylistsController : BaseJellyfinApiController
}
/// <summary>
+ /// Get a playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist id.</param>
+ /// <response code="200">The playlist.</response>
+ /// <response code="404">Playlist not found.</response>
+ /// <returns>
+ /// A <see cref="Playlist"/> objects.
+ /// </returns>
+ [HttpGet("{playlistId}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<PlaylistDto> GetPlaylist(
+ [FromRoute, Required] Guid playlistId)
+ {
+ var userId = User.GetUserId();
+
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ return new PlaylistDto()
+ {
+ Shares = playlist.Shares,
+ OpenAccess = playlist.OpenAccess,
+ ItemIds = playlist.GetManageableItems().Select(t => t.Item2.Id).ToList()
+ };
+ }
+
+ /// <summary>
/// Get a playlist's users.
/// </summary>
/// <param name="playlistId">The playlist id.</param>
@@ -395,7 +427,7 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
- await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
+ await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex, callingUserId).ConfigureAwait(false);
return NoContent();
}
@@ -467,32 +499,24 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- userId = RequestHelpers.GetUserId(User, userId);
- var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId.Value);
+ var callingUserId = userId ?? User.GetUserId();
+ var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId);
if (playlist is null)
{
return NotFound("Playlist not found");
}
var isPermitted = playlist.OpenAccess
- || playlist.OwnerUserId.Equals(userId.Value)
- || playlist.Shares.Any(s => s.UserId.Equals(userId.Value));
+ || playlist.OwnerUserId.Equals(callingUserId)
+ || playlist.Shares.Any(s => s.UserId.Equals(callingUserId));
if (!isPermitted)
{
return Forbid();
}
- var user = userId.IsNullOrEmpty()
- ? null
- : _userManager.GetUserById(userId.Value);
- var item = _libraryManager.GetItemById<Playlist>(playlistId, user);
- if (item is null)
- {
- return NotFound();
- }
-
- var items = item.GetManageableItems().ToArray();
+ var user = _userManager.GetUserById(callingUserId);
+ var items = playlist.GetManageableItems().Where(i => i.Item2.IsVisible(user)).ToArray();
var count = items.Length;
if (startIndex.HasValue)
{
@@ -511,7 +535,7 @@ public class PlaylistsController : BaseJellyfinApiController
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
for (int index = 0; index < dtos.Count; index++)
{
- dtos[index].PlaylistItemId = items[index].Item1.Id;
+ dtos[index].PlaylistItemId = items[index].Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture);
}
var result = new QueryResult<BaseItemDto>(
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 88aa0178f9..794c6500c6 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -72,7 +72,7 @@ public class PlaystateController : BaseJellyfinApiController
[HttpPost("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
+ public async Task<ActionResult<UserItemDataDto?>> MarkPlayedItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
@@ -121,7 +121,7 @@ public class PlaystateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public Task<ActionResult<UserItemDataDto>> MarkPlayedItemLegacy(
+ public Task<ActionResult<UserItemDataDto?>> MarkPlayedItemLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
@@ -138,7 +138,7 @@ public class PlaystateController : BaseJellyfinApiController
[HttpDelete("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem(
+ public async Task<ActionResult<UserItemDataDto?>> MarkUnplayedItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
@@ -185,7 +185,7 @@ public class PlaystateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public Task<ActionResult<UserItemDataDto>> MarkUnplayedItemLegacy(
+ public Task<ActionResult<UserItemDataDto?>> MarkUnplayedItemLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> MarkUnplayedItem(userId, itemId);
@@ -502,7 +502,7 @@ public class PlaystateController : BaseJellyfinApiController
/// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
/// <param name="datePlayed">The date played.</param>
/// <returns>Task.</returns>
- private UserItemDataDto UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed)
+ private UserItemDataDto? UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed)
{
if (wasPlayed)
{
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 942bdeb9e8..2f9e9f091d 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -1,18 +1,13 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
-using Jellyfin.Api.Models.SessionDtos;
using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
-using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
@@ -32,22 +27,18 @@ public class SessionController : BaseJellyfinApiController
{
private readonly ISessionManager _sessionManager;
private readonly IUserManager _userManager;
- private readonly IDeviceManager _deviceManager;
/// <summary>
/// Initializes a new instance of the <see cref="SessionController"/> class.
/// </summary>
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
/// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param>
- /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
public SessionController(
ISessionManager sessionManager,
- IUserManager userManager,
- IDeviceManager deviceManager)
+ IUserManager userManager)
{
_sessionManager = sessionManager;
_userManager = userManager;
- _deviceManager = deviceManager;
}
/// <summary>
@@ -57,78 +48,22 @@ public class SessionController : BaseJellyfinApiController
/// <param name="deviceId">Filter by device Id.</param>
/// <param name="activeWithinSeconds">Optional. Filter by sessions that were active in the last n seconds.</param>
/// <response code="200">List of sessions returned.</response>
- /// <returns>An <see cref="IEnumerable{SessionInfo}"/> with the available sessions.</returns>
+ /// <returns>An <see cref="IReadOnlyList{SessionInfoDto}"/> with the available sessions.</returns>
[HttpGet("Sessions")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<IEnumerable<SessionInfo>> GetSessions(
+ public ActionResult<IReadOnlyList<SessionInfoDto>> GetSessions(
[FromQuery] Guid? controllableByUserId,
[FromQuery] string? deviceId,
[FromQuery] int? activeWithinSeconds)
{
- var result = _sessionManager.Sessions;
- var isRequestingFromAdmin = User.IsInRole(UserRoles.Administrator);
-
- if (!string.IsNullOrEmpty(deviceId))
- {
- result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
- }
-
- if (!controllableByUserId.IsNullOrEmpty())
- {
- result = result.Where(i => i.SupportsRemoteControl);
-
- var user = _userManager.GetUserById(controllableByUserId.Value);
- if (user is null)
- {
- return NotFound();
- }
-
- if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
- {
- // User cannot control other user's sessions, validate user id.
- result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(RequestHelpers.GetUserId(User, controllableByUserId)));
- }
-
- if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
- {
- result = result.Where(i => !i.UserId.IsEmpty());
- }
-
- result = result.Where(i =>
- {
- if (!string.IsNullOrWhiteSpace(i.DeviceId))
- {
- if (!_deviceManager.CanAccessDevice(user, i.DeviceId))
- {
- return false;
- }
- }
-
- return true;
- });
- }
- else if (!isRequestingFromAdmin)
- {
- // Request isn't from administrator, limit to "own" sessions.
- result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(User.GetUserId()));
- }
-
- if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
- {
- var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value);
- result = result.Where(i => i.LastActivityDate >= minActiveDate);
- }
-
- // Request isn't from administrator, don't report acceleration type.
- if (!isRequestingFromAdmin)
- {
- result = result.Select(r =>
- {
- r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
- return r;
- });
- }
+ Guid? controllableUserToCheck = controllableByUserId is null ? null : RequestHelpers.GetUserId(User, controllableByUserId);
+ var result = _sessionManager.GetSessions(
+ User.GetUserId(),
+ deviceId,
+ activeWithinSeconds,
+ controllableUserToCheck,
+ User.GetIsApiKey());
return Ok(result);
}
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 41b0858d19..a6bc84311f 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -93,7 +93,6 @@ public class StartupController : BaseJellyfinApiController
{
NetworkConfiguration settings = _config.GetNetworkConfiguration();
settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
- settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
_config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 9da1dce93e..e5df873f5b 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -395,7 +395,7 @@ public class SubtitleController : BaseJellyfinApiController
var url = string.Format(
CultureInfo.InvariantCulture,
- "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
+ "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&ApiKey={2}",
positionTicks.ToString(CultureInfo.InvariantCulture),
endPositionTicks.ToString(CultureInfo.InvariantCulture),
accessToken);
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
index c1ff0f3401..2cf66144ce 100644
--- a/Jellyfin.Api/Controllers/TrickplayController.cs
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -80,7 +80,7 @@ public class TrickplayController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
- public async Task<ActionResult> GetTrickplayTileImageAsync(
+ public async Task<ActionResult> GetTrickplayTileImage(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int width,
[FromRoute, Required] int index,
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 41c4886d4f..4fe2d52daf 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -222,7 +222,7 @@ public class UniversalAudioController : BaseJellyfinApiController
TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
Context = EncodingContext.Static,
StreamOptions = new Dictionary<string, string>(),
- EnableAdaptiveBitrateStreaming = true,
+ EnableAdaptiveBitrateStreaming = false,
EnableAudioVbrEncoding = enableAudioVbrEncoding
};
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index e7bf717274..7cce13e424 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -305,7 +305,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> DeleteUserItemRating(
+ public ActionResult<UserItemDataDto?> DeleteUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
@@ -338,7 +338,7 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public ActionResult<UserItemDataDto> DeleteUserItemRatingLegacy(
+ public ActionResult<UserItemDataDto?> DeleteUserItemRatingLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId)
=> DeleteUserItemRating(userId, itemId);
@@ -353,7 +353,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<UserItemDataDto> UpdateUserItemRating(
+ public ActionResult<UserItemDataDto?> UpdateUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
[FromQuery] bool? likes)
@@ -388,7 +388,7 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Kept for backwards compatibility")]
[ApiExplorerSettings(IgnoreApi = true)]
- public ActionResult<UserItemDataDto> UpdateUserItemRatingLegacy(
+ public ActionResult<UserItemDataDto?> UpdateUserItemRatingLegacy(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
[FromQuery] bool? likes)
@@ -634,10 +634,10 @@ public class UserLibraryController : BaseJellyfinApiController
{
if (item is Person)
{
- var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
- var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
+ var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
+ var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
- if (!hasMetdata)
+ if (!hasMetadata)
{
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
@@ -662,12 +662,15 @@ public class UserLibraryController : BaseJellyfinApiController
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
- // Set favorite status
- data.IsFavorite = isFavorite;
+ if (data is not null)
+ {
+ // Set favorite status
+ data.IsFavorite = isFavorite;
- _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ }
- return _userDataRepository.GetUserDataDto(item, user);
+ return _userDataRepository.GetUserDataDto(item, user)!;
}
/// <summary>
@@ -676,14 +679,17 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="user">The user.</param>
/// <param name="item">The item.</param>
/// <param name="likes">if set to <c>true</c> [likes].</param>
- private UserItemDataDto UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
+ private UserItemDataDto? UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
{
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
- data.Likes = likes;
+ if (data is not null)
+ {
+ data.Likes = likes;
- _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+ }
return _userDataRepository.GetUserDataDto(item, user);
}
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index e4aa0ea42d..e709e43e26 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Extensions;
@@ -105,18 +106,18 @@ public class YearsController : BaseJellyfinApiController
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
- IList<BaseItem> items;
+ IReadOnlyList<BaseItem> items;
if (parentItem.IsFolder)
{
var folder = (Folder)parentItem;
if (userId.IsNullOrEmpty())
{
- items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
+ items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToArray();
}
else
{
- items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList();
+ items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToArray();
}
}
else
diff --git a/Jellyfin.Api/Formatters/CssOutputFormatter.cs b/Jellyfin.Api/Formatters/CssOutputFormatter.cs
index 495f771e1f..9ad1c863ea 100644
--- a/Jellyfin.Api/Formatters/CssOutputFormatter.cs
+++ b/Jellyfin.Api/Formatters/CssOutputFormatter.cs
@@ -1,6 +1,4 @@
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
+using System.Net.Mime;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace Jellyfin.Api.Formatters;
@@ -8,28 +6,14 @@ namespace Jellyfin.Api.Formatters;
/// <summary>
/// Css output formatter.
/// </summary>
-public class CssOutputFormatter : TextOutputFormatter
+public sealed class CssOutputFormatter : StringOutputFormatter
{
/// <summary>
/// Initializes a new instance of the <see cref="CssOutputFormatter"/> class.
/// </summary>
public CssOutputFormatter()
{
- SupportedMediaTypes.Add("text/css");
-
- SupportedEncodings.Add(Encoding.UTF8);
- SupportedEncodings.Add(Encoding.Unicode);
- }
-
- /// <summary>
- /// Write context object to stream.
- /// </summary>
- /// <param name="context">Writer context.</param>
- /// <param name="selectedEncoding">Unused. Writer encoding.</param>
- /// <returns>Write stream task.</returns>
- public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
- {
- var stringResponse = context.Object?.ToString();
- return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
+ SupportedMediaTypes.Clear();
+ SupportedMediaTypes.Add(MediaTypeNames.Text.Css);
}
}
diff --git a/Jellyfin.Api/Formatters/XmlOutputFormatter.cs b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs
index 1c9feedcb7..8dbb91d0aa 100644
--- a/Jellyfin.Api/Formatters/XmlOutputFormatter.cs
+++ b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs
@@ -1,7 +1,4 @@
using System.Net.Mime;
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace Jellyfin.Api.Formatters;
@@ -9,7 +6,7 @@ namespace Jellyfin.Api.Formatters;
/// <summary>
/// Xml output formatter.
/// </summary>
-public class XmlOutputFormatter : TextOutputFormatter
+public sealed class XmlOutputFormatter : StringOutputFormatter
{
/// <summary>
/// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class.
@@ -18,15 +15,5 @@ public class XmlOutputFormatter : TextOutputFormatter
{
SupportedMediaTypes.Clear();
SupportedMediaTypes.Add(MediaTypeNames.Text.Xml);
-
- SupportedEncodings.Add(Encoding.UTF8);
- SupportedEncodings.Add(Encoding.Unicode);
- }
-
- /// <inheritdoc />
- public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
- {
- var stringResponse = context.Object?.ToString();
- return stringResponse is null ? Task.CompletedTask : context.HttpContext.Response.WriteAsync(stringResponse);
}
}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 0e620e72a9..6487160303 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -267,7 +267,7 @@ public class DynamicHlsHelper
if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIP()))
{
- var requestedVideoBitrate = state.VideoRequest is null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
+ var requestedVideoBitrate = state.VideoRequest?.VideoBitRate ?? 0;
// By default, vary by just 200k
var variation = GetBitrateVariation(totalBitrate);
@@ -526,9 +526,7 @@ public class DynamicHlsHelper
return false;
}
- // Having problems in android
- return false;
- // return state.VideoRequest.VideoBitRate.HasValue;
+ return state.VideoRequest?.VideoBitRate.HasValue ?? false;
}
private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user)
@@ -550,7 +548,7 @@ public class DynamicHlsHelper
var url = string.Format(
CultureInfo.InvariantCulture,
- "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
+ "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&ApiKey={3}",
state.Request.MediaSourceId,
stream.Index.ToString(CultureInfo.InvariantCulture),
30.ToString(CultureInfo.InvariantCulture),
@@ -587,7 +585,7 @@ public class DynamicHlsHelper
var url = string.Format(
CultureInfo.InvariantCulture,
- "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&api_key={2}",
+ "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&ApiKey={2}",
width.ToString(CultureInfo.InvariantCulture),
state.Request.MediaSourceId,
user.GetToken());
@@ -616,7 +614,7 @@ public class DynamicHlsHelper
&& state.VideoStream is not null
&& state.VideoStream.Level.HasValue)
{
- levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
+ levelString = state.VideoStream.Level.Value.ToString(CultureInfo.InvariantCulture);
}
else
{
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 5050cab418..4adda0b695 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -293,6 +293,10 @@ public class MediaInfoHelper
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
mediaSource.TranscodingContainer = streamInfo.Container;
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
+ if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
+ {
+ mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
+ }
}
else
{
@@ -310,6 +314,11 @@ public class MediaInfoHelper
{
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
}
+
+ if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding)
+ {
+ mediaSource.TranscodingUrl += "&alwaysBurnInSubtitleWhenTranscoding=true";
+ }
}
}
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 3cc6a393bc..2601fa3be8 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -132,7 +132,7 @@ public static class StreamingHelpers
mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
? mediaSources[0]
- : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
+ : mediaSources.FirstOrDefault(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
if (mediaSource is null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id))
{
@@ -210,7 +210,7 @@ public static class StreamingHelpers
&& state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
{
// Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,
- // and the requested video bitrate is higher than source video bitrate.
+ // and the requested video bitrate is greater than source video bitrate.
if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)
{
state.VideoRequest.MaxWidth = state.VideoStream?.Width;
@@ -219,16 +219,27 @@ public static class StreamingHelpers
}
else
{
+ var h264EquivalentBitrate = EncodingHelper.ScaleBitrate(
+ state.OutputVideoBitrate.Value,
+ state.ActualOutputVideoCodec,
+ "h264");
var resolution = ResolutionNormalizer.Normalize(
state.VideoStream?.BitRate,
state.OutputVideoBitrate.Value,
+ h264EquivalentBitrate,
state.VideoRequest.MaxWidth,
- state.VideoRequest.MaxHeight);
+ state.VideoRequest.MaxHeight,
+ state.TargetFramerate);
state.VideoRequest.MaxWidth = resolution.MaxWidth;
state.VideoRequest.MaxHeight = resolution.MaxHeight;
}
}
+
+ if (state.AudioStream is not null && !EncodingHelper.IsCopyCodec(state.OutputAudioCodec) && string.Equals(state.AudioStream.Codec, state.OutputAudioCodec, StringComparison.OrdinalIgnoreCase) && state.OutputAudioBitrate.HasValue)
+ {
+ state.OutputAudioCodec = state.SupportedAudioCodecs.Where(c => !EncodingHelper.LosslessAudioCodecs.Contains(c)).FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec);
+ }
}
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 5f86a6b6be..25feaa2d75 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
index d07349bdf6..c492436689 100644
--- a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs
@@ -29,6 +29,11 @@ public class LibraryOptionsResultDto
public IReadOnlyList<LibraryOptionInfoDto> LyricFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
/// <summary>
+ /// Gets or sets the list of MediaSegment Providers.
+ /// </summary>
+ public IReadOnlyList<LibraryOptionInfoDto> MediaSegmentProviders { get; set; } = Array.Empty<LibraryOptionInfoDto>();
+
+ /// <summary>
/// Gets or sets the type options.
/// </summary>
public IReadOnlyList<LibraryTypeOptionsDto> TypeOptions { get; set; } = Array.Empty<LibraryTypeOptionsDto>();
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
index 978e99b35c..758c89938e 100644
--- a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
+++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
@@ -61,7 +61,7 @@ public class OpenLiveStreamDto
public bool? EnableDirectPlay { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether to enale direct stream.
+ /// Gets or sets a value indicating whether to enable direct stream.
/// </summary>
public bool? EnableDirectStream { get; set; }
diff --git a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
index 82f603ca1e..73ab76817c 100644
--- a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
+++ b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs
@@ -4,7 +4,7 @@ using MediaBrowser.Model.Dlna;
namespace Jellyfin.Api.Models.MediaInfoDtos;
/// <summary>
-/// Plabyback info dto.
+/// Playback info dto.
/// </summary>
public class PlaybackInfoDto
{
diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
deleted file mode 100644
index c699c469d9..0000000000
--- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Text.Json.Serialization;
-using Jellyfin.Data.Enums;
-using Jellyfin.Extensions.Json.Converters;
-using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Session;
-
-namespace Jellyfin.Api.Models.SessionDtos;
-
-/// <summary>
-/// Client capabilities dto.
-/// </summary>
-public class ClientCapabilitiesDto
-{
- /// <summary>
- /// Gets or sets the list of playable media types.
- /// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<MediaType> PlayableMediaTypes { get; set; } = Array.Empty<MediaType>();
-
- /// <summary>
- /// Gets or sets the list of supported commands.
- /// </summary>
- [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = Array.Empty<GeneralCommandType>();
-
- /// <summary>
- /// Gets or sets a value indicating whether session supports media control.
- /// </summary>
- public bool SupportsMediaControl { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether session supports a persistent identifier.
- /// </summary>
- public bool SupportsPersistentIdentifier { get; set; }
-
- /// <summary>
- /// Gets or sets the device profile.
- /// </summary>
- public DeviceProfile? DeviceProfile { get; set; }
-
- /// <summary>
- /// Gets or sets the app store url.
- /// </summary>
- public string? AppStoreUrl { get; set; }
-
- /// <summary>
- /// Gets or sets the icon url.
- /// </summary>
- public string? IconUrl { get; set; }
-
-#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
- // TODO: Remove after 10.9
- [Obsolete("Unused")]
- [DefaultValue(false)]
- public bool? SupportsContentUploading { get; set; } = false;
-
- // TODO: Remove after 10.9
- [Obsolete("Unused")]
- [DefaultValue(false)]
- public bool? SupportsSync { get; set; } = false;
-#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
-
- /// <summary>
- /// Convert the dto to the full <see cref="ClientCapabilities"/> model.
- /// </summary>
- /// <returns>The converted <see cref="ClientCapabilities"/> model.</returns>
- public ClientCapabilities ToClientCapabilities()
- {
- return new ClientCapabilities
- {
- PlayableMediaTypes = PlayableMediaTypes,
- SupportedCommands = SupportedCommands,
- SupportsMediaControl = SupportsMediaControl,
- SupportsPersistentIdentifier = SupportsPersistentIdentifier,
- DeviceProfile = DeviceProfile,
- AppStoreUrl = AppStoreUrl,
- IconUrl = IconUrl
- };
- }
-}
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
index 1ae2cad4b6..9c29e372cf 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs
@@ -1,3 +1,4 @@
+using System;
using System.ComponentModel.DataAnnotations;
namespace Jellyfin.Api.Models.StartupDtos;
@@ -17,5 +18,6 @@ public class StartupRemoteAccessDto
/// Gets or sets a value indicating whether enable automatic port mapping.
/// </summary>
[Required]
+ [Obsolete("No longer supported")]
public bool EnableAutomaticPortMapping { get; set; }
}