diff options
Diffstat (limited to 'Jellyfin.Api')
17 files changed, 476 insertions, 168 deletions
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs index 965b7e7e60..2b6b2a82c4 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs @@ -1,10 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Api.Extensions; -using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy @@ -15,19 +11,14 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy public class FirstTimeSetupHandler : AuthorizationHandler<FirstTimeSetupRequirement> { private readonly IConfigurationManager _configurationManager; - private readonly IUserManager _userManager; /// <summary> /// Initializes a new instance of the <see cref="FirstTimeSetupHandler" /> class. /// </summary> /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> - /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> - public FirstTimeSetupHandler( - IConfigurationManager configurationManager, - IUserManager userManager) + public FirstTimeSetupHandler(IConfigurationManager configurationManager) { _configurationManager = configurationManager; - _userManager = userManager; } /// <inheritdoc /> @@ -36,37 +27,14 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) { context.Succeed(requirement); - return Task.CompletedTask; } - - var contextUser = context.User; - if (requirement.RequireAdmin && !contextUser.IsInRole(UserRoles.Administrator)) + else if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator)) { context.Fail(); - return Task.CompletedTask; } - - var userId = contextUser.GetUserId(); - if (userId.IsEmpty()) - { - context.Fail(); - return Task.CompletedTask; - } - - if (!requirement.ValidateParentalSchedule) - { - context.Succeed(requirement); - return Task.CompletedTask; - } - - var user = _userManager.GetUserById(userId); - if (user is null) - { - throw new ResourceNotFoundException(); - } - - if (user.IsParentalScheduleAllowed()) + else { + // Any user-specific checks are handled in the DefaultAuthorizationHandler. context.Succeed(requirement); } diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index cd09d2bfab..72be555133 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -91,18 +91,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task<ActionResult> GetAudioStream( [FromRoute, Required] Guid itemId, - [FromQuery] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -132,8 +132,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -261,12 +261,12 @@ public class AudioController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -296,8 +296,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 8db22f7ebe..abe8bec2db 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -125,12 +125,15 @@ public class ConfigurationController : BaseJellyfinApiController /// <param name="mediaEncoderPath">Media encoder path form body.</param> /// <response code="204">Media encoder path updated.</response> /// <returns>Status.</returns> + [Obsolete("This endpoint is obsolete.")] + [ApiExplorerSettings(IgnoreApi = true)] [HttpPost("MediaEncoder/Path")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath) { - _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); + // API ENDPOINT DISABLED (NOOP) FOR SECURITY PURPOSES + // _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 590cdc33f0..49fc2f3d78 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -163,18 +163,18 @@ public class DynamicHlsController : BaseJellyfinApiController [ProducesPlaylistFile] public async Task<ActionResult> GetLiveHlsStream( [FromRoute, Required] Guid itemId, - [FromQuery] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -204,8 +204,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -406,12 +406,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -443,8 +443,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -577,12 +577,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -613,8 +613,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -742,12 +742,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -779,8 +779,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -909,12 +909,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -945,8 +945,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1085,12 +1085,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1122,8 +1122,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1265,12 +1265,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1301,8 +1301,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1604,7 +1604,7 @@ public class DynamicHlsController : BaseJellyfinApiController Path.GetFileNameWithoutExtension(outputPath)); } - var hlsArguments = GetHlsArguments(isEventPlaylist, state.SegmentLength); + var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0"; return string.Format( CultureInfo.InvariantCulture, @@ -1626,33 +1626,6 @@ public class DynamicHlsController : BaseJellyfinApiController } /// <summary> - /// Gets the HLS arguments for transcoding. - /// </summary> - /// <returns>The command line arguments for HLS transcoding.</returns> - private string GetHlsArguments(bool isEventPlaylist, int segmentLength) - { - var enableThrottling = _encodingOptions.EnableThrottling; - var enableSegmentDeletion = _encodingOptions.EnableSegmentDeletion; - - // Only enable segment deletion when throttling is enabled - if (enableThrottling && enableSegmentDeletion) - { - // Store enough segments for configured seconds of playback; this needs to be above throttling settings - var segmentCount = _encodingOptions.SegmentKeepSeconds / segmentLength; - - _logger.LogDebug("Using throttling and segment deletion, keeping {0} segments", segmentCount); - - return string.Format(CultureInfo.InvariantCulture, "-hls_list_size {0} -hls_flags delete_segments", segmentCount.ToString(CultureInfo.InvariantCulture)); - } - else - { - _logger.LogDebug("Using normal playback, is event playlist? {0}", isEventPlaylist); - - return string.Format(CultureInfo.InvariantCulture, "-hls_playlist_type {0} -hls_list_size 0", isEventPlaylist ? "event" : "vod"); - } - } - - /// <summary> /// Gets the audio arguments for transcoding. /// </summary> /// <param name="state">The <see cref="StreamState"/>.</param> @@ -1802,11 +1775,17 @@ public class DynamicHlsController : BaseJellyfinApiController || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) { + 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); + if (EncodingHelper.IsCopyCodec(codec) - && (state.VideoStream.VideoRangeType == VideoRangeType.DOVI - || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase))) + && ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI && requestHasDOVI) + || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 && requestHasDOVIWithHDR10) + || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG) + || (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR))) { // Prefer dvh1 to dvhe args += " -tag:v:0 dvh1 -strict -2"; diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 984dc77896..360389d292 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -520,7 +520,11 @@ public class LibraryController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden) { - var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList(); + var items = _libraryManager.GetUserRootFolder().Children + .Concat(_libraryManager.RootFolder.VirtualChildren) + .Where(i => _libraryManager.GetLibraryOptions(i).Enabled) + .OrderBy(i => i.SortName) + .ToList(); if (isHidden.HasValue) { diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 0e7c3f1556..1100f85cf5 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -92,29 +92,267 @@ public class PlaylistsController : BaseJellyfinApiController Name = name ?? createPlaylistRequest?.Name, ItemIdList = ids, UserId = userId.Value, - MediaType = mediaType ?? createPlaylistRequest?.MediaType + MediaType = mediaType ?? createPlaylistRequest?.MediaType, + Users = createPlaylistRequest?.Users.ToArray() ?? [], + Public = createPlaylistRequest?.IsPublic }).ConfigureAwait(false); return result; } /// <summary> + /// Updates a playlist. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <param name="updatePlaylistRequest">The <see cref="UpdatePlaylistDto"/> id.</param> + /// <response code="204">Playlist updated.</response> + /// <response code="403">Access forbidden.</response> + /// <response code="404">Playlist not found.</response> + /// <returns> + /// A <see cref="Task" /> that represents the asynchronous operation to update a playlist. + /// The task result contains an <see cref="OkResult"/> indicating success. + /// </returns> + [HttpPost("{playlistId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task<ActionResult> UpdatePlaylist( + [FromRoute, Required] Guid playlistId, + [FromBody, Required] UpdatePlaylistDto updatePlaylistRequest) + { + var callingUserId = User.GetUserId(); + + var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); + + if (!isPermitted) + { + return Forbid(); + } + + await _playlistManager.UpdatePlaylist(new PlaylistUpdateRequest + { + UserId = callingUserId, + Id = playlistId, + Name = updatePlaylistRequest.Name, + Ids = updatePlaylistRequest.Ids, + Users = updatePlaylistRequest.Users, + Public = updatePlaylistRequest.IsPublic + }).ConfigureAwait(false); + + return NoContent(); + } + + /// <summary> + /// Get a playlist's users. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <response code="200">Found shares.</response> + /// <response code="403">Access forbidden.</response> + /// <response code="404">Playlist not found.</response> + /// <returns> + /// A list of <see cref="PlaylistUserPermissions"/> objects. + /// </returns> + [HttpGet("{playlistId}/Users")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult<IReadOnlyList<PlaylistUserPermissions>> GetPlaylistUsers( + [FromRoute, Required] Guid playlistId) + { + var userId = User.GetUserId(); + + var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(userId); + + return isPermitted ? playlist.Shares.ToList() : Forbid(); + } + + /// <summary> + /// Get a playlist user. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <param name="userId">The user id.</param> + /// <response code="200">User permission found.</response> + /// <response code="403">Access forbidden.</response> + /// <response code="404">Playlist not found.</response> + /// <returns> + /// <see cref="PlaylistUserPermissions"/>. + /// </returns> + [HttpGet("{playlistId}/Users/{userId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult<PlaylistUserPermissions?> GetPlaylistUser( + [FromRoute, Required] Guid playlistId, + [FromRoute, Required] Guid userId) + { + var callingUserId = User.GetUserId(); + + var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var userPermission = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId)); + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)) + || userId.Equals(callingUserId); + + if (!isPermitted) + { + return Forbid(); + } + + if (userPermission is not null) + { + return userPermission; + } + + return NotFound("User permissions not found"); + } + + /// <summary> + /// Modify a user of a playlist's users. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <param name="userId">The user id.</param> + /// <param name="updatePlaylistUserRequest">The <see cref="UpdatePlaylistUserDto"/>.</param> + /// <response code="204">User's permissions modified.</response> + /// <response code="403">Access forbidden.</response> + /// <response code="404">Playlist not found.</response> + /// <returns> + /// A <see cref="Task" /> that represents the asynchronous operation to modify an user's playlist permissions. + /// The task result contains an <see cref="OkResult"/> indicating success. + /// </returns> + [HttpPost("{playlistId}/Users/{userId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task<ActionResult> UpdatePlaylistUser( + [FromRoute, Required] Guid playlistId, + [FromRoute, Required] Guid userId, + [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow), Required] UpdatePlaylistUserDto updatePlaylistUserRequest) + { + var callingUserId = User.GetUserId(); + + var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(callingUserId); + + if (!isPermitted) + { + return Forbid(); + } + + await _playlistManager.AddUserToShares(new PlaylistUserUpdateRequest + { + Id = playlistId, + UserId = userId, + CanEdit = updatePlaylistUserRequest.CanEdit + }).ConfigureAwait(false); + + return NoContent(); + } + + /// <summary> + /// Remove a user from a playlist's users. + /// </summary> + /// <param name="playlistId">The playlist id.</param> + /// <param name="userId">The user id.</param> + /// <response code="204">User permissions removed from playlist.</response> + /// <response code="401">Unauthorized access.</response> + /// <response code="404">No playlist or user permissions found.</response> + /// <returns> + /// A <see cref="Task" /> that represents the asynchronous operation to delete a user from a playlist's shares. + /// The task result contains an <see cref="OkResult"/> indicating success. + /// </returns> + [HttpDelete("{playlistId}/Users/{userId}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task<ActionResult> RemoveUserFromPlaylist( + [FromRoute, Required] Guid playlistId, + [FromRoute, Required] Guid userId) + { + var callingUserId = User.GetUserId(); + + var playlist = _playlistManager.GetPlaylistForUser(playlistId, callingUserId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); + + if (!isPermitted) + { + return Forbid(); + } + + var share = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId)); + if (share is null) + { + return NotFound("User permissions not found"); + } + + await _playlistManager.RemoveUserFromShares(playlistId, callingUserId, share).ConfigureAwait(false); + + return NoContent(); + } + + /// <summary> /// Adds items to a playlist. /// </summary> /// <param name="playlistId">The playlist id.</param> /// <param name="ids">Item id, comma delimited.</param> /// <param name="userId">The userId.</param> /// <response code="204">Items added to playlist.</response> + /// <response code="403">Access forbidden.</response> + /// <response code="404">Playlist not found.</response> /// <returns>An <see cref="NoContentResult"/> on success.</returns> [HttpPost("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task<ActionResult> AddToPlaylist( + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task<ActionResult> AddItemToPlaylist( [FromRoute, Required] Guid playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false); + var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId.Value); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(userId.Value) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(userId.Value)); + + if (!isPermitted) + { + return Forbid(); + } + + await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false); return NoContent(); } @@ -125,14 +363,34 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="itemId">The item id.</param> /// <param name="newIndex">The new index.</param> /// <response code="204">Item moved to new index.</response> + /// <response code="403">Access forbidden.</response> + /// <response code="404">Playlist not found.</response> /// <returns>An <see cref="NoContentResult"/> on success.</returns> [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task<ActionResult> MoveItem( [FromRoute, Required] string playlistId, [FromRoute, Required] string itemId, [FromRoute, Required] int newIndex) { + var callingUserId = User.GetUserId(); + + var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); + + if (!isPermitted) + { + return Forbid(); + } + await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false); return NoContent(); } @@ -143,14 +401,34 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="playlistId">The playlist id.</param> /// <param name="entryIds">The item ids, comma delimited.</param> /// <response code="204">Items removed.</response> + /// <response code="403">Access forbidden.</response> + /// <response code="404">Playlist not found.</response> /// <returns>An <see cref="NoContentResult"/> on success.</returns> [HttpDelete("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task<ActionResult> RemoveFromPlaylist( + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task<ActionResult> RemoveItemFromPlaylist( [FromRoute, Required] string playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds) { - await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); + var callingUserId = User.GetUserId(); + + var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId); + if (playlist is null) + { + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OwnerUserId.Equals(callingUserId) + || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId)); + + if (!isPermitted) + { + return Forbid(); + } + + await _playlistManager.RemoveItemFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); return NoContent(); } @@ -167,10 +445,12 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <response code="200">Original playlist returned.</response> + /// <response code="404">Access forbidden.</response> /// <response code="404">Playlist not found.</response> /// <returns>The original playlist items.</returns> [HttpGet("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems( [FromRoute, Required] Guid playlistId, @@ -184,10 +464,19 @@ public class PlaylistsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { userId = RequestHelpers.GetUserId(User, userId); - var playlist = (Playlist)_libraryManager.GetItemById(playlistId); + var playlist = _playlistManager.GetPlaylistForUser(playlistId, userId.Value); if (playlist is null) { - return NotFound(); + return NotFound("Playlist not found"); + } + + var isPermitted = playlist.OpenAccess + || playlist.OwnerUserId.Equals(userId.Value) + || playlist.Shares.Any(s => s.UserId.Equals(userId.Value)); + + if (!isPermitted) + { + return Forbid(); } var user = userId.IsNullOrEmpty() diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index cc2a630e1d..e2c5486d98 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -165,7 +165,7 @@ public class SubtitleController : BaseJellyfinApiController /// <response code="200">File returned.</response> /// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns> [HttpGet("Providers/Subtitles/Subtitles/{subtitleId}")] - [Authorize] + [Authorize(Policy = Policies.SubtitleManagement)] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] [ProducesFile("text/*")] diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 4c3ef2c7f7..db78e99464 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -92,13 +92,13 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] int? maxAudioChannels, [FromQuery] int? transcodingAudioChannels, [FromQuery] int? maxStreamingBitrate, [FromQuery] int? audioBitRate, [FromQuery] long? startTimeTicks, - [FromQuery] string? transcodingContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? transcodingContainer, [FromQuery] MediaStreamProtocol? transcodingProtocol, [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, @@ -157,7 +157,7 @@ public class UniversalAudioController : BaseJellyfinApiController } var isStatic = mediaSource.SupportsDirectStream; - if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.Hls) + if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.hls) { // hls segment container can only be mpegts or fmp4 per ffmpeg documentation // ffmpeg option -> file extension @@ -268,7 +268,7 @@ public class UniversalAudioController : BaseJellyfinApiController Context = EncodingContext.Streaming, Container = transcodingContainer ?? "mp3", AudioCodec = audioCodec ?? "mp3", - Protocol = transcodingProtocol ?? MediaStreamProtocol.Http, + Protocol = transcodingProtocol ?? MediaStreamProtocol.http, BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture) } diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index b3029d6fa8..3801200320 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -311,18 +311,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public async Task<ActionResult> GetVideoStream( [FromRoute, Required] Guid itemId, - [FromQuery] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -354,8 +354,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -555,12 +555,12 @@ public class VideosController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -592,8 +592,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 5eec1b0ca6..ec67b4c1bf 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -192,7 +192,7 @@ public static class HlsCodecStringHelpers /// <returns>The AV1 codec string.</returns> public static string GetAv1String(string? profile, int level, bool tierFlag, int bitDepth) { - // https://aomedia.org/av1/specification/annex-a/ + // https://aomediacodec.github.io/av1-isobmff/#codecsparam // FORMAT: [codecTag].[profile].[level][tier].[bitDepth] StringBuilder result = new StringBuilder("av01", 13); @@ -214,8 +214,7 @@ public static class HlsCodecStringHelpers result.Append(".0"); } - if (level <= 0 - || level > 31) + if (level is <= 0 or > 31) { // Default to the maximum defined level 6.3 level = 19; @@ -230,7 +229,8 @@ public static class HlsCodecStringHelpers } result.Append('.') - .Append(level) + // Needed to pad it double digits; otherwise, browsers will reject the stream. + .AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", level) .Append(tierFlag ? 'H' : 'M'); string bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture); diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs index bdc4888719..3cbdd031a1 100644 --- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs +++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json.Converters; +using MediaBrowser.Model.Entities; namespace Jellyfin.Api.Models.PlaylistDtos; @@ -14,13 +15,13 @@ public class CreatePlaylistDto /// <summary> /// Gets or sets the name of the new playlist. /// </summary> - public string? Name { get; set; } + public required string Name { get; set; } /// <summary> /// Gets or sets item ids to add to the playlist. /// </summary> [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList<Guid> Ids { get; set; } = Array.Empty<Guid>(); + public IReadOnlyList<Guid> Ids { get; set; } = []; /// <summary> /// Gets or sets the user id. @@ -31,4 +32,14 @@ public class CreatePlaylistDto /// Gets or sets the media type. /// </summary> public MediaType? MediaType { get; set; } + + /// <summary> + /// Gets or sets the playlist users. + /// </summary> + public IReadOnlyList<PlaylistUserPermissions> Users { get; set; } = []; + + /// <summary> + /// Gets or sets a value indicating whether the playlist is public. + /// </summary> + public bool IsPublic { get; set; } = true; } diff --git a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs new file mode 100644 index 0000000000..80e20995c6 --- /dev/null +++ b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Jellyfin.Extensions.Json.Converters; +using MediaBrowser.Model.Entities; + +namespace Jellyfin.Api.Models.PlaylistDtos; + +/// <summary> +/// Update existing playlist dto. Fields set to `null` will not be updated and keep their current values. +/// </summary> +public class UpdatePlaylistDto +{ + /// <summary> + /// Gets or sets the name of the new playlist. + /// </summary> + public string? Name { get; set; } + + /// <summary> + /// Gets or sets item ids of the playlist. + /// </summary> + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList<Guid>? Ids { get; set; } + + /// <summary> + /// Gets or sets the playlist users. + /// </summary> + public IReadOnlyList<PlaylistUserPermissions>? Users { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the playlist is public. + /// </summary> + public bool? IsPublic { get; set; } +} diff --git a/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs new file mode 100644 index 0000000000..60467b5e70 --- /dev/null +++ b/Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs @@ -0,0 +1,12 @@ +namespace Jellyfin.Api.Models.PlaylistDtos; + +/// <summary> +/// Update existing playlist user dto. Fields set to `null` will not be updated and keep their current values. +/// </summary> +public class UpdatePlaylistUserDto +{ + /// <summary> + /// Gets or sets a value indicating whether the user can edit the playlist. + /// </summary> + public bool? CanEdit { get; set; } +} diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs index 12ce19368b..b72dcff88e 100644 --- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs +++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs @@ -55,12 +55,12 @@ public class ClientCapabilitiesDto // TODO: Remove after 10.9 [Obsolete("Unused")] [DefaultValue(false)] - public bool? SupportsContentUploading { get; set; } + public bool? SupportsContentUploading { get; set; } = false; // TODO: Remove after 10.9 [Obsolete("Unused")] [DefaultValue(false)] - public bool? SupportsSync { get; set; } + public bool? SupportsSync { get; set; } = false; #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member /// <summary> diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index ba228cb002..99516e9384 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -20,6 +20,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi /// </summary> private readonly IActivityManager _activityManager; + private bool _disposed; + /// <summary> /// Initializes a new instance of the <see cref="ActivityLogWebSocketListener"/> class. /// </summary> @@ -51,14 +53,15 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi } /// <inheritdoc /> - protected override void Dispose(bool dispose) + protected override async ValueTask DisposeAsyncCore() { - if (dispose) + if (!_disposed) { _activityManager.EntryCreated -= OnEntryCreated; + _disposed = true; } - base.Dispose(dispose); + await base.DisposeAsyncCore().ConfigureAwait(false); } /// <summary> @@ -75,8 +78,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi base.Start(message); } - private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e) + private void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e) { - await SendData(true).ConfigureAwait(false); + SendData(true); } } diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs index 37c108d5a6..dd9286210a 100644 --- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs @@ -20,6 +20,8 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener<IEn /// <value>The task manager.</value> private readonly ITaskManager _taskManager; + private bool _disposed; + /// <summary> /// Initializes a new instance of the <see cref="ScheduledTasksWebSocketListener"/> class. /// </summary> @@ -56,31 +58,32 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener<IEn } /// <inheritdoc /> - protected override void Dispose(bool dispose) + protected override async ValueTask DisposeAsyncCore() { - if (dispose) + if (!_disposed) { _taskManager.TaskExecuting -= OnTaskExecuting; _taskManager.TaskCompleted -= OnTaskCompleted; + _disposed = true; } - base.Dispose(dispose); + await base.DisposeAsyncCore().ConfigureAwait(false); } - private async void OnTaskCompleted(object? sender, TaskCompletionEventArgs e) + private void OnTaskCompleted(object? sender, TaskCompletionEventArgs e) { e.Task.TaskProgress -= OnTaskProgress; - await SendData(true).ConfigureAwait(false); + SendData(true); } - private async void OnTaskExecuting(object? sender, GenericEventArgs<IScheduledTaskWorker> e) + private void OnTaskExecuting(object? sender, GenericEventArgs<IScheduledTaskWorker> e) { - await SendData(true).ConfigureAwait(false); + SendData(true); e.Argument.TaskProgress += OnTaskProgress; } - private async void OnTaskProgress(object? sender, GenericEventArgs<double> e) + private void OnTaskProgress(object? sender, GenericEventArgs<double> e) { - await SendData(false).ConfigureAwait(false); + SendData(false); } } diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index 3c2b86142e..a6cfe4d56c 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -16,6 +16,7 @@ namespace Jellyfin.Api.WebSocketListeners; public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState> { private readonly ISessionManager _sessionManager; + private bool _disposed; /// <summary> /// Initializes a new instance of the <see cref="SessionInfoWebSocketListener"/> class. @@ -55,9 +56,9 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume } /// <inheritdoc /> - protected override void Dispose(bool dispose) + protected override async ValueTask DisposeAsyncCore() { - if (dispose) + if (!_disposed) { _sessionManager.SessionStarted -= OnSessionManagerSessionStarted; _sessionManager.SessionEnded -= OnSessionManagerSessionEnded; @@ -66,9 +67,10 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress; _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged; _sessionManager.SessionActivity -= OnSessionManagerSessionActivity; + _disposed = true; } - base.Dispose(dispose); + await base.DisposeAsyncCore().ConfigureAwait(false); } /// <summary> @@ -85,38 +87,38 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume base.Start(message); } - private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e) + private void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e) { - await SendData(false).ConfigureAwait(false); + SendData(false); } - private async void OnSessionManagerCapabilitiesChanged(object? sender, SessionEventArgs e) + private void OnSessionManagerCapabilitiesChanged(object? sender, SessionEventArgs e) { - await SendData(true).ConfigureAwait(false); + SendData(true); } - private async void OnSessionManagerPlaybackProgress(object? sender, PlaybackProgressEventArgs e) + private void OnSessionManagerPlaybackProgress(object? sender, PlaybackProgressEventArgs e) { - await SendData(!e.IsAutomated).ConfigureAwait(false); + SendData(!e.IsAutomated); } - private async void OnSessionManagerPlaybackStopped(object? sender, PlaybackStopEventArgs e) + private void OnSessionManagerPlaybackStopped(object? sender, PlaybackStopEventArgs e) { - await SendData(true).ConfigureAwait(false); + SendData(true); } - private async void OnSessionManagerPlaybackStart(object? sender, PlaybackProgressEventArgs e) + private void OnSessionManagerPlaybackStart(object? sender, PlaybackProgressEventArgs e) { - await SendData(true).ConfigureAwait(false); + SendData(true); } - private async void OnSessionManagerSessionEnded(object? sender, SessionEventArgs e) + private void OnSessionManagerSessionEnded(object? sender, SessionEventArgs e) { - await SendData(true).ConfigureAwait(false); + SendData(true); } - private async void OnSessionManagerSessionStarted(object? sender, SessionEventArgs e) + private void OnSessionManagerSessionStarted(object? sender, SessionEventArgs e) { - await SendData(true).ConfigureAwait(false); + SendData(true); } } |
