diff options
Diffstat (limited to 'Jellyfin.Api/Controllers')
| -rw-r--r-- | Jellyfin.Api/Controllers/ChannelsController.cs | 6 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/DynamicHlsController.cs | 93 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/FilterController.cs | 1 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/ItemUpdateController.cs | 15 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/ItemsController.cs | 4 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/LibraryController.cs | 64 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/LiveTvController.cs | 10 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/PlaylistsController.cs | 6 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/PluginsController.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/QuickConnectController.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/SearchController.cs | 1 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/StartupController.cs | 4 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/SubtitleController.cs | 6 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/SystemController.cs | 12 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/UniversalAudioController.cs | 1 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/UserController.cs | 34 |
16 files changed, 165 insertions, 96 deletions
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index b5c4d8346..11c4ac376 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -52,7 +52,7 @@ public class ChannelsController : BaseJellyfinApiController /// <returns>An <see cref="OkResult"/> containing the channels.</returns> [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetChannels( + public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannels( [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, @@ -61,7 +61,7 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery] bool? isFavorite) { userId = RequestHelpers.GetUserId(User, userId); - return _channelManager.GetChannels(new ChannelQuery + return await _channelManager.GetChannelsAsync(new ChannelQuery { Limit = limit, StartIndex = startIndex, @@ -69,7 +69,7 @@ public class ChannelsController : BaseJellyfinApiController SupportsLatestItems = supportsLatestItems, SupportsMediaDeletion = supportsMediaDeletion, IsFavorite = isFavorite - }); + }).ConfigureAwait(false); } /// <summary> diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 4d8b4de24..ce684e457 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -12,6 +12,8 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using Jellyfin.MediaEncoding.Hls.Playlist; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; @@ -19,6 +21,7 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.MediaEncoding.Encoder; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; @@ -1639,9 +1642,11 @@ public class DynamicHlsController : BaseJellyfinApiController Path.GetFileNameWithoutExtension(outputPath)); } + var hlsArguments = GetHlsArguments(isEventPlaylist, state.SegmentLength); + return string.Format( CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{12}\" -hls_playlist_type {11} -hls_list_size 0 -y \"{13}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{11}\" {12} -y \"{13}\"", inputModifier, _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer), threads, @@ -1653,9 +1658,36 @@ public class DynamicHlsController : BaseJellyfinApiController segmentFormat, startNumber.ToString(CultureInfo.InvariantCulture), baseUrlParam, - isEventPlaylist ? "event" : "vod", - outputTsArg, - outputPath).Trim(); + EncodingUtils.NormalizePath(outputTsArg), + hlsArguments, + EncodingUtils.NormalizePath(outputPath)).Trim(); + } + + /// <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> @@ -1685,14 +1717,25 @@ public class DynamicHlsController : BaseJellyfinApiController audioTranscodeParams += "-acodec " + audioCodec; - if (state.OutputAudioBitrate.HasValue) + var audioBitrate = state.OutputAudioBitrate; + var audioChannels = state.OutputAudioChannels; + + if (audioBitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(state.ActualOutputAudioCodec, StringComparison.OrdinalIgnoreCase)) { - audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture); + var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, audioBitrate.Value / (audioChannels ?? 2)); + if (_encodingOptions.EnableAudioVbr && vbrParam is not null) + { + audioTranscodeParams += vbrParam; + } + else + { + audioTranscodeParams += " -ab " + audioBitrate.Value.ToString(CultureInfo.InvariantCulture); + } } - if (state.OutputAudioChannels.HasValue) + if (audioChannels.HasValue) { - audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture); + audioTranscodeParams += " -ac " + audioChannels.Value.ToString(CultureInfo.InvariantCulture); } if (state.OutputAudioSampleRate.HasValue) @@ -1706,11 +1749,11 @@ public class DynamicHlsController : BaseJellyfinApiController // dts, flac, opus and truehd are experimental in mp4 muxer var strictArgs = string.Empty; - - if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) + var actualOutputAudioCodec = state.ActualOutputAudioCodec; + if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase) + || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) { strictArgs = " -strict -2"; } @@ -1744,10 +1787,17 @@ public class DynamicHlsController : BaseJellyfinApiController } var bitrate = state.OutputAudioBitrate; - - if (bitrate.HasValue) + if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase)) { - args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); + var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, bitrate.Value / (channels ?? 2)); + if (_encodingOptions.EnableAudioVbr && vbrParam is not null) + { + args += vbrParam; + } + else + { + args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); + } } if (state.OutputAudioSampleRate.HasValue) @@ -1789,7 +1839,7 @@ public class DynamicHlsController : BaseJellyfinApiController || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) { if (EncodingHelper.IsCopyCodec(codec) - && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase) + && (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))) @@ -1840,7 +1890,11 @@ public class DynamicHlsController : BaseJellyfinApiController // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; // video processing filters. - args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec); + var videoProcessParam = _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec); + + var negativeMapArgs = _encodingHelper.GetNegativeMapArgsByFilters(state, videoProcessParam); + + args = negativeMapArgs + args + videoProcessParam; // -start_at_zero is necessary to use with -ss when seeking, // otherwise the target position cannot be determined. @@ -2005,8 +2059,7 @@ public class DynamicHlsController : BaseJellyfinApiController { return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false) .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase)) - .OrderByDescending(fileSystem.GetLastWriteTimeUtc) - .FirstOrDefault(); + .MaxBy(fileSystem.GetLastWriteTimeUtc); } catch (IOException) { diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index dac07429f..d51a5325f 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 9c7148241..504f2fa1d 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -246,7 +246,10 @@ public class ItemUpdateController : BaseJellyfinApiController episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber; } - item.Tags = request.Tags; + if (request.Height is not null && item is LiveTvChannel channel) + { + channel.Height = request.Height.Value; + } if (request.Taglines is not null) { @@ -271,12 +274,19 @@ public class ItemUpdateController : BaseJellyfinApiController item.OfficialRating = request.OfficialRating; item.CustomRating = request.CustomRating; + var currentTags = item.Tags; + var newTags = request.Tags; + var removedTags = currentTags.Except(newTags).ToList(); + var addedTags = newTags.Except(currentTags).ToList(); + item.Tags = newTags; + if (item is Series rseries) { foreach (Season season in rseries.Children) { season.OfficialRating = request.OfficialRating; season.CustomRating = request.CustomRating; + season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); season.OnMetadataChanged(); await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); @@ -284,6 +294,7 @@ public class ItemUpdateController : BaseJellyfinApiController { ep.OfficialRating = request.OfficialRating; ep.CustomRating = request.CustomRating; + ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); ep.OnMetadataChanged(); await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } @@ -295,6 +306,7 @@ public class ItemUpdateController : BaseJellyfinApiController { ep.OfficialRating = request.OfficialRating; ep.CustomRating = request.CustomRating; + ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); ep.OnMetadataChanged(); await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } @@ -305,6 +317,7 @@ public class ItemUpdateController : BaseJellyfinApiController { track.OfficialRating = request.OfficialRating; track.CustomRating = request.CustomRating; + track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); track.OnMetadataChanged(); await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 377526729..80128536d 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -256,8 +256,7 @@ public class ItemsController : BaseJellyfinApiController .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); if (includeItemTypes.Length == 1 - && (includeItemTypes[0] == BaseItemKind.Playlist - || includeItemTypes[0] == BaseItemKind.BoxSet)) + && includeItemTypes[0] == BaseItemKind.BoxSet) { parentId = null; } @@ -503,6 +502,7 @@ public class ItemsController : BaseJellyfinApiController } } + query.Parent = null; result = folder.GetItems(query); } else diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index bf59febed..46c0a8d52 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -15,6 +15,7 @@ using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -332,12 +333,26 @@ public class LibraryController : BaseJellyfinApiController [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteItem(Guid itemId) { + var isApiKey = User.GetIsApiKey(); + var userId = User.GetUserId(); + var user = !isApiKey && !userId.Equals(default) + ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException() + : null; + if (!isApiKey && user is null) + { + return Unauthorized("Unauthorized access"); + } + var item = _libraryManager.GetItemById(itemId); - var user = _userManager.GetUserById(User.GetUserId()); + if (item is null) + { + return NotFound(); + } - if (!item.CanDelete(user)) + if (user is not null && !item.CanDelete(user)) { return Unauthorized("Unauthorized access"); } @@ -361,26 +376,31 @@ public class LibraryController : BaseJellyfinApiController [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) { - if (ids.Length == 0) + var isApiKey = User.GetIsApiKey(); + var userId = User.GetUserId(); + var user = !isApiKey && !userId.Equals(default) + ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException() + : null; + + if (!isApiKey && user is null) { - return NoContent(); + return Unauthorized("Unauthorized access"); } foreach (var i in ids) { var item = _libraryManager.GetItemById(i); - var user = _userManager.GetUserById(User.GetUserId()); - - if (!item.CanDelete(user)) + if (item is null) { - if (ids.Length > 1) - { - return Unauthorized("Unauthorized access"); - } + return NotFound(); + } - continue; + if (user is not null && !item.CanDelete(user)) + { + return Unauthorized("Unauthorized access"); } _libraryManager.DeleteItem( @@ -949,12 +969,8 @@ public class LibraryController : BaseJellyfinApiController || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); } - var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - return metadataOptions.Length == 0 - || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); + var metadataOptions = _serverConfigurationManager.GetMetadataOptionsForType(type); + return metadataOptions is null || !metadataOptions.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) @@ -975,15 +991,7 @@ public class LibraryController : BaseJellyfinApiController || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase); } - var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions - .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - if (metadataOptions.Length == 0) - { - return true; - } - - return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); + var metadataOptions = _serverConfigurationManager.GetMetadataOptionsForType(type); + return metadataOptions is null || !metadataOptions.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase); } } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 96fc91f93..267ba4afb 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -252,7 +252,7 @@ public class LiveTvController : BaseJellyfinApiController [HttpGet("Recordings")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.LiveTvAccess)] - public ActionResult<QueryResult<BaseItemDto>> GetRecordings( + public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordings( [FromQuery] string? channelId, [FromQuery] Guid? userId, [FromQuery] int? startIndex, @@ -278,7 +278,7 @@ public class LiveTvController : BaseJellyfinApiController .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - return _liveTvManager.GetRecordings( + return await _liveTvManager.GetRecordingsAsync( new RecordingQuery { ChannelId = channelId, @@ -299,7 +299,7 @@ public class LiveTvController : BaseJellyfinApiController ImageTypeLimit = imageTypeLimit, EnableImages = enableImages }, - dtoOptions); + dtoOptions).ConfigureAwait(false); } /// <summary> @@ -383,13 +383,13 @@ public class LiveTvController : BaseJellyfinApiController [HttpGet("Recordings/Folders")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.LiveTvAccess)] - public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId) + public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordingFolders([FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); - var folders = _liveTvManager.GetRecordingFolders(user); + var folders = await _liveTvManager.GetRecordingFoldersAsync(user).ConfigureAwait(false); var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index c6dbea5e2..8d2a738d4 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -64,6 +64,7 @@ public class PlaylistsController : BaseJellyfinApiController /// <param name="userId">The user id.</param> /// <param name="mediaType">The media type.</param> /// <param name="createPlaylistRequest">The create playlist payload.</param> + /// <response code="200">Playlist created.</response> /// <returns> /// A <see cref="Task" /> that represents the asynchronous operation to create a playlist. /// The task result contains an <see cref="OkResult"/> indicating success. @@ -167,6 +168,8 @@ public class PlaylistsController : BaseJellyfinApiController /// <response code="404">Playlist not found.</response> /// <returns>The original playlist items.</returns> [HttpGet("{playlistId}/Items")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems( [FromRoute, Required] Guid playlistId, [FromQuery, Required] Guid userId, @@ -189,9 +192,7 @@ public class PlaylistsController : BaseJellyfinApiController : _userManager.GetUserById(userId); var items = playlist.GetManageableItems().ToArray(); - var count = items.Length; - if (startIndex.HasValue) { items = items.Skip(startIndex.Value).ToArray(); @@ -207,7 +208,6 @@ public class PlaylistsController : BaseJellyfinApiController .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); 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; diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 4726cf066..72ad14a28 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -146,7 +146,7 @@ public class PluginsController : BaseJellyfinApiController var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList(); // Select the un-instanced one first. - var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault(); + var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.MinBy(p => p.Manifest.Status); if (plugin is not null) { diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index d7e54b5b6..14f5265aa 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -1,8 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; -using Jellyfin.Api.Constants; -using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index f638c31c3..387b3ea5a 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -3,7 +3,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index aab390d1f..1098733b2 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -131,6 +131,10 @@ public class StartupController : BaseJellyfinApiController public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); + if (string.IsNullOrWhiteSpace(startupUserDto.Password)) + { + return BadRequest("Password must not be empty"); + } if (startupUserDto.Name is not null) { diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index e38421338..b3e9d6297 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -533,10 +533,8 @@ public class SubtitleController : BaseJellyfinApiController _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize); return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName)); } - else - { - _logger.LogWarning("The selected font is null or empty"); - } + + _logger.LogWarning("The selected font is null or empty"); } else { diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 4ab705f40..9ed69f420 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -59,10 +59,12 @@ public class SystemController : BaseJellyfinApiController /// Gets information about the server. /// </summary> /// <response code="200">Information retrieved.</response> + /// <response code="403">User does not have permission to retrieve information.</response> /// <returns>A <see cref="SystemInfo"/> with info about the system.</returns> [HttpGet("Info")] [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult<SystemInfo> GetSystemInfo() { return _appHost.GetSystemInfo(Request); @@ -97,10 +99,12 @@ public class SystemController : BaseJellyfinApiController /// Restarts the application. /// </summary> /// <response code="204">Server restarted.</response> + /// <response code="403">User does not have permission to restart server.</response> /// <returns>No content. Server restarted.</returns> [HttpPost("Restart")] [Authorize(Policy = Policies.LocalAccessOrRequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult RestartApplication() { Task.Run(async () => @@ -115,10 +119,12 @@ public class SystemController : BaseJellyfinApiController /// Shuts down the application. /// </summary> /// <response code="204">Server shut down.</response> + /// <response code="403">User does not have permission to shutdown server.</response> /// <returns>No content. Server shut down.</returns> [HttpPost("Shutdown")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult ShutdownApplication() { Task.Run(async () => @@ -133,10 +139,12 @@ public class SystemController : BaseJellyfinApiController /// Gets a list of available server log files. /// </summary> /// <response code="200">Information retrieved.</response> + /// <response code="403">User does not have permission to get server logs.</response> /// <returns>An array of <see cref="LogFile"/> with the available log files.</returns> [HttpGet("Logs")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult<LogFile[]> GetServerLogs() { IEnumerable<FileSystemMetadata> files; @@ -170,10 +178,12 @@ public class SystemController : BaseJellyfinApiController /// Gets information about the request endpoint. /// </summary> /// <response code="200">Information retrieved.</response> + /// <response code="403">User does not have permission to get endpoint information.</response> /// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns> [HttpGet("Endpoint")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] public ActionResult<EndPointInfo> GetEndpointInfo() { return new EndPointInfo @@ -188,10 +198,12 @@ public class SystemController : BaseJellyfinApiController /// </summary> /// <param name="name">The name of the log file to get.</param> /// <response code="200">Log file retrieved.</response> + /// <response code="403">User does not have permission to get log files.</response> /// <returns>The log file.</returns> [HttpGet("Logs/Log")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesFile(MediaTypeNames.Text.Plain)] public ActionResult GetLogFile([FromQuery, Required] string name) { diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 12d033ae6..2e9035d24 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.StreamingDtos; diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index b0973b8a1..530bd9603 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -15,6 +15,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Configuration; @@ -41,6 +42,7 @@ public class UserController : BaseJellyfinApiController private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly IQuickConnect _quickConnectManager; + private readonly IPlaylistManager _playlistManager; /// <summary> /// Initializes a new instance of the <see cref="UserController"/> class. @@ -53,6 +55,7 @@ public class UserController : BaseJellyfinApiController /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> /// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param> + /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param> public UserController( IUserManager userManager, ISessionManager sessionManager, @@ -61,7 +64,8 @@ public class UserController : BaseJellyfinApiController IAuthorizationContext authContext, IServerConfigurationManager config, ILogger<UserController> logger, - IQuickConnect quickConnectManager) + IQuickConnect quickConnectManager, + IPlaylistManager playlistManager) { _userManager = userManager; _sessionManager = sessionManager; @@ -71,6 +75,7 @@ public class UserController : BaseJellyfinApiController _config = config; _logger = logger; _quickConnectManager = quickConnectManager; + _playlistManager = playlistManager; } /// <summary> @@ -153,6 +158,7 @@ public class UserController : BaseJellyfinApiController } await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false); + await _playlistManager.RemovePlaylistsAsync(userId).ConfigureAwait(false); await _userManager.DeleteUserAsync(userId).ConfigureAwait(false); return NoContent(); } @@ -317,36 +323,16 @@ public class UserController : BaseJellyfinApiController /// <response code="404">User not found.</response> /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns> [HttpPost("{userId}/EasyPassword")] + [Obsolete("Use Quick Connect instead")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task<ActionResult> UpdateUserEasyPassword( + public ActionResult UpdateUserEasyPassword( [FromRoute, Required] Guid userId, [FromBody, Required] UpdateUserEasyPassword request) { - if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) - { - return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password."); - } - - var user = _userManager.GetUserById(userId); - - if (user is null) - { - return NotFound("User not found"); - } - - if (request.ResetPassword) - { - await _userManager.ResetEasyPassword(user).ConfigureAwait(false); - } - else - { - await _userManager.ChangeEasyPassword(user, request.NewPw ?? string.Empty, request.NewPassword ?? string.Empty).ConfigureAwait(false); - } - - return NoContent(); + return Forbid(); } /// <summary> |
