diff options
Diffstat (limited to 'Jellyfin.Api/Controllers')
33 files changed, 424 insertions, 348 deletions
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index b54825775..44796bcc4 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Equals(default)) { user = _userManager.GetUserById(userId.Value); } @@ -329,7 +329,7 @@ namespace Jellyfin.Api.Controllers User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Equals(default)) { user = _userManager.GetUserById(userId.Value); } @@ -467,7 +467,7 @@ namespace Jellyfin.Api.Controllers var item = _libraryManager.GetArtist(name, dtoOptions); - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 54ac06276..94f7a7b82 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -207,7 +207,7 @@ namespace Jellyfin.Api.Controllers /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="playSessionId">The play session id.</param> /// <param name="segmentContainer">The segment container.</param> - /// <param name="segmentLength">The segment lenght.</param> + /// <param name="segmentLength">The segment length.</param> /// <param name="minSegments">The minimum number of segments.</param> /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 54bd80095..d5b589a3f 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -125,9 +125,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var query = new InternalItemsQuery(user) { @@ -199,9 +199,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var query = new InternalItemsQuery(user) { diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 60529e990..464fadc06 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -86,21 +86,23 @@ namespace Jellyfin.Api.Controllers /// Updates named configuration. /// </summary> /// <param name="key">Configuration key.</param> + /// <param name="configuration">Configuration.</param> /// <response code="204">Named configuration updated.</response> /// <returns>Update status.</returns> [HttpPost("Configuration/{key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task<ActionResult> UpdateNamedConfiguration([FromRoute, Required] string key) + public ActionResult UpdateNamedConfiguration([FromRoute, Required] string key, [FromBody, Required] JsonDocument configuration) { var configurationType = _configurationManager.GetConfigurationType(key); - var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false); - if (configuration == null) + var deserializedConfiguration = configuration.Deserialize(configurationType, _serializerOptions); + + if (deserializedConfiguration == null) { throw new ArgumentException("Body doesn't contain a valid configuration"); } - _configurationManager.SaveConfiguration(key, configuration); + _configurationManager.SaveConfiguration(key, deserializedConfiguration); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index 87cb418d9..c8411f44b 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -4,10 +4,12 @@ using System.IO; using System.Linq; using System.Net.Mime; using Jellyfin.Api.Attributes; +using Jellyfin.Api.Constants; using Jellyfin.Api.Models; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -46,6 +48,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("web/ConfigurationPages")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages( [FromQuery] bool? enableInMainMenu) { diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index b1c576c33..401c0197a 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers /// Dlna Server Controller. /// </summary> [Route("Dlna")] + [DlnaEnabled] [Authorize(Policy = Policies.AnonymousLanAccessPolicy)] public class DlnaServerController : BaseJellyfinApiController { @@ -55,15 +56,10 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult GetDescriptionXml([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - var url = GetAbsoluteUri(); - var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); - var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); - return Ok(xml); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + var url = GetAbsoluteUri(); + var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); + var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress); + return Ok(xml); } /// <summary> @@ -83,12 +79,7 @@ namespace Jellyfin.Api.Controllers [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetContentDirectory([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return Ok(_contentDirectory.GetServiceXml()); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return Ok(_contentDirectory.GetServiceXml()); } /// <summary> @@ -108,12 +99,7 @@ namespace Jellyfin.Api.Controllers [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return Ok(_mediaReceiverRegistrar.GetServiceXml()); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return Ok(_mediaReceiverRegistrar.GetServiceXml()); } /// <summary> @@ -133,12 +119,7 @@ namespace Jellyfin.Api.Controllers [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetConnectionManager([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return Ok(_connectionManager.GetServiceXml()); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return Ok(_connectionManager.GetServiceXml()); } /// <summary> @@ -155,12 +136,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); } /// <summary> @@ -177,12 +153,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); } /// <summary> @@ -199,12 +170,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId) { - if (DlnaEntryPoint.Enabled) - { - return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); } /// <summary> @@ -224,12 +190,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) { - if (DlnaEntryPoint.Enabled) - { - return ProcessEventRequest(_mediaReceiverRegistrar); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return ProcessEventRequest(_mediaReceiverRegistrar); } /// <summary> @@ -249,12 +210,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) { - if (DlnaEntryPoint.Enabled) - { - return ProcessEventRequest(_contentDirectory); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return ProcessEventRequest(_contentDirectory); } /// <summary> @@ -274,12 +230,7 @@ namespace Jellyfin.Api.Controllers [ProducesFile(MediaTypeNames.Text.Xml)] public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) { - if (DlnaEntryPoint.Enabled) - { - return ProcessEventRequest(_connectionManager); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return ProcessEventRequest(_connectionManager); } /// <summary> @@ -299,12 +250,7 @@ namespace Jellyfin.Api.Controllers [ProducesImageFile] public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName) { - if (DlnaEntryPoint.Enabled) - { - return GetIconInternal(fileName); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return GetIconInternal(fileName); } /// <summary> @@ -322,12 +268,7 @@ namespace Jellyfin.Api.Controllers [ProducesImageFile] public ActionResult GetIcon([FromRoute, Required] string fileName) { - if (DlnaEntryPoint.Enabled) - { - return GetIconInternal(fileName); - } - - return StatusCode(StatusCodes.Status503ServiceUnavailable); + return GetIconInternal(fileName); } private ActionResult GetIconInternal(string fileName) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index af6916630..3ed80f662 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -121,7 +121,7 @@ namespace Jellyfin.Api.Controllers /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="playSessionId">The play session id.</param> /// <param name="segmentContainer">The segment container.</param> - /// <param name="segmentLength">The segment lenght.</param> + /// <param name="segmentLength">The segment length.</param> /// <param name="minSegments">The minimum number of segments.</param> /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> @@ -285,7 +285,7 @@ namespace Jellyfin.Api.Controllers // Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token // since it gets disposed when ffmpeg exits var cancellationToken = cancellationTokenSource.Token; - using var state = await StreamingHelpers.GetStreamingState( + var state = await StreamingHelpers.GetStreamingState( streamingRequest, Request, _authContext, @@ -1414,7 +1414,8 @@ namespace Jellyfin.Api.Controllers state.RunTimeTicks ?? 0, state.Request.SegmentContainer ?? string.Empty, "hls1/main/", - Request.QueryString.ToString()); + Request.QueryString.ToString(), + EncodingHelper.IsCopyCodec(state.OutputVideoCodec)); var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request); return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8")); @@ -1431,7 +1432,7 @@ namespace Jellyfin.Api.Controllers var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - using var state = await StreamingHelpers.GetStreamingState( + var state = await StreamingHelpers.GetStreamingState( streamingRequest, Request, _authContext, @@ -1599,7 +1600,6 @@ namespace Jellyfin.Api.Controllers state.BaseRequest.BreakOnNonKeyFrames = false; } - var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions); var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); @@ -1608,12 +1608,15 @@ namespace Jellyfin.Api.Controllers var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); var outputTsArg = outputPrefix + "%d" + outputExtension; - var segmentFormat = outputExtension.TrimStart('.'); - if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) + var segmentFormat = string.Empty; + var segmentContainer = outputExtension.TrimStart('.'); + var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer); + + if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase)) { segmentFormat = "mpegts"; } - else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)) { var outputFmp4HeaderArg = OperatingSystem.IsWindows() switch { @@ -1627,7 +1630,8 @@ namespace Jellyfin.Api.Controllers } else { - _logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat); + _logger.LogError("Invalid HLS segment container: {SegmentContainer}, default to mpegts", segmentContainer); + segmentFormat = "mpegts"; } var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128 @@ -1647,7 +1651,7 @@ namespace Jellyfin.Api.Controllers CultureInfo.InvariantCulture, "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{12}\" -hls_playlist_type {11} -hls_list_size 0 -y \"{13}\"", inputModifier, - _encodingHelper.GetInputArgument(state, _encodingOptions), + _encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer), threads, mapArgs, GetVideoArguments(state, startNumber, isEventPlaylist), @@ -1708,20 +1712,30 @@ namespace Jellyfin.Api.Controllers return audioTranscodeParams; } + // flac and opus are experimental in mp4 muxer + var strictArgs = string.Empty; + + if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)) + { + strictArgs = " -strict -2"; + } + if (EncodingHelper.IsCopyCodec(audioCodec)) { var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); + var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs; if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) { - return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0" + bitStreamArgs; + return copyArgs + " -copypriorss:a:0 0"; } - return "-codec:a:0 copy -strict -2" + bitStreamArgs; + return copyArgs; } - var args = "-codec:a:0 " + audioCodec; + var args = "-codec:a:0 " + audioCodec + strictArgs; var channels = state.OutputAudioChannels; @@ -1770,13 +1784,25 @@ namespace Jellyfin.Api.Controllers var args = "-codec:v:0 " + codec; - // Prefer hvc1 to hev1. if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) { - args += " -tag:v:0 hvc1"; + if (EncodingHelper.IsCopyCodec(codec) + && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase))) + { + // Prefer dvh1 to dvhe + args += " -tag:v:0 dvh1 -strict -2"; + } + else + { + // Prefer hvc1 to hev1 + args += " -tag:v:0 hvc1"; + } } // if (state.EnableMpegtsM2TsMode) @@ -1806,7 +1832,7 @@ namespace Jellyfin.Api.Controllers // Set the key frame params for video encoding to match the hls segment time. args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber); - // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now. + // Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now. if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) { args += " -bf 0"; diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index a4f12666d..11808b1b8 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -52,9 +52,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); BaseItem? item = null; if (includeItemTypes.Length != 1 @@ -144,9 +144,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? isSeries, [FromQuery] bool? recursive) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); BaseItem? parentItem = null; if (includeItemTypes.Length == 1 diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 37e6ae184..e28a50750 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -95,7 +95,9 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null; + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var parentItem = _libraryManager.GetParentItem(parentId, userId); @@ -157,29 +159,26 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions() .AddClientFields(Request); - Genre item = new Genre(); - if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) + Genre? item; + if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase)) { - var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre); - - if (result != null) - { - item = result; - } + item = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre); } else { item = _libraryManager.GetGenre(genreName); } - if (userId.HasValue && !userId.Equals(Guid.Empty)) - { - var user = _userManager.GetUserById(userId.Value); + item ??= new Genre(); - return _dtoService.GetBaseItemDto(item, dtoOptions, user); + if (userId is null || userId.Value.Equals(default)) + { + return _dtoService.GetBaseItemDto(item, dtoOptions); } - return _dtoService.GetBaseItemDto(item, dtoOptions); + var user = _userManager.GetUserById(userId.Value); + + return _dtoService.GetBaseItemDto(item, dtoOptions, user); } private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 05d80ba35..6c7842c7b 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1724,6 +1724,11 @@ namespace Jellyfin.Api.Controllers [FromQuery, Range(0, 100)] int quality = 90) { var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + if (!brandingOptions.SplashscreenEnabled) + { + return NotFound(); + } + string splashscreenPath; if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation) @@ -1776,6 +1781,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Uploads a custom splashscreen. + /// The body is expected to the image contents base64 encoded. /// </summary> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <response code="204">Successfully uploaded new splashscreen.</response> @@ -1799,7 +1805,13 @@ namespace Jellyfin.Api.Controllers return BadRequest("Error reading mimetype from uploaded image"); } - var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType.Value)); + var extension = MimeTypes.ToExtension(mimeType.Value); + if (string.IsNullOrEmpty(extension)) + { + return BadRequest("Error converting mimetype to an image extension"); + } + + var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); brandingOptions.SplashscreenLocation = filePath; _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); @@ -1812,6 +1824,29 @@ namespace Jellyfin.Api.Controllers return NoContent(); } + /// <summary> + /// Delete a custom splashscreen. + /// </summary> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + /// <response code="204">Successfully deleted the custom splashscreen.</response> + /// <response code="403">User does not have permission to delete splashscreen..</response> + [HttpDelete("Branding/Splashscreen")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult DeleteCustomSplashscreen() + { + var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation) + && System.IO.File.Exists(brandingOptions.SplashscreenLocation)) + { + System.IO.File.Delete(brandingOptions.SplashscreenLocation); + brandingOptions.SplashscreenLocation = null; + _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); + } + + return NoContent(); + } + private static async Task<MemoryStream> GetMemoryStream(Stream inputStream) { using var reader = new StreamReader(inputStream); diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index e9d48b624..9abea5938 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -75,9 +75,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -111,9 +111,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var album = _libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -147,9 +147,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var playlist = (Playlist)_libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -182,9 +182,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -218,9 +218,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -254,9 +254,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -327,9 +327,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index 49865eb5e..9340737b5 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers /// Item Refresh Controller. /// </summary> [Route("Items")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.RequiresElevation)] public class ItemRefreshController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers [Description("Refreshes metadata for an item.")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult Post( + public ActionResult RefreshItem( [FromRoute, Required] Guid itemId, [FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None, [FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None, diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index dc7af0a20..4d09070db 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; @@ -9,6 +10,7 @@ using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -32,6 +34,7 @@ namespace Jellyfin.Api.Controllers private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly IDtoService _dtoService; + private readonly IAuthorizationContext _authContext; private readonly ILogger<ItemsController> _logger; private readonly ISessionManager _sessionManager; @@ -42,6 +45,7 @@ namespace Jellyfin.Api.Controllers /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> + /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> public ItemsController( @@ -49,6 +53,7 @@ namespace Jellyfin.Api.Controllers ILibraryManager libraryManager, ILocalizationManager localization, IDtoService dtoService, + IAuthorizationContext authContext, ILogger<ItemsController> logger, ISessionManager sessionManager) { @@ -56,6 +61,7 @@ namespace Jellyfin.Api.Controllers _libraryManager = libraryManager; _localization = localization; _dtoService = dtoService; + _authContext = authContext; _logger = logger; _sessionManager = sessionManager; } @@ -63,7 +69,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Gets items based on a query. /// </summary> - /// <param name="userId">The user id supplied as query parameter.</param> + /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param> /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="hasThemeSong">Optional filter by items with theme songs.</param> /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param> @@ -89,6 +95,11 @@ namespace Jellyfin.Api.Controllers /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> + /// <param name="isMovie">Optional filter for live tv movies.</param> + /// <param name="isSeries">Optional filter for live tv series.</param> + /// <param name="isNews">Optional filter for live tv news.</param> + /// <param name="isKids">Optional filter for live tv kids.</param> + /// <param name="isSports">Optional filter for live tv sports.</param> /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param> /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="limit">Optional. The maximum number of records to return.</param> @@ -146,15 +157,15 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns> [HttpGet("Items")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetItems( - [FromQuery] Guid userId, + public async Task<ActionResult<QueryResult<BaseItemDto>>> GetItems( + [FromQuery] Guid? userId, [FromQuery] string? maxOfficialRating, [FromQuery] bool? hasThemeSong, [FromQuery] bool? hasThemeVideo, [FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasTrailer, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] int? parentIndexNumber, [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, @@ -173,6 +184,11 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasImdbId, [FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTvdbId, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, @@ -228,7 +244,19 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - var user = userId == Guid.Empty ? null : _userManager.GetUserById(userId); + var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); + + // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method + var user = !auth.IsApiKey && userId.HasValue && !userId.Value.Equals(default) + ? _userManager.GetUserById(userId.Value) + : null; + + // beyond this point, we're either using an api key or we have a valid user + if (!auth.IsApiKey && user is null) + { + return BadRequest("userId is required"); + } + var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -260,30 +288,39 @@ namespace Jellyfin.Api.Controllers includeItemTypes = new[] { BaseItemKind.Playlist }; } - var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels); + var enabledChannels = auth.IsApiKey + ? Array.Empty<Guid>() + : user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels); - bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1 + // api keys are always enabled for all folders + bool isInEnabledFolder = auth.IsApiKey + || Array.IndexOf(user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1 // Assume all folders inside an EnabledChannel are enabled || Array.IndexOf(enabledChannels, item.Id) != -1 // Assume all items inside an EnabledChannel are enabled || Array.IndexOf(enabledChannels, item.ChannelId) != -1; - var collectionFolders = _libraryManager.GetCollectionFolders(item); - foreach (var collectionFolder in collectionFolders) + if (!isInEnabledFolder) { - if (user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id)) + var collectionFolders = _libraryManager.GetCollectionFolders(item); + foreach (var collectionFolder in collectionFolders) { - isInEnabledFolder = true; + // api keys never enter this block, so user is never null + if (user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id)) + { + isInEnabledFolder = true; + } } } + // api keys are always enabled for all folders, so user is never null if (item is not UserRootFolder && !isInEnabledFolder - && !user.HasPermission(PermissionKind.EnableAllFolders) + && !user!.HasPermission(PermissionKind.EnableAllFolders) && !user.HasPermission(PermissionKind.EnableAllChannels) && !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase)) { - _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name); + _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user.Username, item.Name); return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); } @@ -316,6 +353,11 @@ namespace Jellyfin.Api.Controllers Is3D = is3D, HasTvdbId = hasTvdbId, HasTmdbId = hasTmdbId, + IsMovie = isMovie, + IsSeries = isSeries, + IsNews = isNews, + IsKids = isKids, + IsSports = isSports, HasOverview = hasOverview, HasOfficialRating = hasOfficialRating, HasParentalRating = hasParentalRating, @@ -515,8 +557,8 @@ namespace Jellyfin.Api.Controllers /// <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> /// <param name="is4K">Optional filter by items that are 4K or not.</param> - /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param> - /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param> + /// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param> + /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param> /// <param name="isMissing">Optional filter by items that are missing episodes or not.</param> /// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param> /// <param name="minCommunityRating">Optional filter by minimum community rating.</param> @@ -529,42 +571,47 @@ namespace Jellyfin.Api.Controllers /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> - /// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param> + /// <param name="isMovie">Optional filter for live tv movies.</param> + /// <param name="isSeries">Optional filter for live tv series.</param> + /// <param name="isNews">Optional filter for live tv news.</param> + /// <param name="isKids">Optional filter for live tv kids.</param> + /// <param name="isSports">Optional filter for live tv sports.</param> + /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param> /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param> /// <param name="searchTerm">Optional. Filter based on a search term.</param> /// <param name="sortOrder">Sort Order - Ascending,Descending.</param> /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> - /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> - /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param> - /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param> - /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param> + /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> + /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param> + /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param> + /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param> /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> /// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param> /// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param> - /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</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="isPlayed">Optional filter by items that are played, or not.</param> - /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param> - /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param> - /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param> - /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param> + /// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param> + /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param> + /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param> + /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param> /// <param name="enableUserData">Optional, include user data.</param> /// <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> /// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param> /// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param> /// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param> - /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param> - /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param> - /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param> + /// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param> + /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param> + /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param> /// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param> /// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param> /// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param> - /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param> - /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param> + /// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param> + /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param> /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param> - /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param> + /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param> /// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="isLocked">Optional filter by items that are locked.</param> /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param> @@ -575,18 +622,18 @@ namespace Jellyfin.Api.Controllers /// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param> /// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param> /// <param name="is3D">Optional filter by items that are 3D, or not.</param> - /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param> + /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param> /// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param> /// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param> - /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param> - /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param> + /// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param> + /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param> /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param> /// <param name="enableImages">Optional, include image information in output.</param> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns> [HttpGet("Users/{userId}/Items")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId( + public Task<ActionResult<QueryResult<BaseItemDto>>> GetItemsByUserId( [FromRoute] Guid userId, [FromQuery] string? maxOfficialRating, [FromQuery] bool? hasThemeSong, @@ -594,7 +641,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasTrailer, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] int? parentIndexNumber, [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, @@ -613,6 +660,11 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasImdbId, [FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTvdbId, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, @@ -695,6 +747,11 @@ namespace Jellyfin.Api.Controllers hasImdbId, hasTmdbId, hasTvdbId, + isMovie, + isSeries, + isNews, + isKids, + isSports, excludeItemIds, startIndex, limit, @@ -799,7 +856,7 @@ namespace Jellyfin.Api.Controllers var ancestorIds = Array.Empty<Guid>(); var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes); - if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0) + if (parentIdGuid.Equals(default) && excludeFolderIds.Length > 0) { ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) @@ -812,7 +869,7 @@ namespace Jellyfin.Api.Controllers if (excludeActiveSessions) { excludeItemIds = _sessionManager.Sessions - .Where(s => s.UserId == userId && s.NowPlayingItem != null) + .Where(s => s.UserId.Equals(userId) && s.NowPlayingItem != null) .Select(s => s.NowPlayingItem.Id) .ToArray(); } diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index c65462ab5..4cc17dd0f 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -149,14 +149,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - - var item = itemId.Equals(Guid.Empty) - ? (!userId.Equals(Guid.Empty) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.RootFolder) + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); if (item == null) @@ -215,14 +215,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - - var item = itemId.Equals(Guid.Empty) - ? (!userId.Equals(Guid.Empty) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.RootFolder) + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); if (item == null) @@ -407,9 +407,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? userId, [FromQuery] bool? isFavorite) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var counts = new ItemCounts { @@ -449,9 +449,9 @@ namespace Jellyfin.Api.Controllers var baseItemDtos = new List<BaseItemDto>(); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions().AddClientFields(Request); BaseItem? parent = item.GetParent(); @@ -689,10 +689,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { - var item = itemId.Equals(Guid.Empty) - ? (!userId.Equals(Guid.Empty) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.RootFolder) + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); if (item is Episode || (item is IItemByName && item is not MusicArtist)) @@ -700,9 +700,9 @@ namespace Jellyfin.Api.Controllers return new QueryResult<BaseItemDto>(); } - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request); diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 484b0a974..05340099b 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -180,9 +180,9 @@ namespace Jellyfin.Api.Controllers dtoOptions, CancellationToken.None); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var fieldsList = dtoOptions.Fields.ToList(); fieldsList.Remove(ItemFields.CanDelete); @@ -211,10 +211,10 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - var item = channelId.Equals(Guid.Empty) + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var item = channelId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(channelId); @@ -382,9 +382,9 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var folders = _liveTvManager.GetRecordingFolders(user); var returnArray = _dtoService.GetBaseItemDtos(folders, new DtoOptions(), user); @@ -404,10 +404,10 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - var item = recordingId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); var dtoOptions = new DtoOptions() .AddClientFields(Request); @@ -561,9 +561,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool enableTotalRecordCount = true) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var query = new InternalItemsQuery(user) { @@ -588,7 +588,7 @@ namespace Jellyfin.Api.Controllers GenreIds = genreIds }; - if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty)) + if (librarySeriesId.HasValue && !librarySeriesId.Equals(default)) { query.IsSeries = true; @@ -617,7 +617,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body) { - var user = body.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(body.UserId); + var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId); var query = new InternalItemsQuery(user) { @@ -642,7 +642,7 @@ namespace Jellyfin.Api.Controllers GenreIds = body.GenreIds }; - if (!body.LibrarySeriesId.Equals(Guid.Empty)) + if (!body.LibrarySeriesId.Equals(default)) { query.IsSeries = true; @@ -700,9 +700,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var query = new InternalItemsQuery(user) { @@ -738,9 +738,9 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string programId, [FromQuery] Guid? userId) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index b422eb78c..75df18204 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false); var profile = playbackInfoDto?.DeviceProfile; - _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile); + _logger.LogDebug("GetPostedPlaybackInfo profile: {@Profile}", profile); if (profile == null) { @@ -225,14 +225,6 @@ namespace Jellyfin.Api.Controllers } } - if (info.MediaSources != null) - { - foreach (var mediaSource in info.MediaSources) - { - _mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video); - } - } - return info; } diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index db72ff2f8..420dd9923 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -68,9 +68,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int categoryLimit = 5, [FromQuery] int itemLimit = 8) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request); diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index c4c03aa4f..0499b2985 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -95,7 +95,9 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null; + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var parentItem = _libraryManager.GetParentItem(parentId, userId); @@ -156,7 +158,7 @@ namespace Jellyfin.Api.Controllers item = _libraryManager.GetMusicGenre(genreName); } - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 5dd49ef2f..9690aead0 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers /// <response code="204">Package repositories saved.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Repositories")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos) { diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index ffc748a6e..be4b9eded 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -82,12 +82,9 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = null; - - if (userId.HasValue && !userId.Equals(Guid.Empty)) - { - user = _userManager.GetUserById(userId.Value); - } + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery( @@ -127,7 +124,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index c18f1b427..ad85f2fb2 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -181,7 +181,9 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; + var user = userId.Equals(default) + ? null + : _userManager.GetUserById(userId); var items = playlist.GetManageableItems().ToArray(); diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 6fcd2ae40..aeed0c0d6 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -6,6 +6,7 @@ using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -59,9 +60,9 @@ namespace Jellyfin.Api.Controllers /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param> /// <param name="searchTerm">The search term to filter on.</param> - /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimeted.</param> - /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimeted.</param> - /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimeted.</param> + /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimited.</param> + /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimited.</param> + /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimited.</param> /// <param name="parentId">If specified, only children of the parent are returned.</param> /// <param name="isMovie">Optional filter for movies.</param> /// <param name="isSeries">Optional filter for series.</param> @@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Description("Gets search hints based on a search term")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<SearchHintResult> Get( + public ActionResult<SearchHintResult> GetSearchHints( [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] Guid? userId, @@ -139,7 +140,7 @@ namespace Jellyfin.Api.Controllers IndexNumber = item.IndexNumber, ParentIndexNumber = item.ParentIndexNumber, Id = item.Id, - Type = item.GetClientTypeName(), + Type = item.GetBaseItemKind(), MediaType = item.MediaType, MatchedTerm = hintInfo.MatchedTerm, RunTimeTicks = item.RunTimeTicks, @@ -148,8 +149,10 @@ namespace Jellyfin.Api.Controllers EndDate = item.EndDate }; - // legacy +#pragma warning disable CS0618 + // Kept for compatibility with older clients result.ItemId = result.Id; +#pragma warning restore CS0618 if (item.IsFolder) { @@ -187,7 +190,7 @@ namespace Jellyfin.Api.Controllers result.AlbumArtist = album.AlbumArtist; break; case Audio song: - result.AlbumArtist = song.AlbumArtists?[0]; + result.AlbumArtist = song.AlbumArtists?.FirstOrDefault(); result.Artists = song.Artists; MusicAlbum musicAlbum = song.AlbumEntity; @@ -205,7 +208,7 @@ namespace Jellyfin.Api.Controllers break; } - if (!item.ChannelId.Equals(Guid.Empty)) + if (!item.ChannelId.Equals(default)) { var channel = _libraryManager.GetItemById(item.ChannelId); result.ChannelName = channel?.Name; diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index a6bbd40cc..860bccb9b 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); } - if (controllableByUserId.HasValue && !controllableByUserId.Equals(Guid.Empty)) + if (controllableByUserId.HasValue && !controllableByUserId.Equals(default)) { result = result.Where(i => i.SupportsRemoteControl); @@ -82,12 +82,12 @@ namespace Jellyfin.Api.Controllers if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) { - result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(controllableByUserId.Value)); + result = result.Where(i => i.UserId.Equals(default) || i.ContainsUser(controllableByUserId.Value)); } if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) { - result = result.Where(i => !i.UserId.Equals(Guid.Empty)); + result = result.Where(i => !i.UserId.Equals(default)); } if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 4422ef32c..053c7baaa 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -91,7 +91,9 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null; + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var parentItem = _libraryManager.GetParentItem(parentId, userId); @@ -141,7 +143,7 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions().AddClientFields(Request); var item = _libraryManager.GetStudio(name); - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 73be26bb2..e9c46dcf3 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -63,7 +63,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] bool enableTotalRecordCount = false) { - var user = !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId) : null; + var user = userId.Equals(default) + ? null + : _userManager.GetUserById(userId); var dtoOptions = new DtoOptions().AddClientFields(Request); var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 5cb7468b2..cf812fa23 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; @@ -31,7 +32,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Finds movies and trailers similar to a given trailer. /// </summary> - /// <param name="userId">The user id.</param> + /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param> /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="hasThemeSong">Optional filter by items with theme songs.</param> /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param> @@ -57,6 +58,11 @@ namespace Jellyfin.Api.Controllers /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param> /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param> /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param> + /// <param name="isMovie">Optional filter for live tv movies.</param> + /// <param name="isSeries">Optional filter for live tv series.</param> + /// <param name="isNews">Optional filter for live tv news.</param> + /// <param name="isKids">Optional filter for live tv kids.</param> + /// <param name="isSports">Optional filter for live tv sports.</param> /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param> /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="limit">Optional. The maximum number of records to return.</param> @@ -113,15 +119,15 @@ namespace Jellyfin.Api.Controllers /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns> [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetTrailers( - [FromQuery] Guid userId, + public Task<ActionResult<QueryResult<BaseItemDto>>> GetTrailers( + [FromQuery] Guid? userId, [FromQuery] string? maxOfficialRating, [FromQuery] bool? hasThemeSong, [FromQuery] bool? hasThemeVideo, [FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasTrailer, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] int? parentIndexNumber, [FromQuery] bool? hasParentalRating, [FromQuery] bool? isHd, @@ -140,6 +146,11 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasImdbId, [FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTvdbId, + [FromQuery] bool? isMovie, + [FromQuery] bool? isSeries, + [FromQuery] bool? isNews, + [FromQuery] bool? isKids, + [FromQuery] bool? isSports, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, @@ -224,6 +235,11 @@ namespace Jellyfin.Api.Controllers hasImdbId, hasTmdbId, hasTvdbId, + isMovie, + isSeries, + isNews, + isKids, + isSports, excludeItemIds, startIndex, limit, diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 636130543..e39d05a6f 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -77,7 +77,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? seriesId, + [FromQuery] Guid? seriesId, [FromQuery] Guid? parentId, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, @@ -107,9 +107,9 @@ namespace Jellyfin.Api.Controllers }, options); - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user); @@ -145,9 +145,9 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); var minPremiereDate = DateTime.UtcNow.Date.AddDays(-1); @@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? season, [FromQuery] Guid? seasonId, [FromQuery] bool? isMissing, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] Guid? startItemId, [FromQuery] int? startIndex, [FromQuery] int? limit, @@ -216,9 +216,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableUserData, [FromQuery] string? sortBy) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); List<BaseItem> episodes; @@ -278,9 +278,9 @@ namespace Jellyfin.Api.Controllers } // This must be the last filter - if (!string.IsNullOrEmpty(adjacentTo)) + if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default)) { - episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo).ToList(); + episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList(); } if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) @@ -326,15 +326,15 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? isSpecialSeason, [FromQuery] bool? isMissing, - [FromQuery] string? adjacentTo, + [FromQuery] Guid? adjacentTo, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); if (_libraryManager.GetItemById(seriesId) is not Series series) { diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index bc9527a0b..6fcafd426 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -16,6 +16,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -223,7 +224,7 @@ namespace Jellyfin.Api.Controllers DeInterlace = false, RequireNonAnamorphic = false, EnableMpegtsM2TsMode = false, - TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(), Context = EncodingContext.Static, StreamOptions = new Dictionary<string, string>(), EnableAdaptiveBitrateStreaming = true @@ -254,7 +255,7 @@ namespace Jellyfin.Api.Controllers CopyTimestamps = true, StartTimeTicks = startTimeTicks, SubtitleMethod = SubtitleDeliveryMethod.Embed, - TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(), Context = EncodingContext.Static }; diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 4263d4fe5..d1109bebc 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -282,16 +282,19 @@ namespace Jellyfin.Api.Controllers } else { - var success = await _userManager.AuthenticateUser( - user.Username, - request.CurrentPw, - request.CurrentPw, - HttpContext.GetNormalizedRemoteIp().ToString(), - false).ConfigureAwait(false); - - if (success == null) + if (!HttpContext.User.IsInRole(UserRoles.Administrator)) { - return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered."); + var success = await _userManager.AuthenticateUser( + user.Username, + request.CurrentPw, + request.CurrentPw, + HttpContext.GetNormalizedRemoteIp().ToString(), + false).ConfigureAwait(false); + + if (success == null) + { + return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered."); + } } await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false); @@ -499,7 +502,7 @@ namespace Jellyfin.Api.Controllers if (isLocal) { - _logger.LogWarning("Password reset proccess initiated from outside the local network with IP: {IP}", ip); + _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip); } var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false); @@ -534,7 +537,7 @@ namespace Jellyfin.Api.Controllers public ActionResult<UserDto> GetCurrentUser() { var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); - if (userId == null) + if (userId is null) { return BadRequest(); } diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 008d2f176..e45f9b58c 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -116,7 +116,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -197,7 +197,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -227,7 +227,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -347,7 +347,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); // Get the user data for this item var data = _userDataRepository.GetUserData(user, item); @@ -370,7 +370,7 @@ namespace Jellyfin.Api.Controllers { var user = _userManager.GetUserById(userId); - var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); + var item = itemId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); // Get the user data for this item var data = _userDataRepository.GetUserData(user, item); diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 04171da8a..5cc8c906f 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; @@ -15,6 +16,7 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -24,6 +26,7 @@ namespace Jellyfin.Api.Controllers /// User views controller. /// </summary> [Route("")] + [Authorize(Policy = Policies.DefaultAuthorization)] public class UserViewsController : BaseJellyfinApiController { private readonly IUserManager _userManager; @@ -65,7 +68,7 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="OkResult"/> containing the user views.</returns> [HttpGet("Users/{userId}/Views")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<QueryResult<BaseItemDto>>> GetUserViews( + public QueryResult<BaseItemDto> GetUserViews( [FromRoute, Required] Guid userId, [FromQuery] bool? includeExternalContent, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews, @@ -87,12 +90,6 @@ namespace Jellyfin.Api.Controllers query.PresetViews = presetViews; } - var app = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Client ?? string.Empty; - if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1) - { - query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows }; - } - var folders = _userViewManager.GetUserViews(query); var dtoOptions = new DtoOptions().AddClientFields(Request); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 44263fd98..4e2895934 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -109,14 +109,14 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - - var item = itemId.Equals(Guid.Empty) - ? (!userId.Equals(Guid.Empty) - ? _libraryManager.GetUserRootFolder() - : _libraryManager.RootFolder) + var user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); + + var item = itemId.Equals(default) + ? (userId is null || userId.Value.Equals(default) + ? _libraryManager.RootFolder + : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); var dtoOptions = new DtoOptions(); @@ -221,7 +221,7 @@ namespace Jellyfin.Api.Controllers var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList(); - foreach (var item in items.Where(i => i.Id != primaryVersion.Id)) + foreach (var item in items.Where(i => !i.Id.Equals(primaryVersion.Id))) { item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture)); @@ -427,7 +427,7 @@ namespace Jellyfin.Api.Controllers StreamOptions = streamOptions }; - using var state = await StreamingHelpers.GetStreamingState( + var state = await StreamingHelpers.GetStreamingState( streamingRequest, Request, _authContext, diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index bac77d43b..7c02e2550 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -90,16 +90,11 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = null; + User? user = userId is null || userId.Value.Equals(default) + ? null + : _userManager.GetUserById(userId.Value); BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(Guid.Empty)) - { - user = _userManager.GetUserById(userId.Value); - } - - IList<BaseItem> items; - var query = new InternalItemsQuery(user) { ExcludeItemTypes = excludeItemTypes, @@ -110,17 +105,18 @@ namespace Jellyfin.Api.Controllers bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes); + IList<BaseItem> items; if (parentItem.IsFolder) { var folder = (Folder)parentItem; - if (!userId.Equals(Guid.Empty)) + if (userId.Equals(default)) { - items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList(); + items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList(); } else { - items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList(); + items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList(); } } else @@ -185,7 +181,7 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions() .AddClientFields(Request); - if (userId.HasValue && !userId.Equals(Guid.Empty)) + if (userId.HasValue && !userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); |
