diff options
| author | cvium <clausvium@gmail.com> | 2022-01-07 10:23:22 +0100 |
|---|---|---|
| committer | cvium <clausvium@gmail.com> | 2022-01-07 10:23:22 +0100 |
| commit | c658a883a2bc84b46ed73d209d2983e8a324cdce (patch) | |
| tree | dabdbb5ac224e202d5433e7062e0c1b6872d1af7 /Jellyfin.Api | |
| parent | 2899b77cd58456470b8dd4d01d3a8c525a9b5911 (diff) | |
| parent | 6b4f5a86631e5bde93dae88553380c7ffd99b8e4 (diff) | |
Merge branch 'master' into keyframe_extraction_v1
# Conflicts:
# Jellyfin.Api/Controllers/DynamicHlsController.cs
# MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
# MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
Diffstat (limited to 'Jellyfin.Api')
72 files changed, 780 insertions, 1128 deletions
diff --git a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs index 49b6689cd..58552d847 100644 --- a/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs +++ b/Jellyfin.Api/Attributes/AcceptsFileAttribute.cs @@ -1,4 +1,6 @@ -using System; +#pragma warning disable CA1813 // Avoid unsealed attributes + +using System; namespace Jellyfin.Api.Attributes { diff --git a/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs b/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs index 001f27409..244a29da4 100644 --- a/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs +++ b/Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs @@ -3,7 +3,7 @@ /// <summary> /// Produces file attribute of "image/*". /// </summary> - public class AcceptsImageFileAttribute : AcceptsFileAttribute + public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute { private const string ContentType = "image/*"; diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs index 2fdd1e489..af8727552 100644 --- a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs +++ b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs @@ -7,7 +7,7 @@ namespace Jellyfin.Api.Attributes /// <summary> /// Identifies an action that supports the HTTP GET method. /// </summary> - public class HttpSubscribeAttribute : HttpMethodAttribute + public sealed class HttpSubscribeAttribute : HttpMethodAttribute { private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" }; diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs index d6d7e4563..1c0b70e71 100644 --- a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs +++ b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs @@ -7,7 +7,7 @@ namespace Jellyfin.Api.Attributes /// <summary> /// Identifies an action that supports the HTTP GET method. /// </summary> - public class HttpUnsubscribeAttribute : HttpMethodAttribute + public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute { private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" }; diff --git a/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs index 56c9772b6..514e7ce97 100644 --- a/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs +++ b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs @@ -6,7 +6,7 @@ namespace Jellyfin.Api.Attributes /// Attribute to mark a parameter as obsolete. /// </summary> [AttributeUsage(AttributeTargets.Parameter)] - public class ParameterObsoleteAttribute : Attribute + public sealed class ParameterObsoleteAttribute : Attribute { } } diff --git a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs index 3adb700eb..9fc25f192 100644 --- a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs @@ -3,7 +3,7 @@ /// <summary> /// Produces file attribute of "image/*". /// </summary> - public class ProducesAudioFileAttribute : ProducesFileAttribute + public sealed class ProducesAudioFileAttribute : ProducesFileAttribute { private const string ContentType = "audio/*"; diff --git a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs index 62a576ede..2bf77d729 100644 --- a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs @@ -1,4 +1,6 @@ -using System; +#pragma warning disable CA1813 // Avoid unsealed attributes + +using System; namespace Jellyfin.Api.Attributes { diff --git a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs index e15813676..1e5b542e2 100644 --- a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs @@ -3,7 +3,7 @@ /// <summary> /// Produces file attribute of "image/*". /// </summary> - public class ProducesImageFileAttribute : ProducesFileAttribute + public sealed class ProducesImageFileAttribute : ProducesFileAttribute { private const string ContentType = "image/*"; diff --git a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs index 5d928ab91..5b15cb1a5 100644 --- a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs @@ -3,7 +3,7 @@ /// <summary> /// Produces file attribute of "image/*". /// </summary> - public class ProducesPlaylistFileAttribute : ProducesFileAttribute + public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute { private const string ContentType = "application/x-mpegURL"; diff --git a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs index d8b2856dc..6857d45ec 100644 --- a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs +++ b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs @@ -3,7 +3,7 @@ /// <summary> /// Produces file attribute of "video/*". /// </summary> - public class ProducesVideoFileAttribute : ProducesFileAttribute + public sealed class ProducesVideoFileAttribute : ProducesFileAttribute { private const string ContentType = "video/*"; diff --git a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs new file mode 100644 index 000000000..88af08dd3 --- /dev/null +++ b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy +{ + /// <summary> + /// LAN access handler. Allows anonymous users. + /// </summary> + public class AnonymousLanAccessHandler : AuthorizationHandler<AnonymousLanAccessRequirement> + { + private readonly INetworkManager _networkManager; + private readonly IHttpContextAccessor _httpContextAccessor; + + /// <summary> + /// Initializes a new instance of the <see cref="AnonymousLanAccessHandler"/> class. + /// </summary> + /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> + /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> + public AnonymousLanAccessHandler( + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + { + _networkManager = networkManager; + _httpContextAccessor = httpContextAccessor; + } + + /// <inheritdoc /> + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement) + { + var ip = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress; + + // Loopback will be on LAN, so we can accept null. + if (ip == null || _networkManager.IsInLocalNetwork(ip)) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessRequirement.cs b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessRequirement.cs new file mode 100644 index 000000000..49af24ff3 --- /dev/null +++ b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy +{ + /// <summary> + /// The local network authorization requirement. Allows anonymous users. + /// </summary> + public class AnonymousLanAccessRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index 392498c53..13d3257df 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,4 +1,4 @@ -using System.Security.Claims; +using System.Security.Claims; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 369e846ae..bd3e7d9e3 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -45,6 +45,11 @@ namespace Jellyfin.Api.Auth try { var authorizationInfo = await _authService.Authenticate(Request).ConfigureAwait(false); + if (!authorizationInfo.HasToken) + { + return AuthenticateResult.NoResult(); + } + var role = UserRoles.User; if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) { diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs index b898ac76c..e6c04eb08 100644 --- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs +++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs @@ -51,7 +51,7 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy { if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups || user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups - || _syncPlayManager.IsUserActive(userId!.Value)) + || _syncPlayManager.IsUserActive(userId.Value)) { context.Succeed(requirement); } @@ -85,7 +85,7 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy } else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup) { - if (_syncPlayManager.IsUserActive(userId!.Value)) + if (_syncPlayManager.IsUserActive(userId.Value)) { context.Succeed(requirement); } diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index 632dedb3c..a72eeea28 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -46,6 +46,11 @@ namespace Jellyfin.Api.Constants public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation"; /// <summary> + /// Policy name for requiring (anonymous) LAN access. + /// </summary> + public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy"; + + /// <summary> /// Policy name for escaping schedule controls or requiring first time setup. /// </summary> public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl"; diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 154a56702..3df975563 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -133,8 +133,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, @@ -337,8 +337,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs new file mode 100644 index 000000000..98fd22430 --- /dev/null +++ b/Jellyfin.Api/Controllers/ClientLogController.cs @@ -0,0 +1,80 @@ +using System.Net.Mime; +using System.Threading.Tasks; +using Jellyfin.Api.Attributes; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.ClientLogDtos; +using MediaBrowser.Controller.ClientEvent; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// Client log controller. + /// </summary> + [Authorize(Policy = Policies.DefaultAuthorization)] + public class ClientLogController : BaseJellyfinApiController + { + private const int MaxDocumentSize = 1_000_000; + private readonly IClientEventLogger _clientEventLogger; + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="ClientLogController"/> class. + /// </summary> + /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public ClientLogController( + IClientEventLogger clientEventLogger, + IServerConfigurationManager serverConfigurationManager) + { + _clientEventLogger = clientEventLogger; + _serverConfigurationManager = serverConfigurationManager; + } + + /// <summary> + /// Upload a document. + /// </summary> + /// <response code="200">Document saved.</response> + /// <response code="403">Event logging disabled.</response> + /// <response code="413">Upload size too large.</response> + /// <returns>Create response.</returns> + [HttpPost("Document")] + [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] + [AcceptsFile(MediaTypeNames.Text.Plain)] + [RequestSizeLimit(MaxDocumentSize)] + public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile() + { + if (!_serverConfigurationManager.Configuration.AllowClientLogUpload) + { + return Forbid(); + } + + if (Request.ContentLength > MaxDocumentSize) + { + // Manually validate to return proper status code. + return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes"); + } + + var (clientName, clientVersion) = GetRequestInformation(); + var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body) + .ConfigureAwait(false); + return Ok(new ClientLogDocumentResponseDto(fileName)); + } + + private (string ClientName, string ClientVersion) GetRequestInformation() + { + var clientName = ClaimHelpers.GetClient(HttpContext.User) ?? "unknown-client"; + var clientVersion = ClaimHelpers.GetIsApiKey(HttpContext.User) + ? "apikey" + : ClaimHelpers.GetVersion(HttpContext.User) ?? "unknown-version"; + + return (clientName, clientVersion); + } + } +} diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index 445733c24..87cb418d9 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers if (enableInMainMenu.HasValue) { - configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList(); + configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList(); } return configPages; diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 87b4577b6..0b2604640 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -8,7 +8,7 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; -using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Dto; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -137,27 +137,30 @@ namespace Jellyfin.Api.Controllers } var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client); - existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null; + existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null; existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop; existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar; existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection; existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion) + && !string.IsNullOrEmpty(chromecastVersion) ? Enum.Parse<ChromecastVersion>(chromecastVersion, true) : ChromecastVersion.Stable; displayPreferences.CustomPrefs.Remove("chromecastVersion"); - existingDisplayPreferences.EnableNextVideoInfoOverlay = displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay) - ? bool.Parse(enableNextVideoInfoOverlay) - : true; + existingDisplayPreferences.EnableNextVideoInfoOverlay = !displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay) + || string.IsNullOrEmpty(enableNextVideoInfoOverlay) + || bool.Parse(enableNextVideoInfoOverlay); displayPreferences.CustomPrefs.Remove("enableNextVideoInfoOverlay"); existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength) + && !string.IsNullOrEmpty(skipBackLength) ? int.Parse(skipBackLength, CultureInfo.InvariantCulture) : 10000; displayPreferences.CustomPrefs.Remove("skipBackLength"); existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength) + && !string.IsNullOrEmpty(skipForwardLength) ? int.Parse(skipForwardLength, CultureInfo.InvariantCulture) : 30000; displayPreferences.CustomPrefs.Remove("skipForwardLength"); @@ -196,7 +199,7 @@ namespace Jellyfin.Api.Controllers } var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, itemId, existingDisplayPreferences.Client); - itemPrefs.SortBy = displayPreferences.SortBy; + itemPrefs.SortBy = displayPreferences.SortBy ?? "SortName"; itemPrefs.SortOrder = displayPreferences.SortOrder; itemPrefs.RememberIndexing = displayPreferences.RememberIndexing; itemPrefs.RememberSorting = displayPreferences.RememberSorting; diff --git a/Jellyfin.Api/Controllers/DlnaController.cs b/Jellyfin.Api/Controllers/DlnaController.cs index 052a6aff2..35c3a3d92 100644 --- a/Jellyfin.Api/Controllers/DlnaController.cs +++ b/Jellyfin.Api/Controllers/DlnaController.cs @@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - _dlnaManager.UpdateProfile(deviceProfile); + _dlnaManager.UpdateProfile(profileId, deviceProfile); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 694d16ad9..4e8c01577 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -7,7 +7,9 @@ using System.Threading.Tasks; using Emby.Dlna; using Emby.Dlna.Main; using Jellyfin.Api.Attributes; +using Jellyfin.Api.Constants; using MediaBrowser.Controller.Dlna; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers /// Dlna Server Controller. /// </summary> [Route("Dlna")] + [Authorize(Policy = Policies.AnonymousLanAccessPolicy)] public class DlnaServerController : BaseJellyfinApiController { private readonly IDlnaManager _dlnaManager; diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index a6d982552..674daa8d1 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -39,7 +39,8 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] public class DynamicHlsController : BaseJellyfinApiController { - private const string DefaultEncoderPreset = "veryfast"; + private const string DefaultVodEncoderPreset = "veryfast"; + private const string DefaultEventEncoderPreset = "superfast"; private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; private readonly ILibraryManager _libraryManager; @@ -110,6 +111,253 @@ namespace Jellyfin.Api.Controllers } /// <summary> + /// Gets a hls live stream. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="container">The audio container.</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="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> + /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param> + /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> + /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> + /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> + /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> + /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> + /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> + /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> + /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param> + /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param> + /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param> + /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param> + /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> + /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> + /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param> + /// <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="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> + /// <param name="maxRefFrames">Optional.</param> + /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> + /// <param name="requireAvc">Optional. Whether to require avc.</param> + /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> + /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param> + /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> + /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param> + /// <param name="liveStreamId">The live stream id.</param> + /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> + /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param> + /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> + /// <param name="transcodeReasons">Optional. The transcoding reason.</param> + /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> + /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> + /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> + /// <param name="streamOptions">Optional. The streaming options.</param> + /// <param name="maxWidth">Optional. The max width.</param> + /// <param name="maxHeight">Optional. The max height.</param> + /// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param> + /// <response code="200">Hls live stream retrieved.</response> + /// <returns>A <see cref="FileResult"/> containing the hls file.</returns> + [HttpGet("Videos/{itemId}/live.m3u8")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] + public async Task<ActionResult> GetLiveHlsStream( + [FromRoute, Required] Guid itemId, + [FromQuery] string? container, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod? subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodeReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary<string, string> streamOptions, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] bool? enableSubtitlesInManifest) + { + VideoRequestDto streamingRequest = new VideoRequestDto + { + Id = itemId, + Container = container, + Static = @static ?? false, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? false, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? false, + DeInterlace = deInterlace ?? false, + RequireNonAnamorphic = requireNonAnamorphic ?? false, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodeReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Streaming, + StreamOptions = streamOptions, + MaxHeight = maxHeight, + MaxWidth = maxWidth, + EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true + }; + + // CTS lifecycle is managed internally. + var cancellationTokenSource = new CancellationTokenSource(); + // 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( + streamingRequest, + Request, + _authContext, + _mediaSourceManager, + _userManager, + _libraryManager, + _serverConfigurationManager, + _mediaEncoder, + _encodingHelper, + _dlnaManager, + _deviceManager, + _transcodingJobHelper, + TranscodingJobType, + cancellationToken) + .ConfigureAwait(false); + + TranscodingJobDto? job = null; + var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); + + if (!System.IO.File.Exists(playlistPath)) + { + var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); + await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (!System.IO.File.Exists(playlistPath)) + { + // If the playlist doesn't already exist, startup ffmpeg + try + { + job = await _transcodingJobHelper.StartFfMpeg( + state, + playlistPath, + GetCommandLineArguments(playlistPath, state, true, 0), + Request, + TranscodingJobType, + cancellationTokenSource) + .ConfigureAwait(false); + job.IsLiveOutput = true; + } + catch + { + state.Dispose(); + throw; + } + + minSegments = state.MinSegments; + if (minSegments > 0) + { + await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false); + } + } + } + finally + { + transcodingLock.Release(); + } + } + + job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + + if (job != null) + { + _transcodingJobHelper.OnTranscodeEndRequest(job); + } + + var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state); + + return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8")); + } + + /// <summary> /// Gets a video hls playlist stream. /// </summary> /// <param name="itemId">The item id.</param> @@ -1186,7 +1434,7 @@ namespace Jellyfin.Api.Controllers var segmentPath = GetSegmentPath(state, playlistPath, segmentId); - var segmentExtension = GetSegmentFileExtension(state.Request.SegmentContainer); + var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); TranscodingJobDto? job; @@ -1258,7 +1506,7 @@ namespace Jellyfin.Api.Controllers job = await _transcodingJobHelper.StartFfMpeg( state, playlistPath, - GetCommandLineArguments(playlistPath, state, true, segmentId), + GetCommandLineArguments(playlistPath, state, false, segmentId), Request, TranscodingJobType, cancellationTokenSource).ConfigureAwait(false); @@ -1318,7 +1566,7 @@ namespace Jellyfin.Api.Controllers return segments; } - private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber) + private string GetCommandLineArguments(string outputPath, StreamState state, bool isEventPlaylist, int startNumber) { var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); @@ -1333,15 +1581,13 @@ namespace Jellyfin.Api.Controllers state.BaseRequest.BreakOnNonKeyFrames = false; } - // If isEncoding is true we're actually starting ffmpeg - var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0"; 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)); var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath); var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension); - var outputExtension = GetSegmentFileExtension(state.Request.SegmentContainer); + var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); var outputTsArg = outputPrefix + "%d" + outputExtension; var segmentFormat = outputExtension.TrimStart('.'); @@ -1363,26 +1609,37 @@ namespace Jellyfin.Api.Controllers } else { - _logger.LogError("Invalid HLS segment container: " + segmentFormat); + _logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat); } var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128 ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) : "128"; + var baseUrlParam = string.Empty; + if (isEventPlaylist) + { + baseUrlParam = string.Format( + CultureInfo.InvariantCulture, + " -hls_base_url \"hls/{0}/\"", + Path.GetFileNameWithoutExtension(outputPath)); + } + return string.Format( CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"", + "{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), threads, mapArgs, - GetVideoArguments(state, startNumber), + GetVideoArguments(state, startNumber, isEventPlaylist), GetAudioArguments(state), maxMuxingQueueSize, state.SegmentLength.ToString(CultureInfo.InvariantCulture), segmentFormat, - startNumberParam, + startNumber.ToString(CultureInfo.InvariantCulture), + baseUrlParam, + isEventPlaylist ? "event" : "vod", outputTsArg, outputPath).Trim(); } @@ -1477,8 +1734,9 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="state">The <see cref="StreamState"/>.</param> /// <param name="startNumber">The first number in the hls sequence.</param> + /// <param name="isEventPlaylist">Whether the playlist is EVENT or VOD.</param> /// <returns>The command line arguments for video transcoding.</returns> - private string GetVideoArguments(StreamState state, int startNumber) + private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist) { if (state.VideoStream == null) { @@ -1511,6 +1769,7 @@ namespace Jellyfin.Api.Controllers // See if we can save come cpu cycles by avoiding encoding. if (EncodingHelper.IsCopyCodec(codec)) { + // If h264_mp4toannexb is ever added, do not use it for live tv. if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream); @@ -1521,15 +1780,13 @@ namespace Jellyfin.Api.Controllers } args += " -start_at_zero"; - - // args += " -flags -global_header"; } else { - args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset); + args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, isEventPlaylist ? DefaultEventEncoderPreset : DefaultVodEncoderPreset); // Set the key frame params for video encoding to match the hls segment time. - args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, false, startNumber); + 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. if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)) @@ -1539,27 +1796,25 @@ namespace Jellyfin.Api.Controllers // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - - if (hasGraphicalSubs) - { - // Graphical subs overlay and resolution params. - args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec); - } - else - { - // Resolution params. - args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec); - } + // video processing filters. + args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec); // -start_at_zero is necessary to use with -ss when seeking, // otherwise the target position cannot be determined. - if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) + if (state.SubtitleStream != null) { - args += " -start_at_zero"; + // Disable start_at_zero for external graphical subs + if (!(state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) + { + args += " -start_at_zero"; + } } + } - // args += " -flags -global_header"; + // TODO why was this not enabled for VOD? + if (isEventPlaylist) + { + args += " -flags -global_header"; } if (!string.IsNullOrEmpty(state.OutputVideoSync)) @@ -1572,22 +1827,12 @@ namespace Jellyfin.Api.Controllers return args; } - private string GetSegmentFileExtension(string? segmentContainer) - { - if (!string.IsNullOrWhiteSpace(segmentContainer)) - { - return "." + segmentContainer; - } - - return ".ts"; - } - private string GetSegmentPath(StreamState state, string playlist, int index) { var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist)); var filename = Path.GetFileNameWithoutExtension(playlist); - return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request.SegmentContainer)); + return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer)); } private async Task<ActionResult> GetSegmentResult( @@ -1681,7 +1926,7 @@ namespace Jellyfin.Api.Controllers return Task.CompletedTask; }); - return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext); + return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath), false, HttpContext); } private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) @@ -1743,7 +1988,7 @@ namespace Jellyfin.Api.Controllers return; } - _logger.LogDebug("Deleting partial HLS file {path}", path); + _logger.LogDebug("Deleting partial HLS file {Path}", path); try { @@ -1751,15 +1996,15 @@ namespace Jellyfin.Api.Controllers } catch (IOException ex) { - _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path); + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); var task = Task.Delay(100); - Task.WaitAll(task); + task.Wait(); DeleteFile(path, retryCount + 1); } catch (Exception ex) { - _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path); + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); } } } diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 223b2a2b6..a4f12666d 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using Jellyfin.Api.Constants; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; @@ -71,7 +70,7 @@ namespace Jellyfin.Api.Controllers { User = user, MediaTypes = mediaTypes, - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + IncludeItemTypes = includeItemTypes, Recursive = true, EnableTotalRecordCount = false, DtoOptions = new DtoOptions @@ -166,7 +165,7 @@ namespace Jellyfin.Api.Controllers var filters = new QueryFilters(); var genreQuery = new InternalItemsQuery(user) { - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + IncludeItemTypes = includeItemTypes, DtoOptions = new DtoOptions { Fields = Array.Empty<ItemFields>(), @@ -198,16 +197,16 @@ namespace Jellyfin.Api.Controllers { filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair { - Name = i.Item1.Name, - Id = i.Item1.Id + Name = i.Item.Name, + Id = i.Item.Id }).ToArray(); } else { filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair { - Name = i.Item1.Name, - Id = i.Item1.Id + Name = i.Item.Name, + Id = i.Item.Id }).ToArray(); } diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 5aa457153..37e6ae184 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -101,8 +101,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers Genre item = new Genre(); if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) { - var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions); + var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre); if (result != null) { @@ -182,27 +182,27 @@ namespace Jellyfin.Api.Controllers return _dtoService.GetBaseItemDto(item, dtoOptions); } - private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions) + private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind) where T : BaseItem, new() { var result = libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '&'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); result ??= libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '/'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); result ??= libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '?'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 473bdc523..7325dca0a 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -64,12 +64,12 @@ namespace Jellyfin.Api.Controllers var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath)) + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid segment."); } - return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext); + return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file), false, HttpContext); } /// <summary> @@ -90,7 +90,7 @@ namespace Jellyfin.Api.Controllers var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8") + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8") { return BadRequest("Invalid segment."); } @@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath)) + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture)) { return BadRequest("Invalid segment."); } @@ -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), false, HttpContext); } } } diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 99ab7f232..89bbf22c9 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - if (!path.StartsWith(_applicationPaths.GeneralPath)) + if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } @@ -177,7 +177,7 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { - if (!path.StartsWith(basePath)) + if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } @@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { - if (!path.StartsWith(basePath)) + if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index b1c860d61..86933074d 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -2007,7 +2007,7 @@ namespace Jellyfin.Api.Controllers Response.Headers.Add(HeaderNames.CacheControl, "public"); } - Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false))); + Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", CultureInfo.InvariantCulture)); // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue) diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 4774ed4ef..a6c2e07c9 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -80,7 +80,7 @@ namespace Jellyfin.Api.Controllers : null; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); } @@ -116,7 +116,7 @@ namespace Jellyfin.Api.Controllers : null; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); } @@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers : null; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); } @@ -187,7 +187,7 @@ namespace Jellyfin.Api.Controllers : null; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); } @@ -223,7 +223,7 @@ namespace Jellyfin.Api.Controllers : null; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); } @@ -259,7 +259,7 @@ namespace Jellyfin.Api.Controllers : null; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); } @@ -332,7 +332,7 @@ namespace Jellyfin.Api.Controllers : null; var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); } diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index 448510c06..4161e43f6 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -5,8 +5,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; @@ -30,7 +28,6 @@ namespace Jellyfin.Api.Controllers public class ItemLookupController : BaseJellyfinApiController { private readonly IProviderManager _providerManager; - private readonly IServerApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; private readonly ILogger<ItemLookupController> _logger; @@ -39,19 +36,16 @@ namespace Jellyfin.Api.Controllers /// Initializes a new instance of the <see cref="ItemLookupController"/> class. /// </summary> /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param> public ItemLookupController( IProviderManager providerManager, - IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILogger<ItemLookupController> logger) { _providerManager = providerManager; - _appPaths = serverConfigurationManager.ApplicationPaths; _fileSystem = fileSystem; _libraryManager = libraryManager; _logger = logger; @@ -270,7 +264,8 @@ namespace Jellyfin.Api.Controllers ReplaceAllMetadata = true, ReplaceAllImages = replaceAllImages, SearchResult = searchResult - }, CancellationToken.None).ConfigureAwait(false); + }, + CancellationToken.None).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 64d7b2f3e..fd137f98f 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -263,8 +263,8 @@ namespace Jellyfin.Api.Controllers item.DateCreated = NormalizeDateTime(request.DateCreated.Value); } - item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : (DateTime?)null; - item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : (DateTime?)null; + item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null; + item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null; item.ProductionYear = request.ProductionYear; item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating; item.CustomRating = request.CustomRating; diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 52eefc5c2..f8192955e 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -8,8 +8,8 @@ using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -33,6 +33,7 @@ namespace Jellyfin.Api.Controllers private readonly ILocalizationManager _localization; private readonly IDtoService _dtoService; private readonly ILogger<ItemsController> _logger; + private readonly ISessionManager _sessionManager; /// <summary> /// Initializes a new instance of the <see cref="ItemsController"/> class. @@ -42,18 +43,21 @@ namespace Jellyfin.Api.Controllers /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> 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( IUserManager userManager, ILibraryManager libraryManager, ILocalizationManager localization, IDtoService dtoService, - ILogger<ItemsController> logger) + ILogger<ItemsController> logger, + ISessionManager sessionManager) { _userManager = userManager; _libraryManager = libraryManager; _localization = localization; _dtoService = dtoService; _logger = logger; + _sessionManager = sessionManager; } /// <summary> @@ -224,9 +228,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - var user = !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId) - : null; + var user = userId == Guid.Empty ? null : _userManager.GetUserById(userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -287,12 +289,12 @@ namespace Jellyfin.Api.Controllers if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || item is not UserRootFolder) { - var query = new InternalItemsQuery(user!) + var query = new InternalItemsQuery(user) { IsPlayed = isPlayed, MediaTypes = mediaTypes, - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, Recursive = recursive ?? false, OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), IsFavorite = isFavorite, @@ -454,7 +456,7 @@ namespace Jellyfin.Api.Controllers { query.AlbumIds = albums.SelectMany(i => { - return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 }); + return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = i, Limit = 1 }); }).ToArray(); } @@ -478,9 +480,9 @@ namespace Jellyfin.Api.Controllers if (query.OrderBy.Count == 0) { // Albums by artist - if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase)) + if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum) { - query.OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending), new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }; + query.OrderBy = new[] { (ItemSortBy.ProductionYear, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Ascending) }; } } @@ -763,6 +765,7 @@ namespace Jellyfin.Api.Controllers /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param> /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param> /// <param name="enableImages">Optional. Include image information in output.</param> + /// <param name="excludeActiveSessions">Optional. Whether to exclude the currently active sessions.</param> /// <response code="200">Items returned.</response> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns> [HttpGet("Users/{userId}/Items/Resume")] @@ -781,7 +784,8 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool? enableImages = true) + [FromQuery] bool? enableImages = true, + [FromQuery] bool excludeActiveSessions = false) { var user = _userManager.GetUserById(userId); var parentIdGuid = parentId ?? Guid.Empty; @@ -801,6 +805,15 @@ namespace Jellyfin.Api.Controllers .ToArray(); } + var excludeItemIds = Array.Empty<Guid>(); + if (excludeActiveSessions) + { + excludeItemIds = _sessionManager.Sessions + .Where(s => s.UserId == userId && s.NowPlayingItem != null) + .Select(s => s.NowPlayingItem.Id) + .ToArray(); + } + var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) }, @@ -815,9 +828,10 @@ namespace Jellyfin.Api.Controllers CollapseBoxSetItems = false, EnableTotalRecordCount = enableTotalRecordCount, AncestorIds = ancestorIds, - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - SearchTerm = searchTerm + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, + SearchTerm = searchTerm, + ExcludeItemIds = excludeItemIds }); var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 0be853ca4..f1b9c2f67 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -14,6 +14,8 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -22,7 +24,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Activity; @@ -36,7 +37,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Book = MediaBrowser.Controller.Entities.Book; namespace Jellyfin.Api.Controllers { @@ -413,14 +413,14 @@ namespace Jellyfin.Api.Controllers var counts = new ItemCounts { - AlbumCount = GetCount(typeof(MusicAlbum), user, isFavorite), - EpisodeCount = GetCount(typeof(Episode), user, isFavorite), - MovieCount = GetCount(typeof(Movie), user, isFavorite), - SeriesCount = GetCount(typeof(Series), user, isFavorite), - SongCount = GetCount(typeof(Audio), user, isFavorite), - MusicVideoCount = GetCount(typeof(MusicVideo), user, isFavorite), - BoxSetCount = GetCount(typeof(BoxSet), user, isFavorite), - BookCount = GetCount(typeof(Book), user, isFavorite) + AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite), + EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite), + MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite), + SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite), + SongCount = GetCount(BaseItemKind.Audio, user, isFavorite), + MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite), + BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite), + BookCount = GetCount(BaseItemKind.Book, user, isFavorite) }; return counts; @@ -529,7 +529,7 @@ namespace Jellyfin.Api.Controllers { var series = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(Series) }, + IncludeItemTypes = new[] { BaseItemKind.Series }, DtoOptions = new DtoOptions(false) { EnableImages = false @@ -559,7 +559,7 @@ namespace Jellyfin.Api.Controllers { var movies = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { nameof(Movie) }, + IncludeItemTypes = new[] { BaseItemKind.Movie }, DtoOptions = new DtoOptions(false) { EnableImages = false @@ -715,30 +715,31 @@ namespace Jellyfin.Api.Controllers bool? isMovie = item is Movie || (program != null && program.IsMovie) || item is Trailer; bool? isSeries = item is Series || (program != null && program.IsSeries); - var includeItemTypes = new List<string>(); + var includeItemTypes = new List<BaseItemKind>(); if (isMovie.Value) { - includeItemTypes.Add(nameof(Movie)); + includeItemTypes.Add(BaseItemKind.Movie); if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { - includeItemTypes.Add(nameof(Trailer)); - includeItemTypes.Add(nameof(LiveTvProgram)); + includeItemTypes.Add(BaseItemKind.Trailer); + includeItemTypes.Add(BaseItemKind.LiveTvProgram); } } else if (isSeries.Value) { - includeItemTypes.Add(nameof(Series)); + includeItemTypes.Add(BaseItemKind.Series); } else { // For non series and movie types these columns are typically null - isSeries = null; + // isSeries = null; isMovie = null; - includeItemTypes.Add(item.GetType().Name); + includeItemTypes.Add(item.GetBaseItemKind()); } var query = new InternalItemsQuery(user) { + Genres = item.Genres, Limit = limit, IncludeItemTypes = includeItemTypes.ToArray(), SimilarTo = item, @@ -785,7 +786,7 @@ namespace Jellyfin.Api.Controllers var typesList = types.ToList(); var plugins = _providerManager.GetAllMetadataPlugins() - .Where(i => types.Contains(i.ItemType, StringComparer.OrdinalIgnoreCase)) + .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase)) .OrderBy(i => typesList.IndexOf(i.ItemType)) .ToList(); @@ -871,11 +872,11 @@ namespace Jellyfin.Api.Controllers return result; } - private int GetCount(Type type, User? user, bool? isFavorite) + private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite) { var query = new InternalItemsQuery(user) { - IncludeItemTypes = new[] { type.Name }, + IncludeItemTypes = new[] { itemKind }, Limit = 0, Recursive = true, IsVirtualItem = false, @@ -940,10 +941,10 @@ namespace Jellyfin.Api.Controllers } var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions - .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) .ToArray(); - return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparer.OrdinalIgnoreCase)); + return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase)); } private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary) @@ -967,7 +968,7 @@ namespace Jellyfin.Api.Controllers .ToArray(); return metadataOptions.Length == 0 - || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); + || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); } private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) @@ -997,7 +998,7 @@ namespace Jellyfin.Api.Controllers return true; } - return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); + return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase)); } } } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index b131530c9..9e2ef8c60 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -278,25 +278,26 @@ namespace Jellyfin.Api.Controllers return _liveTvManager.GetRecordings( new RecordingQuery - { - ChannelId = channelId, - UserId = userId ?? Guid.Empty, - StartIndex = startIndex, - Limit = limit, - Status = status, - SeriesTimerId = seriesTimerId, - IsInProgress = isInProgress, - EnableTotalRecordCount = enableTotalRecordCount, - IsMovie = isMovie, - IsNews = isNews, - IsSeries = isSeries, - IsKids = isKids, - IsSports = isSports, - IsLibraryItem = isLibraryItem, - Fields = fields, - ImageTypeLimit = imageTypeLimit, - EnableImages = enableImages - }, dtoOptions); + { + ChannelId = channelId, + UserId = userId ?? Guid.Empty, + StartIndex = startIndex, + Limit = limit, + Status = status, + SeriesTimerId = seriesTimerId, + IsInProgress = isInProgress, + EnableTotalRecordCount = enableTotalRecordCount, + IsMovie = isMovie, + IsNews = isNews, + IsSeries = isSeries, + IsKids = isKids, + IsSports = isSports, + IsLibraryItem = isLibraryItem, + Fields = fields, + ImageTypeLimit = imageTypeLimit, + EnableImages = enableImages + }, + dtoOptions); } /// <summary> @@ -489,14 +490,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? isScheduled) { return await _liveTvManager.GetTimers( - new TimerQuery - { - ChannelId = channelId, - SeriesTimerId = seriesTimerId, - IsActive = isActive, - IsScheduled = isScheduled - }, CancellationToken.None) - .ConfigureAwait(false); + new TimerQuery + { + ChannelId = channelId, + SeriesTimerId = seriesTimerId, + IsActive = isActive, + IsScheduled = isScheduled + }, + CancellationToken.None).ConfigureAwait(false); } /// <summary> @@ -867,7 +868,8 @@ namespace Jellyfin.Api.Controllers { SortOrder = sortOrder ?? SortOrder.Ascending, SortBy = sortBy - }, CancellationToken.None).ConfigureAwait(false); + }, + CancellationToken.None).ConfigureAwait(false); } /// <summary> diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index 7c78928f7..b422eb78c 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -184,7 +184,7 @@ namespace Jellyfin.Api.Controllers audioStreamIndex, subtitleStreamIndex, maxAudioChannels, - info!.PlaySessionId!, + info.PlaySessionId!, userId ?? Guid.Empty, enableDirectPlay.Value, enableDirectStream.Value, @@ -316,7 +316,7 @@ namespace Jellyfin.Api.Controllers byte[] buffer = ArrayPool<byte>.Shared.Rent(size); try { - new Random().NextBytes(buffer); + Random.Shared.NextBytes(buffer); return File(buffer, MediaTypeNames.Application.Octet); } finally diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 99c90d19e..db72ff2f8 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -11,9 +11,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -84,7 +82,7 @@ namespace Jellyfin.Api.Controllers { IncludeItemTypes = new[] { - nameof(Movie), + BaseItemKind.Movie, // nameof(Trailer), // nameof(LiveTvProgram) }, @@ -99,11 +97,11 @@ namespace Jellyfin.Api.Controllers var recentlyPlayedMovies = _libraryManager.GetItemList(query); - var itemTypes = new List<string> { nameof(Movie) }; + var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { - itemTypes.Add(nameof(Trailer)); - itemTypes.Add(nameof(LiveTvProgram)); + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); } var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user) @@ -182,11 +180,11 @@ namespace Jellyfin.Api.Controllers DtoOptions dtoOptions, RecommendationType type) { - var itemTypes = new List<string> { nameof(Movie) }; + var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { - itemTypes.Add(nameof(Trailer)); - itemTypes.Add(nameof(LiveTvProgram)); + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); } foreach (var name in names) @@ -224,11 +222,11 @@ namespace Jellyfin.Api.Controllers private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) { - var itemTypes = new List<string> { nameof(Movie) }; + var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { - itemTypes.Add(nameof(Trailer)); - itemTypes.Add(nameof(LiveTvProgram)); + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); } foreach (var name in names) @@ -264,11 +262,11 @@ namespace Jellyfin.Api.Controllers private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) { - var itemTypes = new List<string> { nameof(Movie) }; + var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { - itemTypes.Add(nameof(Trailer)); - itemTypes.Add(nameof(LiveTvProgram)); + itemTypes.Add(BaseItemKind.Trailer); + itemTypes.Add(BaseItemKind.LiveTvProgram); } foreach (var item in baselineItems) diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 27eec2b9a..c4c03aa4f 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -101,8 +101,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -149,7 +149,7 @@ namespace Jellyfin.Api.Controllers if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1) { - item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions); + item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre); } else { @@ -166,27 +166,27 @@ namespace Jellyfin.Api.Controllers return _dtoService.GetBaseItemDto(item, dtoOptions); } - private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions) + private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind) where T : BaseItem, new() { var result = libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '&'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); result ??= libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '/'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); result ??= libraryManager.GetItemList(new InternalItemsQuery { Name = name.Replace(BaseItem.SlugChar, '?'), - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { baseItemKind }, DtoOptions = dtoOptions }).OfType<T>().FirstOrDefault(); diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index b98307f87..cb4894d77 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -26,7 +26,6 @@ namespace Jellyfin.Api.Controllers private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; private readonly IUserManager _userManager; - private readonly IUserDataManager _userDataManager; /// <summary> /// Initializes a new instance of the <see cref="PersonsController"/> class. @@ -34,17 +33,14 @@ namespace Jellyfin.Api.Controllers /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> - /// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param> public PersonsController( ILibraryManager libraryManager, IDtoService dtoService, - IUserManager userManager, - IUserDataManager userDataManager) + IUserManager userManager) { _libraryManager = libraryManager; _dtoService = dtoService; _userManager = userManager; - _userDataManager = userDataManager; } /// <summary> diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 0ae6109bc..b41df1abb 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -9,7 +9,6 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.PluginDtos; using Jellyfin.Extensions.Json; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Net; @@ -28,7 +27,6 @@ namespace Jellyfin.Api.Controllers { private readonly IInstallationManager _installationManager; private readonly IPluginManager _pluginManager; - private readonly IConfigurationManager _config; private readonly JsonSerializerOptions _serializerOptions; /// <summary> @@ -36,16 +34,13 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param> /// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param> - /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param> public PluginsController( IInstallationManager installationManager, - IPluginManager pluginManager, - IConfigurationManager config) + IPluginManager pluginManager) { _installationManager = installationManager; _pluginManager = pluginManager; _serializerOptions = JsonDefaults.Options; - _config = config; } /// <summary> diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 8a33b12f4..dbee56e14 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -3,18 +3,13 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Extensions; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -30,7 +25,6 @@ namespace Jellyfin.Api.Controllers { private readonly IProviderManager _providerManager; private readonly IServerApplicationPaths _applicationPaths; - private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryManager _libraryManager; /// <summary> @@ -38,17 +32,14 @@ namespace Jellyfin.Api.Controllers /// </summary> /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param> - /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> public RemoteImageController( IProviderManager providerManager, IServerApplicationPaths applicationPaths, - IHttpClientFactory httpClientFactory, ILibraryManager libraryManager) { _providerManager = providerManager; _applicationPaths = applicationPaths; - _httpClientFactory = httpClientFactory; _libraryManager = libraryManager; } @@ -89,7 +80,8 @@ namespace Jellyfin.Api.Controllers IncludeAllLanguages = includeAllLanguages, IncludeDisabledProviders = true, ImageType = type - }, CancellationToken.None) + }, + CancellationToken.None) .ConfigureAwait(false); var imageArray = images.ToArray(); @@ -183,36 +175,5 @@ namespace Jellyfin.Api.Controllers { return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); } - - /// <summary> - /// Downloads the image. - /// </summary> - /// <param name="url">The URL.</param> - /// <param name="urlHash">The URL hash.</param> - /// <param name="pointerCachePath">The pointer cache path.</param> - /// <returns>Task.</returns> - private async Task DownloadImage(Uri url, Guid urlHash, string pointerCachePath) - { - var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); - using var response = await httpClient.GetAsync(url).ConfigureAwait(false); - if (response.Content.Headers.ContentType?.MediaType == null) - { - throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType)); - } - - var ext = response.Content.Headers.ContentType.MediaType.AsSpan().RightPart('/').ToString(); - var fullCachePath = GetFullCachePath(urlHash + "." + ext); - - var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid."); - Directory.CreateDirectory(fullCacheDirectory); - // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); - - var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath)); - Directory.CreateDirectory(pointerCacheDirectory); - await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None) - .ConfigureAwait(false); - } } } diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 73bdf9018..26acb4cdc 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using Jellyfin.Api.Constants; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Drawing; @@ -110,8 +109,8 @@ namespace Jellyfin.Api.Controllers IncludeStudios = includeStudios, StartIndex = startIndex, UserId = userId ?? Guid.Empty, - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, MediaTypes = mediaTypes, ParentId = parentId, diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 3a04cb3a4..a6bbd40cc 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task<ActionResult> DisplayContent( [FromRoute, Required] string sessionId, - [FromQuery, Required] string itemType, + [FromQuery, Required] BaseItemKind itemType, [FromQuery, Required] string itemId, [FromQuery, Required] string itemName) { diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index a01a617fc..c49bde93f 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -93,7 +93,7 @@ namespace Jellyfin.Api.Controllers NetworkConfiguration settings = _config.GetNetworkConfiguration(); settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess; settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping; - _config.SaveConfiguration("network", settings); + _config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index da8f8b199..4422ef32c 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -97,8 +97,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 11f67ee89..16acedcf3 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers { var video = (Video)_libraryManager.GetItemById(itemId); - return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false); + return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, false, CancellationToken.None).ConfigureAwait(false); } /// <summary> @@ -376,7 +376,7 @@ namespace Jellyfin.Api.Controllers var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks); var url = string.Format( - CultureInfo.CurrentCulture, + CultureInfo.InvariantCulture, "stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}", positionTicks.ToString(CultureInfo.InvariantCulture), endPositionTicks.ToString(CultureInfo.InvariantCulture), @@ -417,6 +417,8 @@ namespace Jellyfin.Api.Controllers IsForced = body.IsForced, Stream = memoryStream }).ConfigureAwait(false); + _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); + return NoContent(); } @@ -526,7 +528,7 @@ namespace Jellyfin.Api.Controllers if (fontFile != null && fileSize != null && fileSize > 0) { - _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize); + _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize); return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName)); } else diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 97eec4bd2..af77c801f 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -58,7 +58,7 @@ namespace Jellyfin.Api.Controllers public ActionResult<QueryResult<BaseItemDto>> GetSuggestions( [FromRoute, Required] Guid userId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool enableTotalRecordCount = false) diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 741bdfee9..411c987f3 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; -using System.Net; using System.Net.Mime; using System.Threading.Tasks; using Jellyfin.Api.Attributes; @@ -66,7 +65,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<SystemInfo> GetSystemInfo() { - return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); + return _appHost.GetSystemInfo(Request); } /// <summary> @@ -78,7 +77,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<PublicSystemInfo> GetPublicSystemInfo() { - return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); + return _appHost.GetPublicSystemInfo(Request); } /// <summary> @@ -212,10 +211,13 @@ namespace Jellyfin.Api.Controllers /// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns> [HttpGet("WakeOnLanInfo")] [Authorize(Policy = Policies.DefaultAuthorization)] + [Obsolete("This endpoint is obsolete.")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo() { - var result = _appHost.GetWakeOnLanInfo(); + var result = _network.GetMacAddresses() + .Select(i => new WakeOnLanInfo(i)) + .ToList(); return Ok(result); } } diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 6eada67cf..e20bcd7a7 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> /// <param name="seriesId">Optional. Filter by series id.</param> /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> - /// <param name="enableImges">Optional. Include image information in output.</param> + /// <param name="enableImages">Optional. Include image information in output.</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="enableUserData">Optional. Include user data.</param> @@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] string? seriesId, [FromQuery] Guid? parentId, - [FromQuery] bool? enableImges, + [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData, @@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers { var options = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var result = _tvSeriesManager.GetNextUp( new NextUpQuery @@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> - /// <param name="enableImges">Optional. Include image information in output.</param> + /// <param name="enableImages">Optional. Include image information in output.</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="enableUserData">Optional. Include user data.</param> @@ -138,7 +138,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] Guid? parentId, - [FromQuery] bool? enableImges, + [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) @@ -153,11 +153,11 @@ namespace Jellyfin.Api.Controllers var options = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { nameof(Episode) }, + IncludeItemTypes = new[] { BaseItemKind.Episode }, OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) }, MinPremiereDate = minPremiereDate, StartIndex = startIndex, @@ -223,7 +223,7 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); if (seasonId.HasValue) // Season id was supplied. Get episodes by season id. { @@ -350,7 +350,7 @@ namespace Jellyfin.Api.Controllers var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) - .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); + .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 20a02bf4a..bc9527a0b 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers null, null, maxAudioChannels, - info!.PlaySessionId!, + info.PlaySessionId!, userId ?? Guid.Empty, true, true, diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index a33a0826c..8b99170d9 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -213,7 +212,7 @@ namespace Jellyfin.Api.Controllers if (item is IHasTrailers hasTrailers) { - var trailers = hasTrailers.GetTrailers(); + var trailers = hasTrailers.LocalTrailers; var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item); var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count]; dtosExtras.CopyTo(allTrailers, 0); @@ -297,12 +296,13 @@ namespace Jellyfin.Api.Controllers new LatestItemsQuery { GroupItems = groupItems, - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + IncludeItemTypes = includeItemTypes, IsPlayed = isPlayed, Limit = limit, ParentId = parentId ?? Guid.Empty, UserId = userId, - }, dtoOptions); + }, + dtoOptions); var dtos = list.Select(i => { diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs deleted file mode 100644 index ef25db8c9..000000000 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ /dev/null @@ -1,586 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; -using Jellyfin.Api.Helpers; -using Jellyfin.Api.Models.PlaybackDtos; -using Jellyfin.Api.Models.StreamingDtos; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Net; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Api.Controllers -{ - /// <summary> - /// The video hls controller. - /// </summary> - [Route("")] - [Authorize(Policy = Policies.DefaultAuthorization)] - public class VideoHlsController : BaseJellyfinApiController - { - private const string DefaultEncoderPreset = "superfast"; - private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; - - private readonly EncodingHelper _encodingHelper; - private readonly IDlnaManager _dlnaManager; - private readonly IAuthorizationContext _authContext; - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IDeviceManager _deviceManager; - private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly ILogger<VideoHlsController> _logger; - private readonly EncodingOptions _encodingOptions; - - /// <summary> - /// Initializes a new instance of the <see cref="VideoHlsController"/> class. - /// </summary> - /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - /// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param> - /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> - /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> - /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> - /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param> - /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param> - /// <param name="logger">Instance of the <see cref="ILogger{VideoHlsController}"/>.</param> - /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> - public VideoHlsController( - IMediaEncoder mediaEncoder, - IDlnaManager dlnaManager, - IUserManager userManger, - IAuthorizationContext authorizationContext, - ILibraryManager libraryManager, - IMediaSourceManager mediaSourceManager, - IServerConfigurationManager serverConfigurationManager, - IDeviceManager deviceManager, - TranscodingJobHelper transcodingJobHelper, - ILogger<VideoHlsController> logger, - EncodingHelper encodingHelper) - { - _dlnaManager = dlnaManager; - _authContext = authorizationContext; - _userManager = userManger; - _libraryManager = libraryManager; - _mediaSourceManager = mediaSourceManager; - _serverConfigurationManager = serverConfigurationManager; - _mediaEncoder = mediaEncoder; - _deviceManager = deviceManager; - _transcodingJobHelper = transcodingJobHelper; - _logger = logger; - _encodingHelper = encodingHelper; - - _encodingOptions = serverConfigurationManager.GetEncodingOptions(); - } - - /// <summary> - /// Gets a hls live stream. - /// </summary> - /// <param name="itemId">The item id.</param> - /// <param name="container">The audio container.</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="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> - /// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param> - /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> - /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> - /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> - /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> - /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> - /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> - /// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param> - /// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param> - /// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param> - /// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param> - /// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> - /// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param> - /// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param> - /// <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="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> - /// <param name="maxRefFrames">Optional.</param> - /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> - /// <param name="requireAvc">Optional. Whether to require avc.</param> - /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> - /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param> - /// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> - /// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param> - /// <param name="liveStreamId">The live stream id.</param> - /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> - /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param> - /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> - /// <param name="transcodeReasons">Optional. The transcoding reason.</param> - /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> - /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> - /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> - /// <param name="streamOptions">Optional. The streaming options.</param> - /// <param name="maxWidth">Optional. The max width.</param> - /// <param name="maxHeight">Optional. The max height.</param> - /// <param name="enableSubtitlesInManifest">Optional. Whether to enable subtitles in the manifest.</param> - /// <response code="200">Hls live stream retrieved.</response> - /// <returns>A <see cref="FileResult"/> containing the hls file.</returns> - [HttpGet("Videos/{itemId}/live.m3u8")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesPlaylistFile] - public async Task<ActionResult> GetLiveHlsStream( - [FromRoute, Required] Guid itemId, - [FromQuery] string? container, - [FromQuery] bool? @static, - [FromQuery] string? @params, - [FromQuery] string? tag, - [FromQuery] string? deviceProfileId, - [FromQuery] string? playSessionId, - [FromQuery] string? segmentContainer, - [FromQuery] int? segmentLength, - [FromQuery] int? minSegments, - [FromQuery] string? mediaSourceId, - [FromQuery] string? deviceId, - [FromQuery] string? audioCodec, - [FromQuery] bool? enableAutoStreamCopy, - [FromQuery] bool? allowVideoStreamCopy, - [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, - [FromQuery] int? audioSampleRate, - [FromQuery] int? maxAudioBitDepth, - [FromQuery] int? audioBitRate, - [FromQuery] int? audioChannels, - [FromQuery] int? maxAudioChannels, - [FromQuery] string? profile, - [FromQuery] string? level, - [FromQuery] float? framerate, - [FromQuery] float? maxFramerate, - [FromQuery] bool? copyTimestamps, - [FromQuery] long? startTimeTicks, - [FromQuery] int? width, - [FromQuery] int? height, - [FromQuery] int? videoBitRate, - [FromQuery] int? subtitleStreamIndex, - [FromQuery] SubtitleDeliveryMethod? subtitleMethod, - [FromQuery] int? maxRefFrames, - [FromQuery] int? maxVideoBitDepth, - [FromQuery] bool? requireAvc, - [FromQuery] bool? deInterlace, - [FromQuery] bool? requireNonAnamorphic, - [FromQuery] int? transcodingMaxAudioChannels, - [FromQuery] int? cpuCoreLimit, - [FromQuery] string? liveStreamId, - [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] string? videoCodec, - [FromQuery] string? subtitleCodec, - [FromQuery] string? transcodeReasons, - [FromQuery] int? audioStreamIndex, - [FromQuery] int? videoStreamIndex, - [FromQuery] EncodingContext? context, - [FromQuery] Dictionary<string, string> streamOptions, - [FromQuery] int? maxWidth, - [FromQuery] int? maxHeight, - [FromQuery] bool? enableSubtitlesInManifest) - { - VideoRequestDto streamingRequest = new VideoRequestDto - { - Id = itemId, - Container = container, - Static = @static ?? false, - Params = @params, - Tag = tag, - DeviceProfileId = deviceProfileId, - PlaySessionId = playSessionId, - SegmentContainer = segmentContainer, - SegmentLength = segmentLength, - MinSegments = minSegments, - MediaSourceId = mediaSourceId, - DeviceId = deviceId, - AudioCodec = audioCodec, - EnableAutoStreamCopy = enableAutoStreamCopy ?? true, - AllowAudioStreamCopy = allowAudioStreamCopy ?? true, - AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, - AudioSampleRate = audioSampleRate, - MaxAudioChannels = maxAudioChannels, - AudioBitRate = audioBitRate, - MaxAudioBitDepth = maxAudioBitDepth, - AudioChannels = audioChannels, - Profile = profile, - Level = level, - Framerate = framerate, - MaxFramerate = maxFramerate, - CopyTimestamps = copyTimestamps ?? false, - StartTimeTicks = startTimeTicks, - Width = width, - Height = height, - VideoBitRate = videoBitRate, - SubtitleStreamIndex = subtitleStreamIndex, - SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode, - MaxRefFrames = maxRefFrames, - MaxVideoBitDepth = maxVideoBitDepth, - RequireAvc = requireAvc ?? false, - DeInterlace = deInterlace ?? false, - RequireNonAnamorphic = requireNonAnamorphic ?? false, - TranscodingMaxAudioChannels = transcodingMaxAudioChannels, - CpuCoreLimit = cpuCoreLimit, - LiveStreamId = liveStreamId, - EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false, - VideoCodec = videoCodec, - SubtitleCodec = subtitleCodec, - TranscodeReasons = transcodeReasons, - AudioStreamIndex = audioStreamIndex, - VideoStreamIndex = videoStreamIndex, - Context = context ?? EncodingContext.Streaming, - StreamOptions = streamOptions, - MaxHeight = maxHeight, - MaxWidth = maxWidth, - EnableSubtitlesInManifest = enableSubtitlesInManifest ?? true - }; - - // CTS lifecycle is managed internally. - var cancellationTokenSource = new CancellationTokenSource(); - // 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( - streamingRequest, - Request, - _authContext, - _mediaSourceManager, - _userManager, - _libraryManager, - _serverConfigurationManager, - _mediaEncoder, - _encodingHelper, - _dlnaManager, - _deviceManager, - _transcodingJobHelper, - TranscodingJobType, - cancellationToken) - .ConfigureAwait(false); - - TranscodingJobDto? job = null; - var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); - - if (!System.IO.File.Exists(playlistPath)) - { - var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); - await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - if (!System.IO.File.Exists(playlistPath)) - { - // If the playlist doesn't already exist, startup ffmpeg - try - { - job = await _transcodingJobHelper.StartFfMpeg( - state, - playlistPath, - GetCommandLineArguments(playlistPath, state), - Request, - TranscodingJobType, - cancellationTokenSource) - .ConfigureAwait(false); - job.IsLiveOutput = true; - } - catch - { - state.Dispose(); - throw; - } - - minSegments = state.MinSegments; - if (minSegments > 0) - { - await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationToken).ConfigureAwait(false); - } - } - } - finally - { - transcodingLock.Release(); - } - } - - job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); - - if (job != null) - { - _transcodingJobHelper.OnTranscodeEndRequest(job); - } - - var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state); - - return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8")); - } - - /// <summary> - /// Gets the command line arguments for ffmpeg. - /// </summary> - /// <param name="outputPath">The output path of the file.</param> - /// <param name="state">The <see cref="StreamState"/>.</param> - /// <returns>The command line arguments as a string.</returns> - private string GetCommandLineArguments(string outputPath, StreamState state) - { - var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); - var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); // GetNumberOfThreads is static. - 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)); - var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath); - var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension); - var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); - var outputTsArg = outputPrefix + "%d" + outputExtension; - - var segmentFormat = outputExtension.TrimStart('.'); - if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) - { - segmentFormat = "mpegts"; - } - else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)) - { - var outputFmp4HeaderArg = string.Empty; - if (OperatingSystem.IsWindows()) - { - // on Windows, the path of fmp4 header file needs to be configured - outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\""; - } - else - { - // on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder - outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""; - } - - segmentFormat = "fmp4" + outputFmp4HeaderArg; - } - else - { - _logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat); - } - - var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128 - ? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) - : "128"; - - var baseUrlParam = string.Format( - CultureInfo.InvariantCulture, - "\"hls/{0}/\"", - Path.GetFileNameWithoutExtension(outputPath)); - - return string.Format( - CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number 0 -hls_base_url {9} -hls_playlist_type event -hls_segment_filename \"{10}\" -y \"{11}\"", - inputModifier, - _encodingHelper.GetInputArgument(state, _encodingOptions), - threads, - mapArgs, - GetVideoArguments(state), - GetAudioArguments(state), - maxMuxingQueueSize, - state.SegmentLength.ToString(CultureInfo.InvariantCulture), - segmentFormat, - baseUrlParam, - outputTsArg, - outputPath).Trim(); - } - - /// <summary> - /// Gets the audio arguments for transcoding. - /// </summary> - /// <param name="state">The <see cref="StreamState"/>.</param> - /// <returns>The command line arguments for audio transcoding.</returns> - private string GetAudioArguments(StreamState state) - { - if (state.AudioStream == null) - { - return string.Empty; - } - - var audioCodec = _encodingHelper.GetAudioEncoder(state); - - if (!state.IsOutputVideo) - { - if (EncodingHelper.IsCopyCodec(audioCodec)) - { - var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); - - return "-acodec copy -strict -2" + bitStreamArgs; - } - - var audioTranscodeParams = string.Empty; - - audioTranscodeParams += "-acodec " + audioCodec; - - if (state.OutputAudioBitrate.HasValue) - { - audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture); - } - - if (state.OutputAudioChannels.HasValue) - { - audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture); - } - - if (state.OutputAudioSampleRate.HasValue) - { - audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); - } - - audioTranscodeParams += " -vn"; - return audioTranscodeParams; - } - - if (EncodingHelper.IsCopyCodec(audioCodec)) - { - var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); - - return "-acodec copy -strict -2" + bitStreamArgs; - } - - var args = "-codec:a:0 " + audioCodec; - - var channels = state.OutputAudioChannels; - - if (channels.HasValue) - { - args += " -ac " + channels.Value; - } - - var bitrate = state.OutputAudioBitrate; - - if (bitrate.HasValue) - { - args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture); - } - - if (state.OutputAudioSampleRate.HasValue) - { - args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); - } - - args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions); - - return args; - } - - /// <summary> - /// Gets the video arguments for transcoding. - /// </summary> - /// <param name="state">The <see cref="StreamState"/>.</param> - /// <returns>The command line arguments for video transcoding.</returns> - private string GetVideoArguments(StreamState state) - { - if (state.VideoStream == null) - { - return string.Empty; - } - - if (!state.IsOutputVideo) - { - return string.Empty; - } - - var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); - - 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 (state.EnableMpegtsM2TsMode) - // { - // args += " -mpegts_m2ts_mode 1"; - // } - - // See if we can save come cpu cycles by avoiding encoding. - if (EncodingHelper.IsCopyCodec(codec)) - { - // If h264_mp4toannexb is ever added, do not use it for live tv. - if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) - { - string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream); - if (!string.IsNullOrEmpty(bitStreamArgs)) - { - args += " " + bitStreamArgs; - } - } - - args += " -start_at_zero"; - } - else - { - args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset); - - // Set the key frame params for video encoding to match the hls segment time. - args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, true, null); - - // Currenly 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"; - } - - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - - if (hasGraphicalSubs) - { - // Graphical subs overlay and resolution params. - args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec); - } - else - { - // Resolution params. - args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec); - } - - if (state.SubtitleStream == null || !state.SubtitleStream.IsExternal || state.SubtitleStream.IsTextSubtitleStream) - { - args += " -start_at_zero"; - } - } - - args += " -flags -global_header"; - - if (!string.IsNullOrEmpty(state.OutputVideoSync)) - { - args += " -vsync " + state.OutputVideoSync; - } - - args += _encodingHelper.GetOutputFFlags(state); - - return args; - } - } -} diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 150f22d1b..3c079a71d 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -451,7 +451,7 @@ namespace Jellyfin.Api.Controllers if (@static.HasValue && @static.Value && state.DirectStreamProvider != null) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, state.Request.StartTimeTicks, Request, _dlnaManager); var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfo(streamingRequest.LiveStreamId); if (liveStreamInfo == null) @@ -461,13 +461,13 @@ namespace Jellyfin.Api.Controllers var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream()); // TODO (moved from MediaBrowser.Api): Don't hardcode contentType - return File(liveStream, MimeTypes.GetMimeType("file.ts")!); + return File(liveStream, MimeTypes.GetMimeType("file.ts")); } // Static remote stream if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http) { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); + 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); @@ -484,7 +484,7 @@ namespace Jellyfin.Api.Controllers var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); var isTranscodeCached = outputPathExists && transcodingJob != null; - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager); + StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, state.Request.StartTimeTicks, Request, _dlnaManager); // Static stream if (@static.HasValue && @static.Value) diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index d6dc6650c..8be6fd1b5 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -101,8 +102,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), - IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, MediaTypes = mediaTypes, DtoOptions = dtoOptions }; @@ -209,7 +210,7 @@ namespace Jellyfin.Api.Controllers } // Include MediaTypes - if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { return false; } diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs index a5e47b8ec..bec961dad 100644 --- a/Jellyfin.Api/Helpers/AudioHelper.cs +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -147,7 +147,7 @@ namespace Jellyfin.Api.Helpers } var outputPath = state.OutputFilePath; - var outputPathExists = System.IO.File.Exists(outputPath); + var outputPathExists = File.Exists(outputPath); var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); var isTranscodeCached = outputPathExists && transcodingJob != null; diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs index 29e6b4193..c1c2f93b4 100644 --- a/Jellyfin.Api/Helpers/ClaimHelpers.cs +++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs @@ -20,7 +20,7 @@ namespace Jellyfin.Api.Helpers var value = GetClaimValue(user, InternalClaimTypes.UserId); return string.IsNullOrEmpty(value) ? null - : (Guid?)Guid.Parse(value); + : Guid.Parse(value); } /// <summary> diff --git a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs deleted file mode 100644 index a911a3324..000000000 --- a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Reflection; - -namespace Jellyfin.Api.Helpers -{ - /// <summary> - /// A static class for copying matching properties from one object to another. - /// TODO: remove at the point when a fixed migration path has been decided upon. - /// </summary> - public static class ClassMigrationHelper - { - /// <summary> - /// Extension for 'Object' that copies the properties to a destination object. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="destination">The destination.</param> - public static void CopyProperties(this object source, object destination) - { - // If any this null throw an exception. - if (source == null || destination == null) - { - throw new Exception("Source or/and Destination Objects are null"); - } - - // Getting the Types of the objects. - Type typeDest = destination.GetType(); - Type typeSrc = source.GetType(); - - // Iterate the Properties of the source instance and populate them from their destination counterparts. - PropertyInfo[] srcProps = typeSrc.GetProperties(); - foreach (PropertyInfo srcProp in srcProps) - { - if (!srcProp.CanRead) - { - continue; - } - - var targetProperty = typeDest.GetProperty(srcProp.Name); - if (targetProperty == null) - { - continue; - } - - if (!targetProperty.CanWrite) - { - continue; - } - - var obj = targetProperty.GetSetMethod(true); - if (obj != null && obj.IsPrivate) - { - continue; - } - - var target = targetProperty.GetSetMethod(); - if (target != null && (target.Attributes & MethodAttributes.Static) != 0) - { - continue; - } - - if (!targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType)) - { - continue; - } - - // Passed all tests, lets set the value. - targetProperty.SetValue(destination, srcProp.GetValue(source, null), null); - } - } - } -} diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 4abe4c5d5..02af2e435 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -462,6 +462,11 @@ namespace Jellyfin.Api.Helpers private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user) { + if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop) + { + return; + } + var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index; const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\""; diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs index 61e18220a..3fa07720a 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs @@ -17,7 +17,6 @@ namespace Jellyfin.Api.Helpers private readonly TranscodingJobDto? _job; private readonly TranscodingJobHelper? _transcodingJobHelper; private readonly int _timeoutMs; - private int _bytesWritten; private bool _disposed; /// <summary> @@ -71,53 +70,58 @@ namespace Jellyfin.Api.Helpers /// <inheritdoc /> public override void Flush() { - _stream.Flush(); + // Not supported } /// <inheritdoc /> public override int Read(byte[] buffer, int offset, int count) + => Read(buffer.AsSpan(offset, count)); + + /// <inheritdoc /> + public override int Read(Span<byte> buffer) { - return _stream.Read(buffer, offset, count); + int totalBytesRead = 0; + var stopwatch = Stopwatch.StartNew(); + + while (KeepReading(stopwatch.ElapsedMilliseconds)) + { + totalBytesRead += _stream.Read(buffer); + if (totalBytesRead > 0) + { + break; + } + + Thread.Sleep(50); + } + + UpdateBytesWritten(totalBytesRead); + + return totalBytesRead; } /// <inheritdoc /> public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); + + /// <inheritdoc /> + public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) { int totalBytesRead = 0; - int remainingBytesToRead = count; var stopwatch = Stopwatch.StartNew(); - int newOffset = offset; - while (remainingBytesToRead > 0) + while (KeepReading(stopwatch.ElapsedMilliseconds)) { - cancellationToken.ThrowIfCancellationRequested(); - int bytesRead = await _stream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false); - - remainingBytesToRead -= bytesRead; - newOffset += bytesRead; - - if (bytesRead > 0) + totalBytesRead += await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + if (totalBytesRead > 0) { - _bytesWritten += bytesRead; - totalBytesRead += bytesRead; - - if (_job != null) - { - _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); - } + break; } - else - { - // If the job is null it's a live stream and will require user action to close, but don't keep it open indefinitely - if (_job?.HasExited ?? stopwatch.ElapsedMilliseconds > _timeoutMs) - { - break; - } - await Task.Delay(50, cancellationToken).ConfigureAwait(false); - } + await Task.Delay(50, cancellationToken).ConfigureAwait(false); } + UpdateBytesWritten(totalBytesRead); + return totalBytesRead; } @@ -159,5 +163,19 @@ namespace Jellyfin.Api.Helpers base.Dispose(disposing); } } + + private void UpdateBytesWritten(int totalBytesRead) + { + if (_job != null) + { + _job.BytesDownloaded += totalBytesRead; + } + } + + private bool KeepReading(long elapsed) + { + // If the job is null it's a live stream and will require user action to close, but don't keep it open indefinitely + return !_job?.HasExited ?? elapsed < _timeoutMs; + } } } diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 0efd3443b..1471f5a24 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -30,7 +30,7 @@ namespace Jellyfin.Api.Helpers { if (sortBy.Count == 0) { - return Array.Empty<ValueTuple<string, SortOrder>>(); + return Array.Empty<(string, SortOrder)>(); } var result = new (string, SortOrder)[sortBy.Count]; @@ -137,21 +137,5 @@ namespace Jellyfin.Api.Helpers TotalRecordCount = result.TotalRecordCount }; } - - internal static string[] GetItemTypeStrings(IReadOnlyList<BaseItemKind> itemKinds) - { - if (itemKinds.Count == 0) - { - return Array.Empty<string>(); - } - - var itemTypes = new string[itemKinds.Count]; - for (var i = 0; i < itemKinds.Count; i++) - { - itemTypes[i] = itemKinds[i].ToString(); - } - - return itemTypes; - } } } diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 4fc791665..ed071bcd7 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -90,6 +90,7 @@ namespace Jellyfin.Api.Helpers } var enableDlnaHeaders = !string.IsNullOrWhiteSpace(streamingRequest.Params) || + streamingRequest.StreamOptions.ContainsKey("dlnaheaders") || string.Equals(httpRequest.Headers["GetContentFeatures.DLNA.ORG"], "1", StringComparison.OrdinalIgnoreCase); var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper) @@ -148,7 +149,7 @@ namespace Jellyfin.Api.Helpers mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId) ? mediaSources[0] - : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.InvariantCulture)); + : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal)); if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id) { diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 14f287aef..3526d56c6 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Data.Enums; +using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; @@ -86,8 +87,8 @@ namespace Jellyfin.Api.Helpers DeleteEncodedMediaCache(); - sessionManager!.PlaybackProgress += OnPlaybackProgress; - sessionManager!.PlaybackStart += OnPlaybackProgress; + sessionManager.PlaybackProgress += OnPlaybackProgress; + sessionManager.PlaybackStart += OnPlaybackProgress; } /// <summary> @@ -217,7 +218,8 @@ namespace Jellyfin.Api.Helpers return KillTranscodingJobs( j => string.IsNullOrWhiteSpace(playSessionId) ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase) - : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase), deleteFiles); + : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase), + deleteFiles); } /// <summary> @@ -282,6 +284,7 @@ namespace Jellyfin.Api.Helpers lock (job.ProcessLock!) { + #pragma warning disable CA1849 // Can't await in lock block job.TranscodingThrottler?.Stop().GetAwaiter().GetResult(); var process = job.Process; @@ -307,6 +310,7 @@ namespace Jellyfin.Api.Helpers { } } + #pragma warning restore CA1849 } if (delete(job.Path!)) @@ -540,8 +544,7 @@ namespace Jellyfin.Api.Helpers state, cancellationTokenSource); - var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; - _logger.LogInformation(commandLineLogMessage); + _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); var logFilePrefix = "FFmpeg.Transcode-"; if (state.VideoRequest != null @@ -559,8 +562,9 @@ namespace Jellyfin.Api.Helpers // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); - await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); + await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false); process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state); @@ -607,6 +611,10 @@ namespace Jellyfin.Api.Helpers { StartThrottler(state, transcodingJob); } + else if (transcodingJob.ExitCode != 0) + { + throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transcodingJob.ExitCode)); + } _logger.LogDebug("StartFfMpeg() finished successfully"); @@ -743,6 +751,7 @@ namespace Jellyfin.Api.Helpers private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state) { job.HasExited = true; + job.ExitCode = process.ExitCode; _logger.LogDebug("Disposing stream resources"); state.Dispose(); @@ -878,8 +887,8 @@ namespace Jellyfin.Api.Helpers if (disposing) { _loggerFactory.Dispose(); - _sessionManager!.PlaybackProgress -= OnPlaybackProgress; - _sessionManager!.PlaybackStart -= OnPlaybackProgress; + _sessionManager.PlaybackProgress -= OnPlaybackProgress; + _sessionManager.PlaybackStart -= OnPlaybackProgress; } } } diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index fd99bb360..4619d7a71 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -10,14 +10,17 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 --> <NoWarn>AD0001</NoWarn> - <AnalysisMode>AllDisabledByDefault</AnalysisMode> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <TreatWarningsAsErrors>false</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.10" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" /> - <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.1" /> - <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.1" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.1" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> + <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.3" /> </ItemGroup> <ItemGroup> @@ -29,7 +32,7 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> diff --git a/Jellyfin.Api/ModelBinders/NullableEnumModelBinderProvider.cs b/Jellyfin.Api/ModelBinders/NullableEnumModelBinderProvider.cs index bc12ad05d..2ccfd0c06 100644 --- a/Jellyfin.Api/ModelBinders/NullableEnumModelBinderProvider.cs +++ b/Jellyfin.Api/ModelBinders/NullableEnumModelBinderProvider.cs @@ -24,4 +24,4 @@ namespace Jellyfin.Api.ModelBinders return new NullableEnumModelBinder(logger); } } -}
\ No newline at end of file +} diff --git a/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs new file mode 100644 index 000000000..44509a9c0 --- /dev/null +++ b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs @@ -0,0 +1,22 @@ +namespace Jellyfin.Api.Models.ClientLogDtos +{ + /// <summary> + /// Client log document response dto. + /// </summary> + public class ClientLogDocumentResponseDto + { + /// <summary> + /// Initializes a new instance of the <see cref="ClientLogDocumentResponseDto"/> class. + /// </summary> + /// <param name="fileName">The file name.</param> + public ClientLogDocumentResponseDto(string fileName) + { + FileName = fileName; + } + + /// <summary> + /// Gets the resulting filename. + /// </summary> + public string FileName { get; } + } +} diff --git a/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs b/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs deleted file mode 100644 index 249d828d3..000000000 --- a/Jellyfin.Api/Models/DisplayPreferencesDtos/DisplayPreferencesDto.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Collections.Generic; -using Jellyfin.Data.Enums; - -namespace Jellyfin.Api.Models.DisplayPreferencesDtos -{ - /// <summary> - /// Defines the display preferences for any item that supports them (usually Folders). - /// </summary> - public class DisplayPreferencesDto - { - /// <summary> - /// Initializes a new instance of the <see cref="DisplayPreferencesDto" /> class. - /// </summary> - public DisplayPreferencesDto() - { - RememberIndexing = false; - PrimaryImageHeight = 250; - PrimaryImageWidth = 250; - ShowBackdrop = true; - CustomPrefs = new Dictionary<string, string>(); - } - - /// <summary> - /// Gets or sets the user id. - /// </summary> - /// <value>The user id.</value> - public string? Id { get; set; } - - /// <summary> - /// Gets or sets the type of the view. - /// </summary> - /// <value>The type of the view.</value> - public string? ViewType { get; set; } - - /// <summary> - /// Gets or sets the sort by. - /// </summary> - /// <value>The sort by.</value> - public string? SortBy { get; set; } - - /// <summary> - /// Gets or sets the index by. - /// </summary> - /// <value>The index by.</value> - public string? IndexBy { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [remember indexing]. - /// </summary> - /// <value><c>true</c> if [remember indexing]; otherwise, <c>false</c>.</value> - public bool RememberIndexing { get; set; } - - /// <summary> - /// Gets or sets the height of the primary image. - /// </summary> - /// <value>The height of the primary image.</value> - public int PrimaryImageHeight { get; set; } - - /// <summary> - /// Gets or sets the width of the primary image. - /// </summary> - /// <value>The width of the primary image.</value> - public int PrimaryImageWidth { get; set; } - - /// <summary> - /// Gets the custom prefs. - /// </summary> - /// <value>The custom prefs.</value> - public Dictionary<string, string> CustomPrefs { get; } - - /// <summary> - /// Gets or sets the scroll direction. - /// </summary> - /// <value>The scroll direction.</value> - public ScrollDirection ScrollDirection { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether to show backdrops on this item. - /// </summary> - /// <value><c>true</c> if showing backdrops; otherwise, <c>false</c>.</value> - public bool ShowBackdrop { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [remember sorting]. - /// </summary> - /// <value><c>true</c> if [remember sorting]; otherwise, <c>false</c>.</value> - public bool RememberSorting { get; set; } - - /// <summary> - /// Gets or sets the sort order. - /// </summary> - /// <value>The sort order.</value> - public SortOrder SortOrder { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [show sidebar]. - /// </summary> - /// <value><c>true</c> if [show sidebar]; otherwise, <c>false</c>.</value> - public bool ShowSidebar { get; set; } - - /// <summary> - /// Gets or sets the client. - /// </summary> - public string? Client { get; set; } - } -} diff --git a/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs index f65988259..8b26ec317 100644 --- a/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs +++ b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs @@ -24,4 +24,4 @@ namespace Jellyfin.Api.Models.LibraryStructureDto /// </summary> public MediaPathInfo? PathInfo { get; set; } } -}
\ No newline at end of file +} diff --git a/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs b/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs index 2ddaa89e8..e7501bd9f 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/SetChannelMappingDto.cs @@ -25,4 +25,4 @@ namespace Jellyfin.Api.Models.LiveTvDtos [Required] public string ProviderChannelId { get; set; } = string.Empty; } -}
\ No newline at end of file +} diff --git a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs index 2cfdba507..c6bd5e56e 100644 --- a/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs +++ b/Jellyfin.Api/Models/MediaInfoDtos/PlaybackInfoDto.cs @@ -83,4 +83,4 @@ namespace Jellyfin.Api.Models.MediaInfoDtos /// </summary> public bool? AutoOpenLiveStream { get; set; } } -}
\ No newline at end of file +} diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs index 291e571dc..ab67c8732 100644 --- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs +++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs @@ -107,6 +107,11 @@ namespace Jellyfin.Api.Models.PlaybackDtos public bool HasExited { get; set; } /// <summary> + /// Gets or sets exit code. + /// </summary> + public int ExitCode { get; set; } + + /// <summary> /// Gets or sets a value indicating whether is user paused. /// </summary> public bool IsUserPaused { get; set; } @@ -129,7 +134,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos /// <summary> /// Gets or sets bytes downloaded. /// </summary> - public long? BytesDownloaded { get; set; } + public long BytesDownloaded { get; set; } /// <summary> /// Gets or sets bytes transcoded. diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs index 7b32d76ba..7a1ca252c 100644 --- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs +++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs @@ -141,7 +141,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos private bool IsThrottleAllowed(TranscodingJobDto job, int thresholdSeconds) { - var bytesDownloaded = job.BytesDownloaded ?? 0; + var bytesDownloaded = job.BytesDownloaded; var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0; var downloadPositionTicks = job.DownloadPositionTicks ?? 0; @@ -197,7 +197,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos } } - _logger.LogDebug("No throttle data for " + path); + _logger.LogDebug("No throttle data for {Path}", path); return false; } diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs index 0f84faeaf..cbabf087b 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -55,7 +55,7 @@ namespace Jellyfin.Api.Models.StreamingDtos /// <summary> /// Gets the video request. /// </summary> - public VideoRequestDto? VideoRequest => Request! as VideoRequestDto; + public VideoRequestDto? VideoRequest => Request as VideoRequestDto; /// <summary> /// Gets or sets the direct stream provicer. diff --git a/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs index 30473255e..be0595798 100644 --- a/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs +++ b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs @@ -31,4 +31,4 @@ namespace Jellyfin.Api.Models.SubtitleDtos [Required] public string Data { get; set; } = string.Empty; } -}
\ No newline at end of file +} |
