aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api')
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs40
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs14
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs3
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs87
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs20
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs60
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs13
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs8
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs82
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs29
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs113
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs46
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs27
-rw-r--r--Jellyfin.Api/Controllers/LyricsController.cs58
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs42
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs5
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs314
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs21
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs9
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs8
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs52
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs1
-rw-r--r--Jellyfin.Api/Controllers/TrickplayController.cs4
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs22
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs50
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs116
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs9
-rw-r--r--Jellyfin.Api/Controllers/VideoAttachmentsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs45
-rw-r--r--Jellyfin.Api/Extensions/DtoExtensions.cs12
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs23
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs70
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs16
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs11
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs33
-rw-r--r--Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs2
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs15
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistDto.cs34
-rw-r--r--Jellyfin.Api/Models/PlaylistDtos/UpdatePlaylistUserDto.cs12
43 files changed, 1097 insertions, 446 deletions
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
index 965b7e7e6..e425000cd 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
@@ -1,10 +1,7 @@
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,61 +12,44 @@ 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 />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement)
{
+ // Succeed if the startup wizard / first time setup is not complete
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
context.Succeed(requirement);
- return Task.CompletedTask;
}
- var contextUser = context.User;
- if (requirement.RequireAdmin && !contextUser.IsInRole(UserRoles.Administrator))
- {
- context.Fail();
- return Task.CompletedTask;
- }
-
- var userId = contextUser.GetUserId();
- if (userId.IsEmpty())
- {
- context.Fail();
- return Task.CompletedTask;
- }
-
- if (!requirement.ValidateParentalSchedule)
+ // Succeed if user is admin
+ else if (context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(requirement);
- return Task.CompletedTask;
}
- var user = _userManager.GetUserById(userId);
- if (user is null)
+ // Fail if admin is required and user is not admin
+ else if (requirement.RequireAdmin)
{
- throw new ResourceNotFoundException();
+ context.Fail();
}
- if (user.IsParentalScheduleAllowed())
+ // Succeed if admin is not required and user is not guest
+ else if (context.User.IsInRole(UserRoles.User))
{
context.Succeed(requirement);
}
+ // Any user-specific checks are handled in the DefaultAuthorizationHandler.
return Task.CompletedTask;
}
}
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 72be55513..8954c8ef5 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -83,6 +83,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <response code="200">Audio stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
@@ -138,7 +139,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string>? streamOptions)
+ [FromQuery] Dictionary<string, string>? streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
StreamingRequestDto streamingRequest = new StreamingRequestDto
{
@@ -189,7 +191,8 @@ public class AudioController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Static,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
@@ -247,6 +250,7 @@ public class AudioController : BaseJellyfinApiController
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <response code="200">Audio stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
@@ -302,7 +306,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string>? streamOptions)
+ [FromQuery] Dictionary<string, string>? streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
StreamingRequestDto streamingRequest = new StreamingRequestDto
{
@@ -353,7 +358,8 @@ public class AudioController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Static,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index 076084c7a..ee912a9be 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Mime;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Models;
+using MediaBrowser.Common.Api;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins;
@@ -45,9 +46,9 @@ public class DashboardController : BaseJellyfinApiController
/// <response code="404">Server still loading.</response>
/// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
[HttpGet("web/ConfigurationPages")]
+ [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- [Authorize]
public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
[FromQuery] bool? enableInMainMenu)
{
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 1cad66326..6d94d96f3 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -194,7 +194,7 @@ public class DisplayPreferencesController : BaseJellyfinApiController
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{
- if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
+ if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out _))
{
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
displayPreferences.CustomPrefs.Remove(key);
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 49fc2f3d7..329dd2c4c 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -156,6 +156,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="maxWidth">Optional. The max width.</param>
/// <param name="maxHeight">Optional. The max height.</param>
/// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param>
+ /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <response code="200">Hls live stream retrieved.</response>
/// <returns>A <see cref="FileResult"/> containing the hls file.</returns>
[HttpGet("Videos/{itemId}/live.m3u8")]
@@ -213,7 +214,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
- [FromQuery] bool? enableSubtitlesInManifest)
+ [FromQuery] bool? enableSubtitlesInManifest,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
VideoRequestDto streamingRequest = new VideoRequestDto
{
@@ -267,7 +269,8 @@ public class DynamicHlsController : BaseJellyfinApiController
StreamOptions = streamOptions,
MaxHeight = maxHeight,
MaxWidth = maxWidth,
- EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true
+ EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
// CTS lifecycle is managed internally.
@@ -393,6 +396,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
/// <param name="enableTrickplay">Enable trickplay image playlists being added to master playlist.</param>
+ /// <param name="enableAudioVbrEncoding">Whether to enable Audio Encoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
[HttpGet("Videos/{itemId}/master.m3u8")]
@@ -451,7 +455,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true,
- [FromQuery] bool enableTrickplay = true)
+ [FromQuery] bool enableTrickplay = true,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
var streamingRequest = new HlsVideoRequestDto
{
@@ -505,7 +510,8 @@ public class DynamicHlsController : BaseJellyfinApiController
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
- EnableTrickplay = enableTrickplay
+ EnableTrickplay = enableTrickplay,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -564,6 +570,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+ /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <response code="200">Audio stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
[HttpGet("Audio/{itemId}/master.m3u8")]
@@ -620,7 +627,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+ [FromQuery] bool enableAdaptiveBitrateStreaming = true,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
var streamingRequest = new HlsAudioRequestDto
{
@@ -671,7 +679,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+ EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -730,6 +739,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Videos/{itemId}/main.m3u8")]
@@ -785,7 +795,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
using var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new VideoRequestDto
@@ -838,7 +849,8 @@ public class DynamicHlsController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
@@ -897,6 +909,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <response code="200">Audio stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/main.m3u8")]
@@ -951,7 +964,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
using var cancellationTokenSource = new CancellationTokenSource();
var streamingRequest = new StreamingRequestDto
@@ -1002,7 +1016,8 @@ public class DynamicHlsController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
@@ -1067,6 +1082,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
@@ -1128,7 +1144,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
var streamingRequest = new VideoRequestDto
{
@@ -1183,7 +1200,8 @@ public class DynamicHlsController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await GetDynamicSegment(streamingRequest, segmentId)
@@ -1247,6 +1265,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")]
@@ -1307,7 +1326,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
var streamingRequest = new StreamingRequestDto
{
@@ -1360,7 +1380,8 @@ public class DynamicHlsController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await GetDynamicSegment(streamingRequest, segmentId)
@@ -1481,7 +1502,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (currentTranscodingIndex.HasValue)
{
- DeleteLastFile(playlistPath, segmentExtension, 0);
+ await DeleteLastFile(playlistPath, segmentExtension, 0).ConfigureAwait(false);
}
streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
@@ -1671,8 +1692,8 @@ public class DynamicHlsController : BaseJellyfinApiController
if (audioBitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(state.ActualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
{
- var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, audioBitrate.Value / (audioChannels ?? 2));
- if (_encodingOptions.EnableAudioVbr && vbrParam is not null)
+ var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, audioBitrate.Value, audioChannels ?? 2);
+ if (_encodingOptions.EnableAudioVbr && state.EnableAudioVbrEncoding && vbrParam is not null)
{
audioTranscodeParams += vbrParam;
}
@@ -1712,12 +1733,11 @@ public class DynamicHlsController : BaseJellyfinApiController
var channels = state.OutputAudioChannels;
+ var useDownMixAlgorithm = state.AudioStream.Channels is 6 && _encodingOptions.DownMixStereoAlgorithm != DownMixStereoAlgorithms.None;
+
if (channels.HasValue
&& (channels.Value != 2
- || (state.AudioStream is not null
- && state.AudioStream.Channels.HasValue
- && state.AudioStream.Channels.Value > 5
- && _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)))
+ || (state.AudioStream?.Channels != null && !useDownMixAlgorithm)))
{
args += " -ac " + channels.Value;
}
@@ -1725,8 +1745,8 @@ public class DynamicHlsController : BaseJellyfinApiController
var bitrate = state.OutputAudioBitrate;
if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
{
- var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, bitrate.Value / (channels ?? 2));
- if (_encodingOptions.EnableAudioVbr && vbrParam is not null)
+ var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, bitrate.Value, channels ?? 2);
+ if (_encodingOptions.EnableAudioVbr && state.EnableAudioVbrEncoding && vbrParam is not null)
{
args += vbrParam;
}
@@ -1740,6 +1760,12 @@ public class DynamicHlsController : BaseJellyfinApiController
{
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
+ else if (state.AudioStream?.CodecTag is not null && state.AudioStream.CodecTag.Equals("ac-4", StringComparison.Ordinal))
+ {
+ // ac-4 audio tends to hava a super weird sample rate that will fail most encoders
+ // force resample it to 48KHz
+ args += " -ar 48000";
+ }
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions);
@@ -2010,17 +2036,19 @@ public class DynamicHlsController : BaseJellyfinApiController
}
}
- private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
+ private Task DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
{
var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
- if (file is not null)
+ if (file is null)
{
- DeleteFile(file.FullName, retryCount);
+ return Task.CompletedTask;
}
+
+ return DeleteFile(file.FullName, retryCount);
}
- private void DeleteFile(string path, int retryCount)
+ private async Task DeleteFile(string path, int retryCount)
{
if (retryCount >= 5)
{
@@ -2037,9 +2065,8 @@ public class DynamicHlsController : BaseJellyfinApiController
{
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
- var task = Task.Delay(100);
- task.Wait();
- DeleteFile(path, retryCount + 1);
+ await Task.Delay(100).ConfigureAwait(false);
+ await DeleteFile(path, retryCount + 1).ConfigureAwait(false);
}
catch (Exception ex)
{
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index d6e043e6a..4abca3271 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -162,7 +162,7 @@ public class FilterController : BaseJellyfinApiController
}
else if (parentId.HasValue)
{
- parentItem = _libraryManager.GetItemById(parentId.Value);
+ parentItem = _libraryManager.GetItemById<BaseItem>(parentId.Value);
}
var filters = new QueryFilters();
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 6b38fa7d3..8e8accab3 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -90,6 +90,7 @@ public class ImageController : BaseJellyfinApiController
/// <param name="userId">User Id.</param>
/// <response code="204">Image updated.</response>
/// <response code="403">User does not have permission to delete the image.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("UserImage")]
[Authorize]
@@ -97,6 +98,7 @@ public class ImageController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> PostUserImage(
[FromQuery] Guid? userId)
{
@@ -289,7 +291,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromQuery] int? imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -317,7 +319,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromRoute] int imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -346,7 +348,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -390,7 +392,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] ImageType imageType,
[FromRoute] int imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -433,7 +435,7 @@ public class ImageController : BaseJellyfinApiController
[FromRoute, Required] int imageIndex,
[FromQuery, Required] int newIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -456,7 +458,7 @@ public class ImageController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IEnumerable<ImageInfo>>> GetItemImageInfos([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -559,7 +561,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? foregroundLayer,
[FromQuery] int? imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -637,7 +639,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -715,7 +717,7 @@ public class ImageController : BaseJellyfinApiController
[FromQuery] string? foregroundLayer,
[FromRoute, Required] int imageIndex)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index 3cf485299..dcbacf1d7 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -62,9 +62,11 @@ public class InstantMixController : 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">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Songs/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -75,11 +77,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -99,9 +106,11 @@ public class InstantMixController : 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">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Albums/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -112,15 +121,20 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var album = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -136,9 +150,11 @@ public class InstantMixController : 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">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Playlists/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -149,15 +165,20 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var playlist = (Playlist)_libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<Playlist>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
- var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
+ var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
@@ -209,9 +230,11 @@ public class InstantMixController : 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">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Artists/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -222,11 +245,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -246,9 +274,11 @@ public class InstantMixController : 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">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Items/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
@@ -259,11 +289,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -283,9 +318,11 @@ public class InstantMixController : 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">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Artists/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Use GetInstantMixFromArtists")]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2(
[FromQuery, Required] Guid id,
@@ -320,9 +357,11 @@ public class InstantMixController : 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">Instant playlist returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
@@ -333,11 +372,16 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(id);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(id, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index e3aee1bf7..d009f80a9 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -4,6 +4,8 @@ using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -64,7 +66,7 @@ public class ItemLookupController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -234,6 +236,7 @@ public class ItemLookupController : BaseJellyfinApiController
/// <param name="searchResult">The remote search result.</param>
/// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
/// <response code="204">Item metadata refreshed.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
/// The task result contains an <see cref="NoContentResult"/>.
@@ -241,12 +244,18 @@ public class ItemLookupController : BaseJellyfinApiController
[HttpPost("Items/RemoteSearch/Apply/{itemId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> ApplySearchCriteria(
[FromRoute, Required] Guid itemId,
[FromBody, Required] RemoteSearchResult searchResult,
[FromQuery] bool replaceAllImages = true)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
+
_logger.LogInformation(
"Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}",
item.Id,
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index 0a8522e1c..d7a8c37c4 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -2,7 +2,10 @@ using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Api;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
@@ -61,7 +64,7 @@ public class ItemRefreshController : BaseJellyfinApiController
[FromQuery] bool replaceAllMetadata = false,
[FromQuery] bool replaceAllImages = false)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -77,7 +80,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|| replaceAllImages
|| replaceAllMetadata,
- IsAutomated = false
+ IsAutomated = false,
+ RemoveOldMetadata = replaceAllMetadata
};
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 9800248c6..4001a6add 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -5,6 +5,8 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Configuration;
@@ -72,7 +74,7 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -145,7 +147,11 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
var info = new MetadataEditorInfo
{
@@ -197,7 +203,7 @@ public class ItemUpdateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -258,7 +264,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (request.Studios is not null)
{
- item.Studios = request.Studios.Select(x => x.Name).ToArray();
+ item.Studios = Array.ConvertAll(request.Studios, x => x.Name);
}
if (request.DateCreated.HasValue)
@@ -282,19 +288,37 @@ public class ItemUpdateController : BaseJellyfinApiController
if (item is Series rseries)
{
- foreach (Season season in rseries.Children)
+ foreach (var season in rseries.Children.OfType<Season>())
{
- season.OfficialRating = request.OfficialRating;
+ if (!season.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ season.OfficialRating = request.OfficialRating;
+ }
+
season.CustomRating = request.CustomRating;
- season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!season.LockedFields.Contains(MetadataField.Tags))
+ {
+ season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
season.OnMetadataChanged();
await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
- foreach (Episode ep in season.Children)
+ foreach (var ep in season.Children.OfType<Episode>())
{
- ep.OfficialRating = request.OfficialRating;
+ if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ ep.OfficialRating = request.OfficialRating;
+ }
+
ep.CustomRating = request.CustomRating;
- ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!ep.LockedFields.Contains(MetadataField.Tags))
+ {
+ ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
@@ -302,11 +326,20 @@ public class ItemUpdateController : BaseJellyfinApiController
}
else if (item is Season season)
{
- foreach (Episode ep in season.Children)
+ foreach (var ep in season.Children.OfType<Episode>())
{
- ep.OfficialRating = request.OfficialRating;
+ if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ ep.OfficialRating = request.OfficialRating;
+ }
+
ep.CustomRating = request.CustomRating;
- ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!ep.LockedFields.Contains(MetadataField.Tags))
+ {
+ ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
@@ -315,9 +348,18 @@ public class ItemUpdateController : BaseJellyfinApiController
{
foreach (BaseItem track in album.Children)
{
- track.OfficialRating = request.OfficialRating;
+ if (!track.LockedFields.Contains(MetadataField.OfficialRating))
+ {
+ track.OfficialRating = request.OfficialRating;
+ }
+
track.CustomRating = request.CustomRating;
- track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+
+ if (!track.LockedFields.Contains(MetadataField.Tags))
+ {
+ track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ }
+
track.OnMetadataChanged();
await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
@@ -373,10 +415,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasAlbumArtist hasAlbumArtists)
{
- hasAlbumArtists.AlbumArtists = request
- .AlbumArtists
- .Select(i => i.Name)
- .ToArray();
+ hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
}
}
@@ -384,10 +423,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasArtist hasArtists)
{
- hasArtists.Artists = request
- .ArtistItems
- .Select(i => i.Name)
- .ToArray();
+ hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
}
}
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 26ae1a820..d33634412 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -76,6 +76,7 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
/// <param name="hasTrailer">Optional filter by items with trailers.</param>
/// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
+ /// <param name="indexNumber">Optional filter by index number.</param>
/// <param name="parentIndexNumber">Optional filter by parent index number.</param>
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
/// <param name="isHd">Optional filter by items that are HD or not.</param>
@@ -165,6 +166,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] bool? hasSpecialFeature,
[FromQuery] bool? hasTrailer,
[FromQuery] Guid? adjacentTo,
+ [FromQuery] int? indexNumber,
[FromQuery] int? parentIndexNumber,
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
@@ -246,9 +248,9 @@ public class ItemsController : BaseJellyfinApiController
var isApiKey = User.GetIsApiKey();
// if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
userId = RequestHelpers.GetUserId(User, userId);
- var user = !isApiKey && !userId.IsNullOrEmpty()
- ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
- : null;
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException();
// beyond this point, we're either using an api key or we have a valid user
if (!isApiKey && user is null)
@@ -256,6 +258,13 @@ public class ItemsController : BaseJellyfinApiController
return BadRequest("userId is required");
}
+ if (user is not null
+ && user.GetPreference(PreferenceKind.AllowedTags).Length != 0
+ && !fields.Contains(ItemFields.Tags))
+ {
+ fields = [..fields, ItemFields.Tags];
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -359,6 +368,7 @@ public class ItemsController : BaseJellyfinApiController
MinCommunityRating = minCommunityRating,
MinCriticRating = minCriticRating,
ParentId = parentId ?? Guid.Empty,
+ IndexNumber = indexNumber,
ParentIndexNumber = parentIndexNumber,
EnableTotalRecordCount = enableTotalRecordCount,
ExcludeItemIds = excludeItemIds,
@@ -710,6 +720,7 @@ public class ItemsController : BaseJellyfinApiController
hasSpecialFeature,
hasTrailer,
adjacentTo,
+ null,
parentIndexNumber,
hasParentalRating,
isHd,
@@ -967,9 +978,13 @@ public class ItemsController : BaseJellyfinApiController
}
var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
- return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user);
+ return _userDataRepository.GetUserDataDto(item, user);
}
/// <summary>
@@ -1014,8 +1029,8 @@ public class ItemsController : BaseJellyfinApiController
}
var user = _userManager.GetUserById(requestUserId) ?? throw new ResourceNotFoundException();
- var item = _libraryManager.GetItemById(itemId);
- if (item == null)
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
{
return NotFound();
}
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 984dc7789..62cb59335 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -102,7 +102,7 @@ public class LibraryController : BaseJellyfinApiController
[ProducesFile("video/*", "audio/*")]
public ActionResult GetFile([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -131,6 +131,8 @@ public class LibraryController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="sortOrder">Optional. Sort Order - Ascending, Descending.</param>
/// <response code="200">Theme songs returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>The item theme songs.</returns>
@@ -141,7 +143,9 @@ public class LibraryController : BaseJellyfinApiController
public ActionResult<ThemeMediaResult> GetThemeSongs(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
+ [FromQuery] bool inheritFromParent = false,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -152,20 +156,23 @@ public class LibraryController : BaseJellyfinApiController
? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
- return NotFound("Item not found.");
+ return NotFound();
}
- IEnumerable<BaseItem> themeItems;
+ sortOrder ??= [];
+ sortBy ??= [];
+ var orderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder);
+
+ IReadOnlyList<BaseItem> themeItems;
while (true)
{
- themeItems = item.GetThemeSongs();
+ themeItems = item.GetThemeSongs(user, orderBy);
- if (themeItems.Any() || !inheritFromParent)
+ if (themeItems.Count > 0 || !inheritFromParent)
{
break;
}
@@ -198,6 +205,8 @@ public class LibraryController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="sortOrder">Optional. Sort Order - Ascending, Descending.</param>
/// <response code="200">Theme videos returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>The item theme videos.</returns>
@@ -208,29 +217,33 @@ public class LibraryController : BaseJellyfinApiController
public ActionResult<ThemeMediaResult> GetThemeVideos(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
+ [FromQuery] bool inheritFromParent = false,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
-
var item = itemId.IsEmpty()
? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
- return NotFound("Item not found.");
+ return NotFound();
}
+ sortOrder ??= [];
+ sortBy ??= [];
+ var orderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder);
+
IEnumerable<BaseItem> themeItems;
while (true)
{
- themeItems = item.GetThemeVideos();
+ themeItems = item.GetThemeVideos(user, orderBy);
if (themeItems.Any() || !inheritFromParent)
{
@@ -265,6 +278,8 @@ public class LibraryController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="inheritFromParent">Optional. Determines whether or not parent items should be searched for theme media.</param>
+ /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
+ /// <param name="sortOrder">Optional. Sort Order - Ascending, Descending.</param>
/// <response code="200">Theme songs and videos returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>The item theme videos.</returns>
@@ -274,19 +289,26 @@ public class LibraryController : BaseJellyfinApiController
public ActionResult<AllThemeMediaResult> GetThemeMedia(
[FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
- [FromQuery] bool inheritFromParent = false)
+ [FromQuery] bool inheritFromParent = false,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[]? sortBy = null,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[]? sortOrder = null)
{
var themeSongs = GetThemeSongs(
itemId,
userId,
- inheritFromParent);
+ inheritFromParent,
+ sortBy,
+ sortOrder);
var themeVideos = GetThemeVideos(
itemId,
userId,
- inheritFromParent);
+ inheritFromParent,
+ sortBy,
+ sortOrder);
- if (themeSongs.Result is NotFoundObjectResult || themeVideos.Result is NotFoundObjectResult)
+ if (themeSongs.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound }
+ || themeVideos.Result is StatusCodeResult { StatusCode: StatusCodes.Status404NotFound })
{
return NotFound();
}
@@ -327,6 +349,7 @@ public class LibraryController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <response code="204">Item deleted.</response>
/// <response code="401">Unauthorized access.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Items/{itemId}")]
[Authorize]
@@ -335,17 +358,18 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteItem(Guid itemId)
{
- var isApiKey = User.GetIsApiKey();
var userId = User.GetUserId();
- var user = !isApiKey && !userId.IsEmpty()
- ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException()
- : null;
- if (!isApiKey && user is null)
+ var isApiKey = User.GetIsApiKey();
+ var user = userId.IsEmpty() && isApiKey
+ ? null
+ : _userManager.GetUserById(userId);
+
+ if (user is null && !isApiKey)
{
- return Unauthorized("Unauthorized access");
+ return NotFound();
}
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
@@ -391,7 +415,7 @@ public class LibraryController : BaseJellyfinApiController
foreach (var i in ids)
{
- var item = _libraryManager.GetItemById(i);
+ var item = _libraryManager.GetItemById<BaseItem>(i, user);
if (item is null)
{
return NotFound();
@@ -459,20 +483,18 @@ public class LibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<BaseItemDto>> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
- var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
-
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
- return NotFound("Item not found");
+ return NotFound();
}
var baseItemDtos = new List<BaseItemDto>();
- var user = userId.IsNullOrEmpty()
- ? null
- : _userManager.GetUserById(userId.Value);
-
var dtoOptions = new DtoOptions().AddClientFields(User);
BaseItem? parent = item.GetParent();
@@ -520,7 +542,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)
{
@@ -640,14 +666,16 @@ public class LibraryController : BaseJellyfinApiController
[ProducesFile("video/*", "audio/*")]
public async Task<ActionResult> GetDownload([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var userId = User.GetUserId();
+ var user = userId.IsEmpty()
+ ? null
+ : _userManager.GetUserById(userId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- var user = _userManager.GetUserById(User.GetUserId());
-
if (user is not null)
{
if (!item.CanDownload(user))
@@ -700,12 +728,14 @@ public class LibraryController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
var item = itemId.IsEmpty()
- ? (userId.IsNullOrEmpty()
+ ? (user is null
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
@@ -716,9 +746,6 @@ public class LibraryController : BaseJellyfinApiController
return new QueryResult<BaseItemDto>();
}
- var user = userId.IsNullOrEmpty()
- ? null
- : _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User);
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 23c430f85..93c2393f3 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -6,6 +6,8 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryStructureDto;
using MediaBrowser.Common.Api;
@@ -73,7 +75,7 @@ public class LibraryStructureController : BaseJellyfinApiController
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddVirtualFolder(
- [FromQuery] string? name,
+ [FromQuery] string name,
[FromQuery] CollectionTypeOptions? collectionType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
@@ -83,7 +85,7 @@ public class LibraryStructureController : BaseJellyfinApiController
if (paths is not null && paths.Length > 0)
{
- libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
+ libraryOptions.PathInfos = Array.ConvertAll(paths, i => new MediaPathInfo(i));
}
await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
@@ -101,7 +103,7 @@ public class LibraryStructureController : BaseJellyfinApiController
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveVirtualFolder(
- [FromQuery] string? name,
+ [FromQuery] string name,
[FromQuery] bool refreshLibrary = false)
{
await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
@@ -178,7 +180,21 @@ public class LibraryStructureController : BaseJellyfinApiController
// No need to start if scanning the library because it will handle it
if (refreshLibrary)
{
- await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
+ await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
+ var newLib = _libraryManager.GetUserRootFolder().Children.FirstOrDefault(f => f.Path.Equals(newPath, StringComparison.OrdinalIgnoreCase));
+ if (newLib is CollectionFolder folder)
+ {
+ foreach (var child in folder.GetPhysicalFolders())
+ {
+ await child.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
+ await child.ValidateChildren(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ // We don't know if this one can be validated individually, trigger a new validation
+ await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
+ }
}
else
{
@@ -265,18 +281,16 @@ public class LibraryStructureController : BaseJellyfinApiController
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path removed.</response>
- /// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
+ /// <exception cref="ArgumentException">The name of the library and path may not be empty.</exception>
[HttpDelete("Paths")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveMediaPath(
- [FromQuery] string? name,
- [FromQuery] string? path,
+ [FromQuery] string name,
+ [FromQuery] string path,
[FromQuery] bool refreshLibrary = false)
{
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ ArgumentException.ThrowIfNullOrWhiteSpace(path);
_libraryMonitor.Stop();
@@ -311,15 +325,21 @@ public class LibraryStructureController : BaseJellyfinApiController
/// </summary>
/// <param name="request">The library name and options.</param>
/// <response code="204">Library updated.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("LibraryOptions")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateLibraryOptions(
[FromBody] UpdateLibraryOptionsDto request)
{
- var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
+ var item = _libraryManager.GetItemById<CollectionFolder>(request.Id);
+ if (item is null)
+ {
+ return NotFound();
+ }
- collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
+ item.UpdateLibraryOptions(request.LibraryOptions);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 7768b3c45..2b26c01f8 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -220,9 +220,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="channelId">Channel id.</param>
/// <param name="userId">Optional. Attach user data.</param>
/// <response code="200">Live tv channel returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
[HttpGet("Channels/{channelId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
{
@@ -232,7 +234,12 @@ public class LiveTvController : BaseJellyfinApiController
: _userManager.GetUserById(userId.Value);
var item = channelId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(channelId);
+ : _libraryManager.GetItemById<BaseItem>(channelId, user);
+
+ if (item is null)
+ {
+ return NotFound();
+ }
var dtoOptions = new DtoOptions()
.AddClientFields(User);
@@ -416,9 +423,11 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="recordingId">Recording id.</param>
/// <param name="userId">Optional. Attach user data.</param>
/// <response code="200">Recording returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
[HttpGet("Recordings/{recordingId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.LiveTvAccess)]
public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
{
@@ -426,7 +435,13 @@ public class LiveTvController : BaseJellyfinApiController
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
- var item = recordingId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
+ var item = recordingId.IsEmpty()
+ ? _libraryManager.GetUserRootFolder()
+ : _libraryManager.GetItemById<BaseItem>(recordingId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
var dtoOptions = new DtoOptions()
.AddClientFields(User);
@@ -611,7 +626,8 @@ public class LiveTvController : BaseJellyfinApiController
{
query.IsSeries = true;
- if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series)
+ var series = _libraryManager.GetItemById<Series>(librarySeriesId.Value);
+ if (series is not null)
{
query.Name = series.Name;
}
@@ -665,7 +681,8 @@ public class LiveTvController : BaseJellyfinApiController
{
query.IsSeries = true;
- if (_libraryManager.GetItemById(body.LibrarySeriesId) is Series series)
+ var series = _libraryManager.GetItemById<Series>(body.LibrarySeriesId);
+ if (series is not null)
{
query.Name = series.Name;
}
@@ -779,7 +796,7 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
{
- var item = _libraryManager.GetItemById(recordingId);
+ var item = _libraryManager.GetItemById<BaseItem>(recordingId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/LyricsController.cs b/Jellyfin.Api/Controllers/LyricsController.cs
index f2b312b47..8eb4cadf8 100644
--- a/Jellyfin.Api/Controllers/LyricsController.cs
+++ b/Jellyfin.Api/Controllers/LyricsController.cs
@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller.Entities.Audio;
@@ -66,37 +67,16 @@ public class LyricsController : BaseJellyfinApiController
[HttpGet("Audio/{itemId}/Lyrics")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<LyricDto>> GetLyrics([FromRoute, Required] Guid itemId)
{
- var isApiKey = User.GetIsApiKey();
- var userId = User.GetUserId();
- if (!isApiKey && userId.IsEmpty())
- {
- return BadRequest();
- }
-
- var audio = _libraryManager.GetItemById<Audio>(itemId);
- if (audio is null)
+ var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
+ if (item is null)
{
return NotFound();
}
- if (!isApiKey)
- {
- var user = _userManager.GetUserById(userId);
- if (user is null)
- {
- return NotFound();
- }
-
- // Check the item is visible for the user
- if (!audio.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {audio.Name}.");
- }
- }
-
- var result = await _lyricManager.GetLyricsAsync(audio, CancellationToken.None).ConfigureAwait(false);
+ var result = await _lyricManager.GetLyricsAsync(item, CancellationToken.None).ConfigureAwait(false);
if (result is not null)
{
return Ok(result);
@@ -124,8 +104,8 @@ public class LyricsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery, Required] string fileName)
{
- var audio = _libraryManager.GetItemById<Audio>(itemId);
- if (audio is null)
+ var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
+ if (item is null)
{
return NotFound();
}
@@ -147,7 +127,7 @@ public class LyricsController : BaseJellyfinApiController
{
await Request.Body.CopyToAsync(stream).ConfigureAwait(false);
var uploadedLyric = await _lyricManager.SaveLyricAsync(
- audio,
+ item,
format,
stream)
.ConfigureAwait(false);
@@ -157,7 +137,7 @@ public class LyricsController : BaseJellyfinApiController
return BadRequest();
}
- _providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
return Ok(uploadedLyric);
}
}
@@ -176,13 +156,13 @@ public class LyricsController : BaseJellyfinApiController
public async Task<ActionResult> DeleteLyrics(
[FromRoute, Required] Guid itemId)
{
- var audio = _libraryManager.GetItemById<Audio>(itemId);
- if (audio is null)
+ var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
+ if (item is null)
{
return NotFound();
}
- await _lyricManager.DeleteLyricsAsync(audio).ConfigureAwait(false);
+ await _lyricManager.DeleteLyricsAsync(item).ConfigureAwait(false);
return NoContent();
}
@@ -199,13 +179,13 @@ public class LyricsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IReadOnlyList<RemoteLyricInfoDto>>> SearchRemoteLyrics([FromRoute, Required] Guid itemId)
{
- var audio = _libraryManager.GetItemById<Audio>(itemId);
- if (audio is null)
+ var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
+ if (item is null)
{
return NotFound();
}
- var results = await _lyricManager.SearchLyricsAsync(audio, false, CancellationToken.None).ConfigureAwait(false);
+ var results = await _lyricManager.SearchLyricsAsync(item, false, CancellationToken.None).ConfigureAwait(false);
return Ok(results);
}
@@ -225,19 +205,19 @@ public class LyricsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string lyricId)
{
- var audio = _libraryManager.GetItemById<Audio>(itemId);
- if (audio is null)
+ var item = _libraryManager.GetItemById<Audio>(itemId, User.GetUserId());
+ if (item is null)
{
return NotFound();
}
- var downloadedLyrics = await _lyricManager.DownloadLyricsAsync(audio, lyricId, CancellationToken.None).ConfigureAwait(false);
+ var downloadedLyrics = await _lyricManager.DownloadLyricsAsync(item, lyricId, CancellationToken.None).ConfigureAwait(false);
if (downloadedLyrics is null)
{
return NotFound();
}
- _providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
return Ok(downloadedLyrics);
}
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 742012b71..bc52be184 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -8,8 +8,10 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization;
@@ -32,6 +34,7 @@ public class MediaInfoController : BaseJellyfinApiController
private readonly ILibraryManager _libraryManager;
private readonly ILogger<MediaInfoController> _logger;
private readonly MediaInfoHelper _mediaInfoHelper;
+ private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="MediaInfoController"/> class.
@@ -41,18 +44,21 @@ public class MediaInfoController : BaseJellyfinApiController
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
/// <param name="mediaInfoHelper">Instance of the <see cref="MediaInfoHelper"/>.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface..</param>
public MediaInfoController(
IMediaSourceManager mediaSourceManager,
IDeviceManager deviceManager,
ILibraryManager libraryManager,
ILogger<MediaInfoController> logger,
- MediaInfoHelper mediaInfoHelper)
+ MediaInfoHelper mediaInfoHelper,
+ IUserManager userManager)
{
_mediaSourceManager = mediaSourceManager;
_deviceManager = deviceManager;
_libraryManager = libraryManager;
_logger = logger;
_mediaInfoHelper = mediaInfoHelper;
+ _userManager = userManager;
}
/// <summary>
@@ -61,16 +67,24 @@ public class MediaInfoController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <param name="userId">The user id.</param>
/// <response code="200">Playback info returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
[HttpGet("Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
- return await _mediaInfoHelper.GetPlaybackInfo(
- itemId,
- userId)
- .ConfigureAwait(false);
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ return await _mediaInfoHelper.GetPlaybackInfo(item, user).ConfigureAwait(false);
}
/// <summary>
@@ -97,9 +111,11 @@ public class MediaInfoController : BaseJellyfinApiController
/// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
/// <param name="playbackInfoDto">The playback info.</param>
/// <response code="200">Playback info returned.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
[HttpPost("Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
[FromRoute, Required] Guid itemId,
[FromQuery, ParameterObsolete] Guid? userId,
@@ -148,9 +164,19 @@ public class MediaInfoController : BaseJellyfinApiController
allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true;
allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? true;
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
var info = await _mediaInfoHelper.GetPlaybackInfo(
- itemId,
- userId,
+ item,
+ user,
mediaSourceId,
liveStreamId)
.ConfigureAwait(false);
@@ -163,8 +189,6 @@ public class MediaInfoController : BaseJellyfinApiController
if (profile is not null)
{
// set device specific data
- var item = _libraryManager.GetItemById(itemId);
-
foreach (var mediaSource in info.MediaSources)
{
_mediaInfoHelper.SetDeviceSpecificData(
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index c5e940108..274e94ee6 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -18,7 +18,7 @@ namespace Jellyfin.Api.Controllers;
/// Package Controller.
/// </summary>
[Route("")]
-[Authorize]
+[Authorize(Policy = Policies.RequiresElevation)]
public class PackageController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;
@@ -90,7 +90,6 @@ public class PackageController : BaseJellyfinApiController
[HttpPost("Packages/Installed/{name}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- [Authorize(Policy = Policies.RequiresElevation)]
public async Task<ActionResult> InstallPackage(
[FromRoute, Required] string name,
[FromQuery] Guid? assemblyGuid,
@@ -128,7 +127,6 @@ public class PackageController : BaseJellyfinApiController
/// <response code="204">Installation cancelled.</response>
/// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
[HttpDelete("Packages/Installing/{packageId}")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CancelPackageInstallation(
[FromRoute, Required] Guid packageId)
@@ -156,7 +154,6 @@ public class PackageController : BaseJellyfinApiController
/// <response code="204">Package repositories saved.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Repositories")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos)
{
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 0e7c3f155..63d6e1cc3 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -92,29 +92,271 @@ 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");
+ }
+
+ if (playlist.OwnerUserId.Equals(callingUserId))
+ {
+ return new PlaylistUserPermissions(callingUserId, true);
+ }
+
+ var userPermission = playlist.Shares.FirstOrDefault(s => s.UserId.Equals(userId));
+ var isPermitted = 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 +367,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 +405,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 +449,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,17 +468,31 @@ 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()
? null
: _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<Playlist>(playlistId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
- var items = playlist.GetManageableItems().ToArray();
+ var items = item.GetManageableItems().ToArray();
var count = items.Length;
if (startIndex.HasValue)
{
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 949d101dc..88aa0178f 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -6,6 +6,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -76,21 +77,21 @@ public class PlaystateController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
}
- var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
-
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
+ var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext, userId).ConfigureAwait(false);
+
var dto = UpdatePlayedStatus(user, item, true, datePlayed);
foreach (var additionalUserInfo in session.AdditionalUsers)
{
@@ -141,21 +142,21 @@ public class PlaystateController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
}
- var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
- var item = _libraryManager.GetItemById(itemId);
-
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
+ var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext, userId).ConfigureAwait(false);
+
var dto = UpdatePlayedStatus(user, item, false, null);
foreach (var additionalUserInfo in session.AdditionalUsers)
{
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index f63e63927..6abd7a23e 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -22,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// Plugins controller.
/// </summary>
-[Authorize]
+[Authorize(Policy = Policies.RequiresElevation)]
public class PluginsController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;
@@ -66,7 +66,6 @@ public class PluginsController : BaseJellyfinApiController
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/{version}/Enable")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
@@ -90,7 +89,6 @@ public class PluginsController : BaseJellyfinApiController
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/{version}/Disable")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
@@ -114,7 +112,6 @@ public class PluginsController : BaseJellyfinApiController
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpDelete("{pluginId}/{version}")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
@@ -137,7 +134,6 @@ public class PluginsController : BaseJellyfinApiController
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpDelete("{pluginId}")]
- [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Please use the UninstallPluginByVersion API.")]
diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 595cab2df..a476005cb 100644
--- a/Jellyfin.Api/Controllers/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -6,8 +6,11 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Api;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -68,7 +71,7 @@ public class RemoteImageController : BaseJellyfinApiController
[FromQuery] string? providerName,
[FromQuery] bool includeAllLanguages = false)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -127,7 +130,7 @@ public class RemoteImageController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute, Required] Guid itemId)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -154,7 +157,7 @@ public class RemoteImageController : BaseJellyfinApiController
[FromQuery, Required] ImageType type,
[FromQuery] string? imageUrl)
{
- var item = _libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 413b7b834..8bae6fb9b 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -211,7 +211,7 @@ public class SearchController : BaseJellyfinApiController
if (!item.ChannelId.IsEmpty())
{
- var channel = _libraryManager.GetItemById(item.ChannelId);
+ var channel = _libraryManager.GetItemById<BaseItem>(item.ChannelId);
result.ChannelName = channel?.Name;
}
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 52b58b8f1..60de66ab0 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -84,7 +84,8 @@ public class SessionController : BaseJellyfinApiController
if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
{
- result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(controllableByUserId.Value));
+ // User cannot control other user's sessions, validate user id.
+ result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(RequestHelpers.GetUserId(User, controllableByUserId)));
}
if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
@@ -105,6 +106,11 @@ public class SessionController : BaseJellyfinApiController
return true;
});
}
+ else if (!User.IsInRole(UserRoles.Administrator))
+ {
+ // Request isn't from administrator, limit to "own" sessions.
+ result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(User.GetUserId()));
+ }
if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0)
{
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index cc2a630e1..9da1dce93 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -12,6 +12,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.SubtitleDtos;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
@@ -95,8 +96,7 @@ public class SubtitleController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int index)
{
- var item = _libraryManager.GetItemById(itemId);
-
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
@@ -113,18 +113,24 @@ public class SubtitleController : BaseJellyfinApiController
/// <param name="language">The language of the subtitles.</param>
/// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
/// <response code="200">Subtitles retrieved.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
[HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
[Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string language,
[FromQuery] bool? isPerfectMatch)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<Video>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
- return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false);
+ return await _subtitleManager.SearchSubtitles(item, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
@@ -133,22 +139,28 @@ public class SubtitleController : BaseJellyfinApiController
/// <param name="itemId">The item id.</param>
/// <param name="subtitleId">The subtitle id.</param>
/// <response code="204">Subtitle downloaded.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
[Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DownloadRemoteSubtitles(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string subtitleId)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<Video>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
try
{
- await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
+ await _subtitleManager.DownloadSubtitles(item, subtitleId, CancellationToken.None)
.ConfigureAwait(false);
- _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
}
catch (Exception ex)
{
@@ -165,7 +177,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/*")]
@@ -223,7 +235,7 @@ public class SubtitleController : BaseJellyfinApiController
if (string.IsNullOrEmpty(format))
{
- var item = (Video)_libraryManager.GetItemById(itemId.Value);
+ var item = _libraryManager.GetItemById<Video>(itemId.Value);
var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture);
var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
@@ -321,10 +333,12 @@ public class SubtitleController : BaseJellyfinApiController
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="segmentLength">The subtitle segment length.</param>
/// <response code="200">Subtitle playlist retrieved.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesPlaylistFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> GetSubtitlePlaylist(
@@ -333,7 +347,11 @@ public class SubtitleController : BaseJellyfinApiController
[FromRoute, Required] string mediaSourceId,
[FromQuery, Required] int segmentLength)
{
- var item = (Video)_libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<Video>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
@@ -397,15 +415,21 @@ public class SubtitleController : BaseJellyfinApiController
/// <param name="itemId">The item the subtitle belongs to.</param>
/// <param name="body">The request body.</param>
/// <response code="204">Subtitle uploaded.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Videos/{itemId}/Subtitles")]
[Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UploadSubtitle(
[FromRoute, Required] Guid itemId,
[FromBody, Required] UploadSubtitleDto body)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
+ var item = _libraryManager.GetItemById<Video>(itemId, User.GetUserId());
+ if (item is null)
+ {
+ return NotFound();
+ }
var bytes = Encoding.UTF8.GetBytes(body.Data);
var memoryStream = new MemoryStream(bytes, 0, bytes.Length, false, true);
@@ -416,7 +440,7 @@ public class SubtitleController : BaseJellyfinApiController
await using (stream.ConfigureAwait(false))
{
await _subtitleManager.UploadSubtitle(
- video,
+ item,
new SubtitleResponse
{
Format = body.Format,
@@ -425,7 +449,7 @@ public class SubtitleController : BaseJellyfinApiController
IsHearingImpaired = body.IsHearingImpaired,
Stream = stream
}).ConfigureAwait(false);
- _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
return NoContent();
}
@@ -452,7 +476,7 @@ public class SubtitleController : BaseJellyfinApiController
long? endPositionTicks,
bool copyTimestamps)
{
- var item = _libraryManager.GetItemById(id);
+ var item = _libraryManager.GetItemById<BaseItem>(id);
return _subtitleEncoder.GetSubtitles(
item,
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 4fbaafa2a..d7d0cc454 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -215,6 +215,7 @@ public class TrailersController : BaseJellyfinApiController
hasSpecialFeature,
hasTrailer,
adjacentTo,
+ null,
parentIndexNumber,
hasParentalRating,
isHd,
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
index 2dc960229..0afe053da 100644
--- a/Jellyfin.Api/Controllers/TrickplayController.cs
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -5,6 +5,8 @@ using System.Text;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model;
@@ -84,7 +86,7 @@ public class TrickplayController : BaseJellyfinApiController
[FromRoute, Required] int index,
[FromQuery] Guid? mediaSourceId)
{
- var item = _libraryManager.GetItemById(mediaSourceId ?? itemId);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 3d84b61bf..426402667 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -231,20 +231,22 @@ public class TvShowsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ var shouldIncludeMissingEpisodes = (user is not null && user.DisplayMissingEpisodes) || User.GetIsApiKey();
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
{
- var item = _libraryManager.GetItemById(seasonId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(seasonId.Value);
if (item is not Season seasonItem)
{
return NotFound("No season exists with Id " + seasonId);
}
- episodes = seasonItem.GetEpisodes(user, dtoOptions);
+ episodes = seasonItem.GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes);
}
else if (season.HasValue) // Season number was supplied. Get episodes by season number
{
- if (_libraryManager.GetItemById(seriesId) is not Series series)
+ var series = _libraryManager.GetItemById<Series>(seriesId);
+ if (series is null)
{
return NotFound("Series not found");
}
@@ -255,16 +257,16 @@ public class TvShowsController : BaseJellyfinApiController
episodes = seasonItem is null ?
new List<BaseItem>()
- : ((Season)seasonItem).GetEpisodes(user, dtoOptions);
+ : ((Season)seasonItem).GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes);
}
else // No season number or season id was supplied. Returning all episodes.
{
- if (_libraryManager.GetItemById(seriesId) is not Series series)
+ if (_libraryManager.GetItemById<BaseItem>(seriesId) is not Series series)
{
return NotFound("Series not found");
}
- episodes = series.GetEpisodes(user, dtoOptions).ToList();
+ episodes = series.GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes).ToList();
}
// Filter after the fact in case the ui doesn't want them
@@ -342,13 +344,13 @@ public class TvShowsController : BaseJellyfinApiController
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
-
- if (_libraryManager.GetItemById(seriesId) is not Series series)
+ var item = _libraryManager.GetItemById<Series>(seriesId, user);
+ if (item is null)
{
- return NotFound("Series not found");
+ return NotFound();
}
- var seasons = series.GetItemList(new InternalItemsQuery(user)
+ var seasons = item.GetItemList(new InternalItemsQuery(user)
{
IsMissing = isMissing,
IsSpecialSeason = isSpecialSeason,
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index db78e9946..fe7353496 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -9,12 +9,15 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -33,6 +36,7 @@ public class UniversalAudioController : BaseJellyfinApiController
private readonly MediaInfoHelper _mediaInfoHelper;
private readonly AudioHelper _audioHelper;
private readonly DynamicHlsHelper _dynamicHlsHelper;
+ private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
@@ -42,18 +46,21 @@ public class UniversalAudioController : BaseJellyfinApiController
/// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
/// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
public UniversalAudioController(
ILibraryManager libraryManager,
ILogger<UniversalAudioController> logger,
MediaInfoHelper mediaInfoHelper,
AudioHelper audioHelper,
- DynamicHlsHelper dynamicHlsHelper)
+ DynamicHlsHelper dynamicHlsHelper,
+ IUserManager userManager)
{
_libraryManager = libraryManager;
_logger = logger;
_mediaInfoHelper = mediaInfoHelper;
_audioHelper = audioHelper;
_dynamicHlsHelper = dynamicHlsHelper;
+ _userManager = userManager;
}
/// <summary>
@@ -75,16 +82,19 @@ public class UniversalAudioController : BaseJellyfinApiController
/// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
+ /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
/// <response code="200">Audio stream returned.</response>
/// <response code="302">Redirected to remote audio stream.</response>
+ /// <response code="404">Item not found.</response>
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/universal")]
[HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status302Found)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId,
@@ -103,25 +113,35 @@ public class UniversalAudioController : BaseJellyfinApiController
[FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] bool? enableRemoteMedia,
+ [FromQuery] bool enableAudioVbrEncoding = true,
[FromQuery] bool breakOnNonKeyFrames = false,
[FromQuery] bool enableRedirection = true)
{
- var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
var info = await _mediaInfoHelper.GetPlaybackInfo(
- itemId,
- userId,
+ item,
+ user,
mediaSourceId)
.ConfigureAwait(false);
// set device specific data
- var item = _libraryManager.GetItemById(itemId);
-
foreach (var sourceInfo in info.MediaSources)
{
+ sourceInfo.TranscodingContainer = transcodingContainer;
+ sourceInfo.TranscodingSubProtocol = transcodingProtocol ?? sourceInfo.TranscodingSubProtocol;
_mediaInfoHelper.SetDeviceSpecificData(
item,
sourceInfo,
@@ -156,6 +176,8 @@ public class UniversalAudioController : BaseJellyfinApiController
return Redirect(mediaSource.Path);
}
+ // This one is currently very misleading as the SupportsDirectStream actually means "can direct play"
+ // The definition of DirectStream also seems changed during development
var isStatic = mediaSource.SupportsDirectStream;
if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.hls)
{
@@ -163,20 +185,25 @@ public class UniversalAudioController : BaseJellyfinApiController
// ffmpeg option -> file extension
// mpegts -> ts
// fmp4 -> mp4
- // TODO: remove this when we switch back to the segment muxer
var supportedHlsContainers = new[] { "ts", "mp4" };
+ // fallback to mpegts if device reports some weird value unsupported by hls
+ var requestedSegmentContainer = Array.Exists(
+ supportedHlsContainers,
+ element => string.Equals(element, transcodingContainer, StringComparison.OrdinalIgnoreCase)) ? transcodingContainer : "ts";
+ var segmentContainer = Array.Exists(
+ supportedHlsContainers,
+ element => string.Equals(element, mediaSource.TranscodingContainer, StringComparison.OrdinalIgnoreCase)) ? mediaSource.TranscodingContainer : requestedSegmentContainer;
var dynamicHlsRequestDto = new HlsAudioRequestDto
{
Id = itemId,
Container = ".m3u8",
Static = isStatic,
PlaySessionId = info.PlaySessionId,
- // fallback to mpegts if device reports some weird value unsupported by hls
- SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts",
+ SegmentContainer = segmentContainer,
MediaSourceId = mediaSourceId,
DeviceId = deviceId,
- AudioCodec = audioCodec,
+ AudioCodec = mediaSource.TranscodeReasons == TranscodeReason.ContainerNotSupported ? "copy" : audioCodec,
EnableAutoStreamCopy = true,
AllowAudioStreamCopy = true,
AllowVideoStreamCopy = true,
@@ -194,7 +221,8 @@ public class UniversalAudioController : BaseJellyfinApiController
TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
Context = EncodingContext.Static,
StreamOptions = new Dictionary<string, string>(),
- EnableAdaptiveBitrateStreaming = true
+ EnableAdaptiveBitrateStreaming = true,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index c19ad33c8..421f1bfb5 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -77,8 +77,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -86,20 +86,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(User);
@@ -133,8 +125,8 @@ public class UserLibraryController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetRootFolder([FromQuery] Guid? userId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -172,8 +164,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -181,20 +173,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(User);
var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
@@ -231,8 +215,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -240,20 +224,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
return MarkFavorite(user, item, true);
}
@@ -286,8 +262,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -295,20 +271,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
return MarkFavorite(user, item, false);
}
@@ -341,8 +309,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -350,20 +318,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
return UpdateUserItemRatingInternal(user, item, null);
}
@@ -398,8 +358,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromQuery] bool? likes)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -407,20 +367,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
return UpdateUserItemRatingInternal(user, item, likes);
}
@@ -455,8 +407,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -464,20 +416,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
var dtoOptions = new DtoOptions().AddClientFields(User);
if (item is IHasTrailers hasTrailers)
{
@@ -519,8 +463,8 @@ public class UserLibraryController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
{
- var requestUserId = RequestHelpers.GetUserId(User, userId);
- var user = _userManager.GetUserById(requestUserId);
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = _userManager.GetUserById(userId.Value);
if (user is null)
{
return NotFound();
@@ -528,20 +472,12 @@ public class UserLibraryController : BaseJellyfinApiController
var item = itemId.IsEmpty()
? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
if (item is null)
{
return NotFound();
}
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
var dtoOptions = new DtoOptions().AddClientFields(User);
return Ok(item
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index bf3ce1d39..01da50d02 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -85,16 +85,11 @@ public class UserViewsController : BaseJellyfinApiController
var folders = _userViewManager.GetUserViews(query);
var dtoOptions = new DtoOptions().AddClientFields(User);
- var fields = dtoOptions.Fields.ToList();
-
- fields.Add(ItemFields.PrimaryImageAspectRatio);
- fields.Add(ItemFields.DisplayPreferencesId);
- dtoOptions.Fields = fields.ToArray();
+ dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId];
var user = _userManager.GetUserById(userId.Value);
- var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
- .ToArray();
+ var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user));
return new QueryResult<BaseItemDto>(dtos);
}
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 23b9ba46f..b67c6fdb7 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -4,7 +4,10 @@ using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using Microsoft.AspNetCore.Http;
@@ -54,7 +57,7 @@ public class VideoAttachmentsController : BaseJellyfinApiController
{
try
{
- var item = _libraryManager.GetItemById(videoId);
+ var item = _libraryManager.GetItemById<BaseItem>(videoId, User.GetUserId());
if (item is null)
{
return NotFound();
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 380120032..7f9608378 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -7,7 +7,6 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -105,7 +104,11 @@ public class VideosController : BaseJellyfinApiController
? (userId.IsNullOrEmpty()
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder())
- : _libraryManager.GetItemById(itemId);
+ : _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
var dtoOptions = new DtoOptions();
dtoOptions = dtoOptions.AddClientFields(User);
@@ -139,24 +142,23 @@ public class VideosController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteAlternateSources([FromRoute, Required] Guid itemId)
{
- var video = (Video)_libraryManager.GetItemById(itemId);
-
- if (video is null)
+ var item = _libraryManager.GetItemById<Video>(itemId, User.GetUserId());
+ if (item is null)
{
- return NotFound("The video either does not exist or the id does not belong to a video.");
+ return NotFound();
}
- if (video.LinkedAlternateVersions.Length == 0)
+ if (item.LinkedAlternateVersions.Length == 0)
{
- video = (Video?)_libraryManager.GetItemById(video.PrimaryVersionId);
+ item = _libraryManager.GetItemById<Video>(Guid.Parse(item.PrimaryVersionId));
}
- if (video is null)
+ if (item is null)
{
return NotFound();
}
- foreach (var link in video.GetLinkedAlternateVersions())
+ foreach (var link in item.GetLinkedAlternateVersions())
{
link.SetPrimaryVersionId(null);
link.LinkedAlternateVersions = Array.Empty<LinkedChild>();
@@ -164,9 +166,9 @@ public class VideosController : BaseJellyfinApiController
await link.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
- video.LinkedAlternateVersions = Array.Empty<LinkedChild>();
- video.SetPrimaryVersionId(null);
- await video.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
+ item.LinkedAlternateVersions = Array.Empty<LinkedChild>();
+ item.SetPrimaryVersionId(null);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
@@ -184,8 +186,9 @@ public class VideosController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
+ var userId = User.GetUserId();
var items = ids
- .Select(i => _libraryManager.GetItemById(i))
+ .Select(i => _libraryManager.GetItemById<BaseItem>(i, userId))
.OfType<Video>()
.OrderBy(i => i.Id)
.ToList();
@@ -303,6 +306,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("{itemId}/stream")]
@@ -360,7 +364,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
// CTS lifecycle is managed internally.
@@ -416,7 +421,8 @@ public class VideosController : BaseJellyfinApiController
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
- StreamOptions = streamOptions
+ StreamOptions = streamOptions,
+ EnableAudioVbrEncoding = enableAudioVbrEncoding
};
var state = await StreamingHelpers.GetStreamingState(
@@ -541,6 +547,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
+ /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("{itemId}/stream.{container}")]
@@ -598,7 +605,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
- [FromQuery] Dictionary<string, string> streamOptions)
+ [FromQuery] Dictionary<string, string> streamOptions,
+ [FromQuery] bool enableAudioVbrEncoding = true)
{
return GetVideoStream(
itemId,
@@ -651,6 +659,7 @@ public class VideosController : BaseJellyfinApiController
audioStreamIndex,
videoStreamIndex,
context,
- streamOptions);
+ streamOptions,
+ enableAudioVbrEncoding);
}
}
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index 7d9823c25..3d17dbda1 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -43,11 +43,7 @@ public static class DtoExtensions
client.Contains("media center", StringComparison.OrdinalIgnoreCase) ||
client.Contains("classic", StringComparison.OrdinalIgnoreCase))
{
- int oldLen = dtoOptions.Fields.Count;
- var arr = new ItemFields[oldLen + 1];
- dtoOptions.Fields.CopyTo(arr, 0);
- arr[oldLen] = ItemFields.RecursiveItemCount;
- dtoOptions.Fields = arr;
+ dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.RecursiveItemCount];
}
}
@@ -61,11 +57,7 @@ public static class DtoExtensions
client.Contains("samsung", StringComparison.OrdinalIgnoreCase) ||
client.Contains("androidtv", StringComparison.OrdinalIgnoreCase))
{
- int oldLen = dtoOptions.Fields.Count;
- var arr = new ItemFields[oldLen + 1];
- dtoOptions.Fields.CopyTo(arr, 0);
- arr[oldLen] = ItemFields.ChildCount;
- dtoOptions.Fields = arr;
+ dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.ChildCount];
}
}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index e5e4356f8..ba92d811c 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -151,6 +151,14 @@ public class DynamicHlsHelper
var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
+ // from universal audio service, need to override the AudioCodec when the actual request differs from original query
+ if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase))
+ {
+ var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString());
+ newQuery["AudioCodec"] = state.OutputAudioCodec;
+ queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
+ }
+
// from universal audio service
if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
&& !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase))
@@ -725,6 +733,21 @@ public class DynamicHlsHelper
return HlsCodecStringHelpers.GetAv1String(profile, level, false, bitDepth);
}
+ // VP9 HLS is for video remuxing only, everything is probed from the original video
+ if (string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
+ {
+ var width = state.VideoStream.Width ?? 0;
+ var height = state.VideoStream.Height ?? 0;
+ var framerate = state.VideoStream.AverageFrameRate ?? 30;
+ var bitDepth = state.VideoStream.BitDepth ?? 8;
+ return HlsCodecStringHelpers.GetVp9String(
+ width,
+ height,
+ state.VideoStream.PixelFormat,
+ framerate,
+ bitDepth);
+ }
+
return string.Empty;
}
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index 5eec1b0ca..d0bfa1fbe 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -183,6 +183,68 @@ public static class HlsCodecStringHelpers
}
/// <summary>
+ /// Gets a VP9 codec string.
+ /// </summary>
+ /// <param name="width">Video width.</param>
+ /// <param name="height">Video height.</param>
+ /// <param name="pixelFormat">Video pixel format.</param>
+ /// <param name="framerate">Video framerate.</param>
+ /// <param name="bitDepth">Video bitDepth.</param>
+ /// <returns>The VP9 codec string.</returns>
+ public static string GetVp9String(int width, int height, string pixelFormat, float framerate, int bitDepth)
+ {
+ // refer: https://www.webmproject.org/vp9/mp4/
+ StringBuilder result = new StringBuilder("vp09", 13);
+
+ var profileString = pixelFormat switch
+ {
+ "yuv420p" => "00",
+ "yuvj420p" => "00",
+ "yuv422p" => "01",
+ "yuv444p" => "01",
+ "yuv420p10le" => "02",
+ "yuv420p12le" => "02",
+ "yuv422p10le" => "03",
+ "yuv422p12le" => "03",
+ "yuv444p10le" => "03",
+ "yuv444p12le" => "03",
+ _ => "00"
+ };
+
+ var lumaPictureSize = width * height;
+ var lumaSampleRate = lumaPictureSize * framerate;
+ var levelString = lumaPictureSize switch
+ {
+ <= 0 => "00",
+ <= 36864 => "10",
+ <= 73728 => "11",
+ <= 122880 => "20",
+ <= 245760 => "21",
+ <= 552960 => "30",
+ <= 983040 => "31",
+ <= 2228224 => lumaSampleRate <= 83558400 ? "40" : "41",
+ <= 8912896 => lumaSampleRate <= 311951360 ? "50" : (lumaSampleRate <= 588251136 ? "51" : "52"),
+ <= 35651584 => lumaSampleRate <= 1176502272 ? "60" : (lumaSampleRate <= 4706009088 ? "61" : "62"),
+ _ => "00" // This should not happen
+ };
+
+ if (bitDepth != 8
+ && bitDepth != 10
+ && bitDepth != 12)
+ {
+ // Default to 8 bits
+ bitDepth = 8;
+ }
+
+ result.Append('.').Append(profileString).Append('.').Append(levelString);
+ var bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture);
+ result.Append('.')
+ .Append(bitDepthD2);
+
+ return result.ToString();
+ }
+
+ /// <summary>
/// Gets an AV1 codec string.
/// </summary>
/// <param name="profile">AV1 profile.</param>
@@ -192,7 +254,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 +276,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 +291,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/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 6a24ad32a..212d678a8 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -24,6 +24,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Helpers;
@@ -76,21 +77,17 @@ public class MediaInfoHelper
/// <summary>
/// Get playback info.
/// </summary>
- /// <param name="id">Item id.</param>
- /// <param name="userId">User Id.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
/// <param name="mediaSourceId">Media source id.</param>
/// <param name="liveStreamId">Live stream id.</param>
/// <returns>A <see cref="Task"/> containing the <see cref="PlaybackInfoResponse"/>.</returns>
public async Task<PlaybackInfoResponse> GetPlaybackInfo(
- Guid id,
- Guid? userId,
+ BaseItem item,
+ User? user,
string? mediaSourceId = null,
string? liveStreamId = null)
{
- var user = userId.IsNullOrEmpty()
- ? null
- : _userManager.GetUserById(userId.Value);
- var item = _libraryManager.GetItemById(id);
var result = new PlaybackInfoResponse();
MediaSourceInfo[] mediaSources;
@@ -402,7 +399,8 @@ public class MediaInfoHelper
if (profile is not null)
{
- var item = _libraryManager.GetItemById(request.ItemId);
+ var item = _libraryManager.GetItemById<BaseItem>(request.ItemId)
+ ?? throw new ResourceNotFoundException();
SetDeviceSpecificData(
item,
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 429e97213..a3d7f471e 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -117,10 +117,15 @@ public static class RequestHelpers
return user.EnableUserPreferenceAccess;
}
- internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext)
+ internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
{
- var userId = httpContext.User.GetUserId();
- var user = userManager.GetUserById(userId);
+ userId ??= httpContext.User.GetUserId();
+ User? user = null;
+ if (!userId.IsNullOrEmpty())
+ {
+ user = userManager.GetUserById(userId.Value);
+ }
+
var session = await sessionManager.LogSessionActivity(
httpContext.User.GetClient(),
httpContext.User.GetVersion(),
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index bfe71fd87..535ef27c3 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -11,6 +11,7 @@ using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
@@ -18,6 +19,7 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Net.Http.Headers;
namespace Jellyfin.Api.Helpers;
@@ -107,7 +109,8 @@ public static class StreamingHelpers
?? state.SupportedSubtitleCodecs.FirstOrDefault();
}
- var item = libraryManager.GetItemById(streamingRequest.Id);
+ var item = libraryManager.GetItemById<BaseItem>(streamingRequest.Id)
+ ?? throw new ResourceNotFoundException();
state.IsInputVideo = item.MediaType == MediaType.Video;
@@ -125,7 +128,7 @@ public static class StreamingHelpers
if (mediaSource is null)
{
- var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
+ var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById<BaseItem>(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
? mediaSources[0]
@@ -139,6 +142,25 @@ public static class StreamingHelpers
}
else
{
+ // Enforce more restrictive transcoding profile for LiveTV due to compatability reasons
+ // Cap the MaxStreamingBitrate to 30Mbps, because we are unable to reliably probe source bitrate,
+ // which will cause the client to request extremely high bitrate that may fail the player/encoder
+ streamingRequest.VideoBitRate = streamingRequest.VideoBitRate > 30000000 ? 30000000 : streamingRequest.VideoBitRate;
+
+ if (streamingRequest.SegmentContainer is not null)
+ {
+ // Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues
+ // Notably: Some channels won't play on FireFox and LG webOS
+ // Some channels from HDHomerun will experience A/V sync issues
+ streamingRequest.SegmentContainer = "ts";
+ streamingRequest.VideoCodec = "h264";
+ streamingRequest.AudioCodec = "aac";
+ state.SupportedVideoCodecs = ["h264"];
+ state.Request.VideoCodec = "h264";
+ state.SupportedAudioCodecs = ["aac"];
+ state.Request.AudioCodec = "aac";
+ }
+
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
mediaSource = liveStreamInfo.Item1;
state.DirectStreamProvider = liveStreamInfo.Item2;
@@ -163,6 +185,9 @@ public static class StreamingHelpers
}
var outputAudioCodec = streamingRequest.AudioCodec;
+ state.OutputAudioCodec = outputAudioCodec;
+ state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
+ state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec))
{
state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0;
@@ -177,10 +202,6 @@ public static class StreamingHelpers
containerInternal = ".pcm";
}
- state.OutputAudioCodec = outputAudioCodec;
- state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
- state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
-
if (state.VideoRequest is not null)
{
state.OutputVideoCodec = state.Request.VideoCodec;
diff --git a/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
index 94ffc5238..7a549aada 100644
--- a/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
+++ b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs
@@ -12,7 +12,7 @@ public class MediaPathDto
/// Gets or sets the name of the library.
/// </summary>
[Required]
- public string? Name { get; set; }
+ public required string Name { get; set; }
/// <summary>
/// Gets or sets the path to add.
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index bdc488871..3cbdd031a 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 000000000..80e20995c
--- /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 000000000..60467b5e7
--- /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; }
+}