diff options
Diffstat (limited to 'Jellyfin.Api/Controllers')
28 files changed, 498 insertions, 594 deletions
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index 8e0332d3e..593846adc 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -38,11 +38,7 @@ namespace Jellyfin.Api.Controllers { var keys = await _authenticationManager.GetApiKeys(); - return new QueryResult<AuthenticationInfo> - { - Items = keys, - TotalRecordCount = keys.Count - }; + return new QueryResult<AuthenticationInfo>(keys); } /// <summary> diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 3df975563..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); } @@ -243,11 +243,10 @@ namespace Jellyfin.Api.Controllers return dto; }); - return new QueryResult<BaseItemDto> - { - Items = dtos.ToArray(), - TotalRecordCount = result.TotalRecordCount - }; + return new QueryResult<BaseItemDto>( + query.StartIndex, + result.TotalRecordCount, + dtos.ToArray()); } /// <summary> @@ -330,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); } @@ -447,11 +446,10 @@ namespace Jellyfin.Api.Controllers return dto; }); - return new QueryResult<BaseItemDto> - { - Items = dtos.ToArray(), - TotalRecordCount = result.TotalRecordCount - }; + return new QueryResult<BaseItemDto>( + query.StartIndex, + result.TotalRecordCount, + dtos.ToArray()); } /// <summary> @@ -469,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/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/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 0b2604640..27eb22339 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -126,9 +126,11 @@ namespace Jellyfin.Api.Controllers HomeSectionType.SmallLibraryTiles, HomeSectionType.Resume, HomeSectionType.ResumeAudio, + HomeSectionType.ResumeBook, HomeSectionType.LiveTv, HomeSectionType.NextUp, - HomeSectionType.LatestMedia, HomeSectionType.None, + HomeSectionType.LatestMedia, + HomeSectionType.None, }; if (!Guid.TryParse(displayPreferencesId, out var itemId)) @@ -182,7 +184,7 @@ namespace Jellyfin.Api.Controllers var order = int.Parse(key.AsSpan().Slice("homesection".Length)); if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type)) { - type = order < 7 ? defaults[order] : HomeSectionType.None; + type = order < 8 ? defaults[order] : HomeSectionType.None; } displayPreferences.CustomPrefs.Remove(key); diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 6ef3a2ff9..f8e8d975c 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -13,6 +13,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.MediaEncoding.Hls.Playlist; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; @@ -28,7 +29,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Controllers { @@ -55,6 +55,7 @@ namespace Jellyfin.Api.Controllers private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ILogger<DynamicHlsController> _logger; private readonly EncodingHelper _encodingHelper; + private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator; private readonly DynamicHlsHelper _dynamicHlsHelper; private readonly EncodingOptions _encodingOptions; @@ -74,6 +75,7 @@ namespace Jellyfin.Api.Controllers /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param> /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> + /// <param name="dynamicHlsPlaylistGenerator">Instance of <see cref="IDynamicHlsPlaylistGenerator"/>.</param> public DynamicHlsController( ILibraryManager libraryManager, IUserManager userManager, @@ -87,7 +89,8 @@ namespace Jellyfin.Api.Controllers TranscodingJobHelper transcodingJobHelper, ILogger<DynamicHlsController> logger, DynamicHlsHelper dynamicHlsHelper, - EncodingHelper encodingHelper) + EncodingHelper encodingHelper, + IDynamicHlsPlaylistGenerator dynamicHlsPlaylistGenerator) { _libraryManager = libraryManager; _userManager = userManager; @@ -102,6 +105,7 @@ namespace Jellyfin.Api.Controllers _logger = logger; _dynamicHlsHelper = dynamicHlsHelper; _encodingHelper = encodingHelper; + _dynamicHlsPlaylistGenerator = dynamicHlsPlaylistGenerator; _encodingOptions = serverConfigurationManager.GetEncodingOptions(); } @@ -385,6 +389,8 @@ namespace Jellyfin.Api.Controllers /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param> /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param> /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param> + /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param> + /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param> /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param> /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param> /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param> @@ -441,6 +447,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] long? startTimeTicks, [FromQuery] int? width, [FromQuery] int? height, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, [FromQuery] SubtitleDeliveryMethod? subtitleMethod, @@ -493,6 +501,8 @@ namespace Jellyfin.Api.Controllers StartTimeTicks = startTimeTicks, Width = width, Height = height, + MaxWidth = maxWidth, + MaxHeight = maxHeight, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, @@ -717,6 +727,8 @@ namespace Jellyfin.Api.Controllers /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param> /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param> /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param> + /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param> + /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param> /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param> /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param> /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param> @@ -771,6 +783,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] long? startTimeTicks, [FromQuery] int? width, [FromQuery] int? height, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, [FromQuery] SubtitleDeliveryMethod? subtitleMethod, @@ -823,6 +837,8 @@ namespace Jellyfin.Api.Controllers StartTimeTicks = startTimeTicks, Width = width, Height = height, + MaxWidth = maxWidth, + MaxHeight = maxHeight, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, @@ -844,7 +860,7 @@ namespace Jellyfin.Api.Controllers StreamOptions = streamOptions }; - return await GetVariantPlaylistInternal(streamingRequest, "main", cancellationTokenSource) + return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) .ConfigureAwait(false); } @@ -1009,7 +1025,7 @@ namespace Jellyfin.Api.Controllers StreamOptions = streamOptions }; - return await GetVariantPlaylistInternal(streamingRequest, "main", cancellationTokenSource) + return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource) .ConfigureAwait(false); } @@ -1020,13 +1036,15 @@ namespace Jellyfin.Api.Controllers /// <param name="playlistId">The playlist id.</param> /// <param name="segmentId">The segment id.</param> /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param> + /// <param name="runtimeTicks">The position of the requested segment in ticks.</param> + /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</param> /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param> /// <param name="params">The streaming parameters.</param> /// <param name="tag">The tag.</param> /// <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 desired 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> @@ -1048,6 +1066,8 @@ namespace Jellyfin.Api.Controllers /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param> /// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param> /// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param> + /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param> + /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param> /// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param> /// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param> /// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param> @@ -1078,6 +1098,8 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, [FromRoute, Required] string container, + [FromQuery, Required] long runtimeTicks, + [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -1106,6 +1128,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] long? startTimeTicks, [FromQuery] int? width, [FromQuery] int? height, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, [FromQuery] int? videoBitRate, [FromQuery] int? subtitleStreamIndex, [FromQuery] SubtitleDeliveryMethod? subtitleMethod, @@ -1129,6 +1153,8 @@ namespace Jellyfin.Api.Controllers var streamingRequest = new VideoRequestDto { Id = itemId, + CurrentRuntimeTicks = runtimeTicks, + ActualSegmentLengthTicks = actualSegmentLengthTicks, Container = container, Static = @static ?? false, Params = @params, @@ -1158,6 +1184,8 @@ namespace Jellyfin.Api.Controllers StartTimeTicks = startTimeTicks, Width = width, Height = height, + MaxWidth = maxWidth, + MaxHeight = maxHeight, VideoBitRate = videoBitRate, SubtitleStreamIndex = subtitleStreamIndex, SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, @@ -1190,6 +1218,8 @@ namespace Jellyfin.Api.Controllers /// <param name="playlistId">The playlist id.</param> /// <param name="segmentId">The segment id.</param> /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param> + /// <param name="runtimeTicks">The position of the requested segment in ticks.</param> + /// <param name="actualSegmentLengthTicks">The length of the requested segment in ticks.</param> /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param> /// <param name="params">The streaming parameters.</param> /// <param name="tag">The tag.</param> @@ -1249,6 +1279,8 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, [FromRoute, Required] string container, + [FromQuery, Required] long runtimeTicks, + [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -1302,6 +1334,8 @@ namespace Jellyfin.Api.Controllers { Id = itemId, Container = container, + CurrentRuntimeTicks = runtimeTicks, + ActualSegmentLengthTicks = actualSegmentLengthTicks, Static = @static ?? false, Params = @params, Tag = tag, @@ -1355,7 +1389,7 @@ namespace Jellyfin.Api.Controllers .ConfigureAwait(false); } - private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource) + private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, CancellationTokenSource cancellationTokenSource) { using var state = await StreamingHelpers.GetStreamingState( streamingRequest, @@ -1374,60 +1408,16 @@ namespace Jellyfin.Api.Controllers cancellationTokenSource.Token) .ConfigureAwait(false); - Response.Headers.Add(HeaderNames.Expires, "0"); - - var segmentLengths = GetSegmentLengths(state); - - var segmentContainer = state.Request.SegmentContainer ?? "ts"; - - // http://ffmpeg.org/ffmpeg-all.html#toc-hls-2 - var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase); - var hlsVersion = isHlsInFmp4 ? "7" : "3"; - - var builder = new StringBuilder(128); - - builder.AppendLine("#EXTM3U") - .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") - .Append("#EXT-X-VERSION:") - .Append(hlsVersion) - .AppendLine() - .Append("#EXT-X-TARGETDURATION:") - .Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength)) - .AppendLine() - .AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); - - var index = 0; - var segmentExtension = EncodingHelper.GetSegmentFileExtension(streamingRequest.SegmentContainer); - var queryString = Request.QueryString; - - if (isHlsInFmp4) - { - builder.Append("#EXT-X-MAP:URI=\"") - .Append("hls1/") - .Append(name) - .Append("/-1") - .Append(segmentExtension) - .Append(queryString) - .Append('"') - .AppendLine(); - } - - foreach (var length in segmentLengths) - { - builder.Append("#EXTINF:") - .Append(length.ToString("0.0000", CultureInfo.InvariantCulture)) - .AppendLine(", nodesc") - .Append("hls1/") - .Append(name) - .Append('/') - .Append(index++) - .Append(segmentExtension) - .Append(queryString) - .AppendLine(); - } + var request = new CreateMainPlaylistRequest( + state.MediaPath, + state.SegmentLength * 1000, + state.RunTimeTicks ?? 0, + state.Request.SegmentContainer ?? string.Empty, + "hls1/main/", + Request.QueryString.ToString()); + var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request); - builder.AppendLine("#EXT-X-ENDLIST"); - return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); + return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8")); } private async Task<ActionResult> GetDynamicSegment(StreamingRequestDto streamingRequest, int segmentId) @@ -1528,7 +1518,7 @@ namespace Jellyfin.Api.Controllers DeleteLastFile(playlistPath, segmentExtension, 0); } - streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId); + streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks; state.WaitForPath = segmentPath; job = await _transcodingJobHelper.StartFfMpeg( @@ -1609,7 +1599,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)); @@ -1618,12 +1607,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 { @@ -1637,7 +1629,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 @@ -1657,7 +1650,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), @@ -1879,7 +1872,7 @@ namespace Jellyfin.Api.Controllers { // Transcoding job is over, so assume all existing files are ready _logger.LogDebug("serving up {0} as transcode is over", segmentPath); - return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob); + return GetSegmentResult(state, segmentPath, transcodingJob); } var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); @@ -1888,7 +1881,7 @@ namespace Jellyfin.Api.Controllers if (segmentIndex < currentTranscodingIndex) { _logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex); - return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob); + return GetSegmentResult(state, segmentPath, transcodingJob); } } @@ -1903,8 +1896,8 @@ namespace Jellyfin.Api.Controllers { if (transcodingJob.HasExited || System.IO.File.Exists(nextSegmentPath)) { - _logger.LogDebug("serving up {0} as it deemed ready", segmentPath); - return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob); + _logger.LogDebug("Serving up {SegmentPath} as it deemed ready", segmentPath); + return GetSegmentResult(state, segmentPath, transcodingJob); } } else @@ -1935,16 +1928,16 @@ namespace Jellyfin.Api.Controllers _logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath); } - return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob); + return GetSegmentResult(state, segmentPath, transcodingJob); } - private ActionResult GetSegmentResult(StreamState state, string segmentPath, int index, TranscodingJobDto? transcodingJob) + private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJobDto? transcodingJob) { - var segmentEndingPositionTicks = GetEndPositionTicks(state, index); + var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks; Response.OnCompleted(() => { - _logger.LogDebug("finished serving {0}", segmentPath); + _logger.LogDebug("Finished serving {SegmentPath}", segmentPath); if (transcodingJob != null) { transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); @@ -1954,30 +1947,7 @@ namespace Jellyfin.Api.Controllers return Task.CompletedTask; }); - return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath), false, HttpContext); - } - - private long GetEndPositionTicks(StreamState state, int requestedIndex) - { - double startSeconds = 0; - var lengths = GetSegmentLengths(state); - - if (requestedIndex >= lengths.Length) - { - var msg = string.Format( - CultureInfo.InvariantCulture, - "Invalid segment index requested: {0} - Segment count: {1}", - requestedIndex, - lengths.Length); - throw new ArgumentException(msg); - } - - for (var i = 0; i <= requestedIndex; i++) - { - startSeconds += lengths[i]; - } - - return TimeSpan.FromSeconds(startSeconds).Ticks; + return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)); } private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) @@ -2058,29 +2028,5 @@ namespace Jellyfin.Api.Controllers _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); } } - - private long GetStartPositionTicks(StreamState state, int requestedIndex) - { - double startSeconds = 0; - var lengths = GetSegmentLengths(state); - - if (requestedIndex >= lengths.Length) - { - var msg = string.Format( - CultureInfo.InvariantCulture, - "Invalid segment index requested: {0} - Segment count: {1}", - requestedIndex, - lengths.Length); - throw new ArgumentException(msg); - } - - for (var i = 0; i < requestedIndex; i++) - { - startSeconds += lengths[i]; - } - - var position = TimeSpan.FromSeconds(startSeconds).Ticks; - return position; - } } } 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/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 7325dca0a..78634f0bf 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers return BadRequest("Invalid segment."); } - return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file), false, HttpContext); + return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)); } /// <summary> @@ -186,7 +186,7 @@ namespace Jellyfin.Api.Controllers return Task.CompletedTask; }); - return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path), false, HttpContext); + return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)); } } } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index e72589cfa..05d80ba35 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -11,12 +12,14 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Branding; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -44,6 +47,8 @@ namespace Jellyfin.Api.Controllers private readonly IAuthorizationContext _authContext; private readonly ILogger<ImageController> _logger; private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IApplicationPaths _appPaths; + private readonly IImageEncoder _imageEncoder; /// <summary> /// Initializes a new instance of the <see cref="ImageController"/> class. @@ -56,6 +61,8 @@ namespace Jellyfin.Api.Controllers /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{ImageController}"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> + /// <param name="imageEncoder">Instance of the <see cref="IImageEncoder"/> interface.</param> public ImageController( IUserManager userManager, ILibraryManager libraryManager, @@ -64,7 +71,9 @@ namespace Jellyfin.Api.Controllers IFileSystem fileSystem, IAuthorizationContext authContext, ILogger<ImageController> logger, - IServerConfigurationManager serverConfigurationManager) + IServerConfigurationManager serverConfigurationManager, + IApplicationPaths appPaths, + IImageEncoder imageEncoder) { _userManager = userManager; _libraryManager = libraryManager; @@ -74,6 +83,8 @@ namespace Jellyfin.Api.Controllers _authContext = authContext; _logger = logger; _serverConfigurationManager = serverConfigurationManager; + _appPaths = appPaths; + _imageEncoder = imageEncoder; } /// <summary> @@ -559,8 +570,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -643,8 +653,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -727,8 +736,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -811,8 +819,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -895,8 +902,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -979,8 +985,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1063,8 +1068,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1147,8 +1151,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1231,8 +1234,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1315,8 +1317,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1399,8 +1400,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1483,8 +1483,7 @@ namespace Jellyfin.Api.Controllers blur, backgroundColor, foregroundLayer, - item, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + item) .ConfigureAwait(false); } @@ -1585,7 +1584,6 @@ namespace Jellyfin.Api.Controllers backgroundColor, foregroundLayer, null, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase), info) .ConfigureAwait(false); } @@ -1687,11 +1685,133 @@ namespace Jellyfin.Api.Controllers backgroundColor, foregroundLayer, null, - Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase), info) .ConfigureAwait(false); } + /// <summary> + /// Generates or gets the splashscreen. + /// </summary> + /// <param name="tag">Supply the cache tag from the item object to receive strong caching headers.</param> + /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param> + /// <param name="maxWidth">The maximum image width to return.</param> + /// <param name="maxHeight">The maximum image height to return.</param> + /// <param name="width">The fixed image width to return.</param> + /// <param name="height">The fixed image height to return.</param> + /// <param name="fillWidth">Width of box to fill.</param> + /// <param name="fillHeight">Height of box to fill.</param> + /// <param name="blur">Blur image.</param> + /// <param name="backgroundColor">Apply a background color for transparent images.</param> + /// <param name="foregroundLayer">Apply a foreground layer on top of the image.</param> + /// <param name="quality">Quality setting, from 0-100.</param> + /// <response code="200">Splashscreen returned successfully.</response> + /// <returns>The splashscreen.</returns> + [HttpGet("Branding/Splashscreen")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesImageFile] + public async Task<ActionResult> GetSplashscreen( + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? fillWidth, + [FromQuery] int? fillHeight, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer, + [FromQuery, Range(0, 100)] int quality = 90) + { + var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + string splashscreenPath; + + if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation) + && System.IO.File.Exists(brandingOptions.SplashscreenLocation)) + { + splashscreenPath = brandingOptions.SplashscreenLocation; + } + else + { + splashscreenPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); + if (!System.IO.File.Exists(splashscreenPath)) + { + return NotFound(); + } + } + + var outputFormats = GetOutputFormats(format); + + TimeSpan? cacheDuration = null; + if (!string.IsNullOrEmpty(tag)) + { + cacheDuration = TimeSpan.FromDays(365); + } + + var options = new ImageProcessingOptions + { + Image = new ItemImageInfo + { + Path = splashscreenPath + }, + Height = height, + MaxHeight = maxHeight, + MaxWidth = maxWidth, + FillHeight = fillHeight, + FillWidth = fillWidth, + Quality = quality, + Width = width, + Blur = blur, + BackgroundColor = backgroundColor, + ForegroundLayer = foregroundLayer, + SupportedOutputFormats = outputFormats + }; + + return await GetImageResult( + options, + cacheDuration, + ImmutableDictionary<string, string>.Empty) + .ConfigureAwait(false); + } + + /// <summary> + /// Uploads a custom splashscreen. + /// </summary> + /// <returns>A <see cref="NoContentResult"/> indicating success.</returns> + /// <response code="204">Successfully uploaded new splashscreen.</response> + /// <response code="400">Error reading MimeType from uploaded image.</response> + /// <response code="403">User does not have permission to upload splashscreen..</response> + /// <exception cref="ArgumentException">Error reading the image format.</exception> + [HttpPost("Branding/Splashscreen")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [AcceptsImageFile] + public async Task<ActionResult> UploadCustomSplashscreen() + { + await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + + var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType; + + if (!mimeType.HasValue) + { + return BadRequest("Error reading mimetype from uploaded image"); + } + + var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType.Value)); + var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); + brandingOptions.SplashscreenLocation = filePath; + _serverConfigurationManager.SaveConfiguration("branding", brandingOptions); + + await using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) + { + await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); + } + + return NoContent(); + } + private static async Task<MemoryStream> GetMemoryStream(Stream inputStream) { using var reader = new StreamReader(inputStream); @@ -1772,7 +1892,6 @@ namespace Jellyfin.Api.Controllers string? backgroundColor, string? foregroundLayer, BaseItem? item, - bool isHeadRequest, ItemImageInfo? imageInfo = null) { if (percentPlayed.HasValue) @@ -1823,28 +1942,37 @@ namespace Jellyfin.Api.Controllers { "realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*" } }; + if (!imageInfo.IsLocalFile && item != null) + { + imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, imageIndex ?? 0).ConfigureAwait(false); + } + + var options = new ImageProcessingOptions + { + Height = height, + ImageIndex = imageIndex ?? 0, + Image = imageInfo, + Item = item, + ItemId = itemId, + MaxHeight = maxHeight, + MaxWidth = maxWidth, + FillHeight = fillHeight, + FillWidth = fillWidth, + Quality = quality ?? 100, + Width = width, + AddPlayedIndicator = addPlayedIndicator ?? false, + PercentPlayed = percentPlayed ?? 0, + UnplayedCount = unplayedCount, + Blur = blur, + BackgroundColor = backgroundColor, + ForegroundLayer = foregroundLayer, + SupportedOutputFormats = outputFormats + }; + return await GetImageResult( - item, - itemId, - imageIndex, - width, - height, - maxWidth, - maxHeight, - fillWidth, - fillHeight, - quality, - addPlayedIndicator, - percentPlayed, - unplayedCount, - blur, - backgroundColor, - foregroundLayer, - imageInfo, - outputFormats, + options, cacheDuration, - responseHeaders, - isHeadRequest).ConfigureAwait(false); + responseHeaders).ConfigureAwait(false); } private ImageFormat[] GetOutputFormats(ImageFormat? format) @@ -1921,56 +2049,11 @@ namespace Jellyfin.Api.Controllers } private async Task<ActionResult> GetImageResult( - BaseItem? item, - Guid itemId, - int? index, - int? width, - int? height, - int? maxWidth, - int? maxHeight, - int? fillWidth, - int? fillHeight, - int? quality, - bool? addPlayedIndicator, - double? percentPlayed, - int? unplayedCount, - int? blur, - string? backgroundColor, - string? foregroundLayer, - ItemImageInfo imageInfo, - IReadOnlyCollection<ImageFormat> supportedFormats, + ImageProcessingOptions imageProcessingOptions, TimeSpan? cacheDuration, - IDictionary<string, string> headers, - bool isHeadRequest) + IDictionary<string, string> headers) { - if (!imageInfo.IsLocalFile && item != null) - { - imageInfo = await _libraryManager.ConvertImageToLocal(item, imageInfo, index ?? 0).ConfigureAwait(false); - } - - var options = new ImageProcessingOptions - { - Height = height, - ImageIndex = index ?? 0, - Image = imageInfo, - Item = item, - ItemId = itemId, - MaxHeight = maxHeight, - MaxWidth = maxWidth, - FillHeight = fillHeight, - FillWidth = fillWidth, - Quality = quality ?? 100, - Width = width, - AddPlayedIndicator = addPlayedIndicator ?? false, - PercentPlayed = percentPlayed ?? 0, - UnplayedCount = unplayedCount, - Blur = blur, - BackgroundColor = backgroundColor, - ForegroundLayer = foregroundLayer, - SupportedOutputFormats = supportedFormats - }; - - var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(options).ConfigureAwait(false); + var (imagePath, imageContentType, dateImageModified) = await _imageProcessor.ProcessImage(imageProcessingOptions).ConfigureAwait(false); var disableCaching = Request.Headers[HeaderNames.CacheControl].Contains("no-cache"); var parsingSuccessful = DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader); @@ -2019,12 +2102,6 @@ namespace Jellyfin.Api.Controllers } } - // if the request is a head request, return a NoContent result with the same headers as it would with a GET request - if (isHeadRequest) - { - return NoContent(); - } - return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain); } } diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index a6c2e07c9..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); @@ -341,10 +341,7 @@ namespace Jellyfin.Api.Controllers { var list = items; - var result = new QueryResult<BaseItemDto> - { - TotalRecordCount = list.Count - }; + var totalCount = list.Count; if (limit.HasValue && limit < list.Count) { @@ -353,7 +350,10 @@ namespace Jellyfin.Api.Controllers var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user); - result.Items = returnList; + var result = new QueryResult<BaseItemDto>( + 0, + totalCount, + returnList); return result; } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index f8192955e..2794a06f3 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -228,7 +228,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - var user = userId == Guid.Empty ? null : _userManager.GetUserById(userId); + var user = userId.Equals(default) ? null : _userManager.GetUserById(userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -491,10 +491,13 @@ namespace Jellyfin.Api.Controllers else { var itemsArray = folder.GetChildren(user, true); - result = new QueryResult<BaseItem> { Items = itemsArray, TotalRecordCount = itemsArray.Count, StartIndex = 0 }; + result = new QueryResult<BaseItem>(itemsArray); } - return new QueryResult<BaseItemDto> { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) }; + return new QueryResult<BaseItemDto>( + startIndex, + result.TotalRecordCount, + _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user)); } /// <summary> @@ -796,7 +799,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) @@ -809,7 +812,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(); } @@ -836,12 +839,10 @@ namespace Jellyfin.Api.Controllers var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user); - return new QueryResult<BaseItemDto> - { - StartIndex = startIndex.GetValueOrDefault(), - TotalRecordCount = itemsResult.TotalRecordCount, - Items = returnItems - }; + return new QueryResult<BaseItemDto>( + startIndex, + itemsResult.TotalRecordCount, + returnItems); } } } diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index f1b9c2f67..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(); @@ -506,13 +506,8 @@ namespace Jellyfin.Api.Controllers } var dtoOptions = new DtoOptions().AddClientFields(Request); - var result = new QueryResult<BaseItemDto> - { - TotalRecordCount = items.Count, - Items = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions)).ToArray() - }; - - return result; + var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions); + return new QueryResult<BaseItemDto>(resultArray); } /// <summary> @@ -694,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)) @@ -705,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); @@ -759,11 +754,10 @@ namespace Jellyfin.Api.Controllers var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user); - return new QueryResult<BaseItemDto> - { - Items = returnList, - TotalRecordCount = itemsResult.Count - }; + return new QueryResult<BaseItemDto>( + query.StartIndex, + itemsResult.Count, + returnList); } /// <summary> diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 9e2ef8c60..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); @@ -193,11 +193,10 @@ namespace Jellyfin.Api.Controllers dtoOptions.AddCurrentProgram = addCurrentProgram; var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, dtoOptions, user); - return new QueryResult<BaseItemDto> - { - Items = returnArray, - TotalRecordCount = channelResult.TotalRecordCount - }; + return new QueryResult<BaseItemDto>( + startIndex, + channelResult.TotalRecordCount, + returnArray); } /// <summary> @@ -212,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); @@ -383,18 +382,14 @@ 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); - return new QueryResult<BaseItemDto> - { - Items = returnArray, - TotalRecordCount = returnArray.Count - }; + return new QueryResult<BaseItemDto>(returnArray); } /// <summary> @@ -409,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); @@ -566,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) { @@ -593,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; @@ -622,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) { @@ -647,7 +642,7 @@ namespace Jellyfin.Api.Controllers GenreIds = body.GenreIds }; - if (!body.LibrarySeriesId.Equals(Guid.Empty)) + if (!body.LibrarySeriesId.Equals(default)) { query.IsSeries = true; @@ -687,7 +682,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Programs/Recommended")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<QueryResult<BaseItemDto>> GetRecommendedPrograms( + public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms( [FromQuery] Guid? userId, [FromQuery] int? limit, [FromQuery] bool? isAiring, @@ -705,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) { @@ -726,7 +721,7 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - return _liveTvManager.GetRecommendedPrograms(query, dtoOptions, CancellationToken.None); + return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); } /// <summary> @@ -743,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/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/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index cb4894d77..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( @@ -101,11 +98,7 @@ namespace Jellyfin.Api.Controllers Limit = limit ?? 0 }); - return new QueryResult<BaseItemDto> - { - Items = peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray(), - TotalRecordCount = peopleItems.Count - }; + return new QueryResult<BaseItemDto>(peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray()); } /// <summary> @@ -131,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 1667d6ede..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(); @@ -208,11 +210,10 @@ namespace Jellyfin.Api.Controllers dtos[index].PlaylistItemId = items[index].Item1.Id; } - var result = new QueryResult<BaseItemDto> - { - Items = dtos, - TotalRecordCount = count - }; + var result = new QueryResult<BaseItemDto>( + startIndex, + count, + dtos); return result; } diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index b41df1abb..b227dba2d 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -7,7 +7,6 @@ using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; -using Jellyfin.Api.Models.PluginDtos; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; @@ -44,61 +43,6 @@ namespace Jellyfin.Api.Controllers } /// <summary> - /// Get plugin security info. - /// </summary> - /// <response code="200">Plugin security info returned.</response> - /// <returns>Plugin security info.</returns> - [Obsolete("This endpoint should not be used.")] - [HttpGet("SecurityInfo")] - [ProducesResponseType(StatusCodes.Status200OK)] - public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo() - { - return new PluginSecurityInfo - { - IsMbSupporter = true, - SupporterKey = "IAmTotallyLegit" - }; - } - - /// <summary> - /// Gets registration status for a feature. - /// </summary> - /// <param name="name">Feature name.</param> - /// <response code="200">Registration status returned.</response> - /// <returns>Mb registration record.</returns> - [Obsolete("This endpoint should not be used.")] - [HttpPost("RegistrationRecords/{name}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name) - { - return new MBRegistrationRecord - { - IsRegistered = true, - RegChecked = true, - TrialVersion = false, - IsValid = true, - RegError = false - }; - } - - /// <summary> - /// Gets registration status for a feature. - /// </summary> - /// <param name="name">Feature name.</param> - /// <response code="501">Not implemented.</response> - /// <returns>Not Implemented.</returns> - /// <exception cref="NotImplementedException">This endpoint is not implemented.</exception> - [Obsolete("Paid plugins are not supported")] - [HttpGet("Registrations/{name}")] - [ProducesResponseType(StatusCodes.Status501NotImplemented)] - public static ActionResult GetRegistration([FromRoute, Required] string name) - { - // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins, - // delete all these registration endpoints. They are only kept for compatibility. - throw new NotImplementedException(); - } - - /// <summary> /// Gets a list of currently installed plugins. /// </summary> /// <response code="200">Installed plugins returned.</response> @@ -317,20 +261,5 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - - /// <summary> - /// Updates plugin security info. - /// </summary> - /// <param name="pluginSecurityInfo">Plugin security info.</param> - /// <response code="204">Plugin security info updated.</response> - /// <returns>An <see cref="NoContentResult"/>.</returns> - [Obsolete("This endpoint should not be used.")] - [HttpPost("SecurityInfo")] - [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo) - { - return NoContent(); - } } } diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 26acb4cdc..6ffedccbd 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -121,11 +121,7 @@ namespace Jellyfin.Api.Controllers IsSports = isSports }); - return new SearchHintResult - { - TotalRecordCount = result.TotalRecordCount, - SearchHints = result.Items.Select(GetSearchHintResult).ToArray() - }; + return new SearchHintResult(result.Items.Select(GetSearchHintResult).ToArray(), result.TotalRecordCount); } /// <summary> @@ -209,7 +205,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 af77c801f..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) @@ -81,11 +83,10 @@ namespace Jellyfin.Api.Controllers var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); - return new QueryResult<BaseItemDto> - { - TotalRecordCount = result.TotalRecordCount, - Items = dtoList - }; + return new QueryResult<BaseItemDto>( + startIndex, + result.TotalRecordCount, + dtoList); } } } diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index e20bcd7a7..179a53fd5 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -68,6 +68,7 @@ namespace Jellyfin.Api.Controllers /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param> /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param> /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param> + /// <param name="enableRewatching">Whether to include watched episode in next up results.</param> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> [HttpGet("NextUp")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -84,7 +85,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableUserData, [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool disableFirstEpisode = false) + [FromQuery] bool disableFirstEpisode = false, + [FromQuery] bool enableRewatching = false) { var options = new DtoOptions { Fields = fields } .AddClientFields(Request) @@ -100,21 +102,21 @@ namespace Jellyfin.Api.Controllers UserId = userId ?? Guid.Empty, EnableTotalRecordCount = enableTotalRecordCount, DisableFirstEpisode = disableFirstEpisode, - NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue + NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, + EnableRewatching = enableRewatching }, 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); - return new QueryResult<BaseItemDto> - { - TotalRecordCount = result.TotalRecordCount, - Items = returnItems - }; + return new QueryResult<BaseItemDto>( + startIndex, + result.TotalRecordCount, + returnItems); } /// <summary> @@ -143,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); @@ -169,11 +171,10 @@ namespace Jellyfin.Api.Controllers var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user); - return new QueryResult<BaseItemDto> - { - TotalRecordCount = itemsResult.Count, - Items = returnItems - }; + return new QueryResult<BaseItemDto>( + startIndex, + itemsResult.Count, + returnItems); } /// <summary> @@ -215,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; @@ -296,11 +297,10 @@ namespace Jellyfin.Api.Controllers var dtos = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user); - return new QueryResult<BaseItemDto> - { - TotalRecordCount = episodes.Count, - Items = dtos - }; + return new QueryResult<BaseItemDto>( + startIndex, + episodes.Count, + dtos); } /// <summary> @@ -332,9 +332,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); if (_libraryManager.GetItemById(seriesId) is not Series series) { @@ -354,11 +354,7 @@ namespace Jellyfin.Api.Controllers var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user); - return new QueryResult<BaseItemDto> - { - TotalRecordCount = returnItems.Count, - Items = returnItems - }; + return new QueryResult<BaseItemDto>(returnItems); } /// <summary> diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 4263d4fe5..6d15d9185 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -534,7 +534,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 8b99170d9..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); @@ -124,11 +124,7 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions().AddClientFields(Request); var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray(); - return new QueryResult<BaseItemDto> - { - Items = dtos, - TotalRecordCount = dtos.Length - }; + return new QueryResult<BaseItemDto>(dtos); } /// <summary> @@ -201,26 +197,21 @@ 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); var dtoOptions = new DtoOptions().AddClientFields(Request); - var dtosExtras = item.GetExtras(new[] { ExtraType.Trailer }) - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) - .ToArray(); if (item is IHasTrailers hasTrailers) { var trailers = hasTrailers.LocalTrailers; - var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item); - var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count]; - dtosExtras.CopyTo(allTrailers, 0); - dtosTrailers.CopyTo(allTrailers, dtosExtras.Length); - return allTrailers; + return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item)); } - return dtosExtras; + return Ok(item.GetExtras() + .Where(e => e.ExtraType == ExtraType.Trailer) + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))); } /// <summary> @@ -236,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); @@ -356,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); @@ -379,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 3d27371f6..04171da8a 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -108,11 +108,7 @@ namespace Jellyfin.Api.Controllers var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)) .ToArray(); - return new QueryResult<BaseItemDto> - { - Items = dtos, - TotalRecordCount = dtos.Length - }; + return new QueryResult<BaseItemDto>(dtos); } /// <summary> diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 3c079a71d..62c05331e 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(); @@ -134,12 +134,7 @@ namespace Jellyfin.Api.Controllers items = Array.Empty<BaseItemDto>(); } - var result = new QueryResult<BaseItemDto> - { - Items = items, - TotalRecordCount = items.Length - }; - + var result = new QueryResult<BaseItemDto>(items); return result; } @@ -226,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)); @@ -470,7 +465,7 @@ namespace Jellyfin.Api.Controllers StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); - return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, httpClient, HttpContext).ConfigureAwait(false); } if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File) @@ -499,9 +494,7 @@ namespace Jellyfin.Api.Controllers return FileStreamResponseHelpers.GetStaticFileResult( state.MediaPath, - contentType, - isHeadRequest, - HttpContext); + contentType); } // Need to start ffmpeg (because media can't be returned directly) diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 8be6fd1b5..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 @@ -136,8 +132,6 @@ namespace Jellyfin.Api.Controllers IEnumerable<BaseItem> ibnItems = ibnItemsArray; - var result = new QueryResult<BaseItemDto> { TotalRecordCount = ibnItemsArray.Count }; - if (startIndex.HasValue || limit.HasValue) { if (startIndex.HasValue) @@ -155,8 +149,10 @@ namespace Jellyfin.Api.Controllers var dtos = tuples.Select(i => _dtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user)); - result.Items = dtos.Where(i => i != null).ToArray(); - + var result = new QueryResult<BaseItemDto>( + startIndex, + ibnItemsArray.Count, + dtos.Where(i => i != null).ToArray()); return result; } @@ -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); |
