aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api/Controllers')
-rw-r--r--Jellyfin.Api/Controllers/ChannelsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs93
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs1
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs15
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs4
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs64
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs10
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/QuickConnectController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs1
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs4
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs6
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs12
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs1
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs34
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>