diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs new file mode 100644 index 0000000000..efdb6a3691 --- /dev/null +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -0,0 +1,153 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The hls segment controller. + /// + public class HlsSegmentController : BaseJellyfinApiController + { + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Initialized instance of the . + public HlsSegmentController( + IFileSystem fileSystem, + IServerConfigurationManager serverConfigurationManager, + TranscodingJobHelper transcodingJobHelper) + { + _fileSystem = fileSystem; + _serverConfigurationManager = serverConfigurationManager; + _transcodingJobHelper = transcodingJobHelper; + } + + /// + /// Gets the specified audio segment for an audio item. + /// + /// The item id. + /// The segment id. + /// Hls audio segment returned. + /// A containing the audio stream. + // Can't require authentication just yet due to seeing some requests come from Chrome without full query string + // [Authenticated] + [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3")] + [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac")] + [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] + public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId) + { + // TODO: Deprecate with new iOS app + var file = segmentId + Path.GetExtension(Request.Path); + file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); + + return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, this); + } + + /// + /// Gets a hls video playlist. + /// + /// The video id. + /// The playlist id. + /// Hls video playlist returned. + /// A containing the playlist. + [HttpGet("/Videos/{itemId}/hls/{playlistId}/stream.m3u8")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] + public ActionResult GetHlsPlaylistLegacy([FromRoute] string itemId, [FromRoute] string playlistId) + { + var file = playlistId + Path.GetExtension(Request.Path); + file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); + + return GetFileResult(file, file); + } + + /// + /// Stops an active encoding. + /// + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// The play session id. + /// Encoding stopped successfully. + /// A indicating success. + [HttpDelete("/Videos/ActiveEncodings")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId) + { + _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true); + return NoContent(); + } + + /// + /// Gets a hls video segment. + /// + /// The item id. + /// The playlist id. + /// The segment id. + /// The segment container. + /// Hls video segment returned. + /// A containing the video segment. + // Can't require authentication just yet due to seeing some requests come from Chrome without full query string + // [Authenticated] + [HttpGet("/Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] + public ActionResult GetHlsVideoSegmentLegacy( + [FromRoute] string itemId, + [FromRoute] string playlistId, + [FromRoute] string segmentId, + [FromRoute] string segmentContainer) + { + var file = segmentId + Path.GetExtension(Request.Path); + var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); + + file = Path.Combine(transcodeFolderPath, file); + + var normalizedPlaylistId = playlistId; + + var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath) + .FirstOrDefault(i => + string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) + && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1); + + return GetFileResult(file, playlistPath); + } + + private ActionResult GetFileResult(string path, string playlistPath) + { + var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); + + Response.OnCompleted(() => + { + if (transcodingJob != null) + { + _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob); + } + + return Task.CompletedTask; + }); + + return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, this); + } + } +} diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index d3537a7a70..b7f3c9b07c 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Models.LibraryStructureDto; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; @@ -16,6 +17,7 @@ using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -64,7 +66,7 @@ namespace Jellyfin.Api.Controllers /// The name of the virtual folder. /// The type of the collection. /// The paths of the virtual folder. - /// The library options. + /// The library options. /// Whether to refresh the library. /// Folder added. /// A . @@ -74,10 +76,10 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? name, [FromQuery] string? collectionType, [FromQuery] string[] paths, - [FromQuery] LibraryOptions? libraryOptions, + [FromBody] LibraryOptionsDto? libraryOptionsDto, [FromQuery] bool refreshLibrary = false) { - libraryOptions ??= new LibraryOptions(); + var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions(); if (paths != null && paths.Length > 0) { @@ -194,9 +196,7 @@ namespace Jellyfin.Api.Controllers /// /// Add a media path to a library. /// - /// The name of the library. - /// The path to add. - /// The path info. + /// The media path dto. /// Whether to refresh the library. /// A . /// Media path added. @@ -204,23 +204,16 @@ namespace Jellyfin.Api.Controllers [HttpPost("Paths")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddMediaPath( - [FromQuery] string? name, - [FromQuery] string? path, - [FromQuery] MediaPathInfo? pathInfo, + [FromBody, BindRequired] MediaPathDto mediaPathDto, [FromQuery] bool refreshLibrary = false) { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - _libraryMonitor.Stop(); try { - var mediaPath = pathInfo ?? new MediaPathInfo { Path = path }; + var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo { Path = mediaPathDto.Path }; - _libraryManager.AddMediaPath(name, mediaPath); + _libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath); } finally { diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index fc38eacafd..16272c37a6 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -726,6 +726,20 @@ namespace Jellyfin.Api.Helpers } } + /// + /// Transcoding video finished. Decrement the active request counter. + /// + /// The which ended. + public void OnTranscodeEndRequest(TranscodingJobDto job) + { + job.ActiveRequestCount--; + _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount); + if (job.ActiveRequestCount <= 0) + { + PingTimer(job, false); + } + } + /// /// Processes the exited. /// diff --git a/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs new file mode 100644 index 0000000000..a13cb90dbe --- /dev/null +++ b/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs @@ -0,0 +1,15 @@ +using MediaBrowser.Model.Configuration; + +namespace Jellyfin.Api.Models.LibraryStructureDto +{ + /// + /// Library options dto. + /// + public class LibraryOptionsDto + { + /// + /// Gets or sets library options. + /// + public LibraryOptions? LibraryOptions { get; set; } + } +} \ No newline at end of file diff --git a/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs new file mode 100644 index 0000000000..f659882595 --- /dev/null +++ b/Jellyfin.Api/Models/LibraryStructureDto/MediaPathDto.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using MediaBrowser.Model.Configuration; + +namespace Jellyfin.Api.Models.LibraryStructureDto +{ + /// + /// Media Path dto. + /// + public class MediaPathDto + { + /// + /// Gets or sets the name of the library. + /// + [Required] + public string? Name { get; set; } + + /// + /// Gets or sets the path to add. + /// + public string? Path { get; set; } + + /// + /// Gets or sets the path info. + /// + public MediaPathInfo? PathInfo { get; set; } + } +} \ No newline at end of file diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs deleted file mode 100644 index 8a3d00283f..0000000000 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Playback.Hls -{ - /// - /// Class GetHlsAudioSegment. - /// - // Can't require authentication just yet due to seeing some requests come from Chrome without full query string - //[Authenticated] - [Route("/Audio/{Id}/hls/{SegmentId}/stream.mp3", "GET")] - [Route("/Audio/{Id}/hls/{SegmentId}/stream.aac", "GET")] - public class GetHlsAudioSegmentLegacy - { - // TODO: Deprecate with new iOS app - - /// - /// Gets or sets the id. - /// - /// The id. - public string Id { get; set; } - - /// - /// Gets or sets the segment id. - /// - /// The segment id. - public string SegmentId { get; set; } - } - - /// - /// Class GetHlsVideoSegment. - /// - [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")] - [Authenticated] - public class GetHlsPlaylistLegacy - { - // TODO: Deprecate with new iOS app - - /// - /// Gets or sets the id. - /// - /// The id. - public string Id { get; set; } - - public string PlaylistId { get; set; } - } - - [Route("/Videos/ActiveEncodings", "DELETE")] - [Authenticated] - public class StopEncodingProcess - { - [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] - public string DeviceId { get; set; } - - [ApiMember(Name = "PlaySessionId", Description = "The play session id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] - public string PlaySessionId { get; set; } - } - - /// - /// Class GetHlsVideoSegment. - /// - // Can't require authentication just yet due to seeing some requests come from Chrome without full query string - //[Authenticated] - [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")] - public class GetHlsVideoSegmentLegacy : VideoStreamRequest - { - public string PlaylistId { get; set; } - - /// - /// Gets or sets the segment id. - /// - /// The segment id. - public string SegmentId { get; set; } - } - - public class HlsSegmentService : BaseApiService - { - private readonly IFileSystem _fileSystem; - - public HlsSegmentService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IFileSystem fileSystem) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _fileSystem = fileSystem; - } - - public Task Get(GetHlsPlaylistLegacy request) - { - var file = request.PlaylistId + Path.GetExtension(Request.PathInfo); - file = Path.Combine(ServerConfigurationManager.GetTranscodePath(), file); - - return GetFileResult(file, file); - } - - public Task Delete(StopEncodingProcess request) - { - return ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, path => true); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public Task Get(GetHlsVideoSegmentLegacy request) - { - var file = request.SegmentId + Path.GetExtension(Request.PathInfo); - var transcodeFolderPath = ServerConfigurationManager.GetTranscodePath(); - - file = Path.Combine(transcodeFolderPath, file); - - var normalizedPlaylistId = request.PlaylistId; - - var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath) - .FirstOrDefault(i => string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1); - - return GetFileResult(file, playlistPath); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public Task Get(GetHlsAudioSegmentLegacy request) - { - // TODO: Deprecate with new iOS app - var file = request.SegmentId + Path.GetExtension(Request.PathInfo); - file = Path.Combine(ServerConfigurationManager.GetTranscodePath(), file); - - return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite); - } - - private Task GetFileResult(string path, string playlistPath) - { - var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); - - return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions - { - Path = path, - FileShare = FileShare.ReadWrite, - OnComplete = () => - { - if (transcodingJob != null) - { - ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob); - } - } - }); - } - } -}