mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
Merge pull request #3811 from crobibero/api-cleanup
Clean up api-migration branch
This commit is contained in:
commit
8385c54591
@ -46,7 +46,6 @@ using Emby.Server.Implementations.SyncPlay;
|
|||||||
using Emby.Server.Implementations.TV;
|
using Emby.Server.Implementations.TV;
|
||||||
using Emby.Server.Implementations.Updates;
|
using Emby.Server.Implementations.Updates;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using MediaBrowser.Api;
|
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Events;
|
using MediaBrowser.Common.Events;
|
||||||
@ -1032,9 +1031,6 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include composable parts in the Api assembly
|
|
||||||
yield return typeof(ApiEntryPoint).Assembly;
|
|
||||||
|
|
||||||
// Include composable parts in the Model assembly
|
// Include composable parts in the Model assembly
|
||||||
yield return typeof(SystemInfo).Assembly;
|
yield return typeof(SystemInfo).Assembly;
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Net;
|
using System.Security.Claims;
|
||||||
using System.Security.Claims;
|
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
|
@ -144,10 +144,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||||
/// <response code="200">Audio stream returned.</response>
|
/// <response code="200">Audio stream returned.</response>
|
||||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||||
[HttpGet("{itemId}/{stream=stream}.{container?}")]
|
[HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetAudioStreamByContainer")]
|
||||||
[HttpGet("{itemId}/stream")]
|
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
|
||||||
[HttpHead("{itemId}/{stream=stream}.{container?}")]
|
[HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadAudioStreamByContainer")]
|
||||||
[HttpHead("{itemId}/stream")]
|
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult> GetAudioStream(
|
public async Task<ActionResult> GetAudioStream(
|
||||||
[FromRoute] Guid itemId,
|
[FromRoute] Guid itemId,
|
||||||
|
@ -44,7 +44,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// or a <see cref="NoContentResult"/> if the css is not configured.
|
/// or a <see cref="NoContentResult"/> if the css is not configured.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Css")]
|
[HttpGet("Css")]
|
||||||
[HttpGet("Css.css")]
|
[HttpGet("Css.css", Name = "GetBrandingCss_2")]
|
||||||
[Produces("text/css")]
|
[Produces("text/css")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
@ -42,8 +42,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Description xml returned.</response>
|
/// <response code="200">Description xml returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
|
||||||
[HttpGet("{serverId}/description.xml")]
|
|
||||||
[HttpGet("{serverId}/description")]
|
[HttpGet("{serverId}/description")]
|
||||||
|
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
|
||||||
[Produces(XMLContentType)]
|
[Produces(XMLContentType)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult GetDescriptionXml([FromRoute] string serverId)
|
public ActionResult GetDescriptionXml([FromRoute] string serverId)
|
||||||
@ -60,8 +60,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Dlna content directory returned.</response>
|
/// <response code="200">Dlna content directory returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
|
||||||
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml")]
|
|
||||||
[HttpGet("{serverId}/ContentDirectory/ContentDirectory")]
|
[HttpGet("{serverId}/ContentDirectory/ContentDirectory")]
|
||||||
|
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_2")]
|
||||||
[Produces(XMLContentType)]
|
[Produces(XMLContentType)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
@ -75,8 +75,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml")]
|
|
||||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar")]
|
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar")]
|
||||||
|
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
|
||||||
[Produces(XMLContentType)]
|
[Produces(XMLContentType)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
@ -90,8 +90,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml")]
|
|
||||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager")]
|
[HttpGet("{serverId}/ConnectionManager/ConnectionManager")]
|
||||||
|
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_2")]
|
||||||
[Produces(XMLContentType)]
|
[Produces(XMLContentType)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
@ -181,7 +181,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <param name="fileName">The icon filename.</param>
|
/// <param name="fileName">The icon filename.</param>
|
||||||
/// <returns>Icon stream.</returns>
|
/// <returns>Icon stream.</returns>
|
||||||
[HttpGet("{serverId}/icons/{filename}")]
|
[HttpGet("{serverId}/icons/{fileName}")]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName)
|
public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName)
|
||||||
{
|
{
|
||||||
@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fileName">The icon filename.</param>
|
/// <param name="fileName">The icon filename.</param>
|
||||||
/// <returns>Icon stream.</returns>
|
/// <returns>Icon stream.</returns>
|
||||||
[HttpGet("icons/{filename}")]
|
[HttpGet("icons/{fileName}")]
|
||||||
public ActionResult GetIcon([FromRoute] string fileName)
|
public ActionResult GetIcon([FromRoute] string fileName)
|
||||||
{
|
{
|
||||||
return GetIconInternal(fileName);
|
return GetIconInternal(fileName);
|
||||||
|
@ -165,7 +165,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Video stream returned.</response>
|
/// <response code="200">Video stream returned.</response>
|
||||||
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
|
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
|
||||||
[HttpGet("/Videos/{itemId}/master.m3u8")]
|
[HttpGet("/Videos/{itemId}/master.m3u8")]
|
||||||
[HttpHead("/Videos/{itemId}/master.m3u8")]
|
[HttpHead("/Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult> GetMasterHlsVideoPlaylist(
|
public async Task<ActionResult> GetMasterHlsVideoPlaylist(
|
||||||
[FromRoute] Guid itemId,
|
[FromRoute] Guid itemId,
|
||||||
@ -335,7 +335,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Audio stream returned.</response>
|
/// <response code="200">Audio stream returned.</response>
|
||||||
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
|
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
|
||||||
[HttpGet("/Audio/{itemId}/master.m3u8")]
|
[HttpGet("/Audio/{itemId}/master.m3u8")]
|
||||||
[HttpHead("/Audio/{itemId}/master.m3u8")]
|
[HttpHead("/Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult> GetMasterHlsAudioPlaylist(
|
public async Task<ActionResult> GetMasterHlsAudioPlaylist(
|
||||||
[FromRoute] Guid itemId,
|
[FromRoute] Guid itemId,
|
||||||
|
@ -50,8 +50,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
|
/// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
|
||||||
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
||||||
// [Authenticated]
|
// [Authenticated]
|
||||||
[HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3")]
|
[HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
|
||||||
[HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac")]
|
[HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||||
public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId)
|
public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId)
|
||||||
|
@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="403">User does not have permission to delete the image.</response>
|
/// <response code="403">User does not have permission to delete the image.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("/Users/{userId}/Images/{imageType}")]
|
[HttpPost("/Users/{userId}/Images/{imageType}")]
|
||||||
[HttpPost("/Users/{userId}/Images/{imageType}/{index?}")]
|
[HttpPost("/Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||||
@ -128,7 +128,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="403">User does not have permission to delete the image.</response>
|
/// <response code="403">User does not have permission to delete the image.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpDelete("/Users/{userId}/Images/{itemType}")]
|
[HttpDelete("/Users/{userId}/Images/{itemType}")]
|
||||||
[HttpDelete("/Users/{userId}/Images/{itemType}/{index?}")]
|
[HttpDelete("/Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
@ -167,7 +167,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="404">Item not found.</response>
|
/// <response code="404">Item not found.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||||
[HttpDelete("/Items/{itemId}/Images/{imageType}")]
|
[HttpDelete("/Items/{itemId}/Images/{imageType}")]
|
||||||
[HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
|
[HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="404">Item not found.</response>
|
/// <response code="404">Item not found.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||||
[HttpPost("/Items/{itemId}/Images/{imageType}")]
|
[HttpPost("/Items/{itemId}/Images/{imageType}")]
|
||||||
[HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
|
[HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
@ -342,9 +342,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("/Items/{itemId}/Images/{imageType}")]
|
[HttpGet("/Items/{itemId}/Images/{imageType}")]
|
||||||
[HttpHead("/Items/{itemId}/Images/{imageType}")]
|
[HttpHead("/Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
|
||||||
[HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")]
|
||||||
[HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}")]
|
[HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> GetItemImage(
|
public async Task<ActionResult> GetItemImage(
|
||||||
@ -422,7 +422,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
|
[HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
|
||||||
[HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")]
|
[HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> GetItemImage2(
|
public async Task<ActionResult> GetItemImage2(
|
||||||
@ -500,7 +500,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("/Artists/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("/Artists/{name}/Images/{imageType}/{imageIndex?}")]
|
||||||
[HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> GetArtistImage(
|
public async Task<ActionResult> GetArtistImage(
|
||||||
@ -578,7 +578,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("/Genres/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("/Genres/{name}/Images/{imageType}/{imageIndex?}")]
|
||||||
[HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> GetGenreImage(
|
public async Task<ActionResult> GetGenreImage(
|
||||||
@ -656,7 +656,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
|
||||||
[HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> GetMusicGenreImage(
|
public async Task<ActionResult> GetMusicGenreImage(
|
||||||
@ -734,7 +734,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("/Persons/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("/Persons/{name}/Images/{imageType}/{imageIndex?}")]
|
||||||
[HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> GetPersonImage(
|
public async Task<ActionResult> GetPersonImage(
|
||||||
@ -812,7 +812,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("/Studios/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("/Studios/{name}/Images/{imageType}/{imageIndex?}")]
|
||||||
[HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> GetStudioImage(
|
public async Task<ActionResult> GetStudioImage(
|
||||||
@ -890,7 +890,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("/Users/{userId}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("/Users/{userId}/Images/{imageType}/{imageIndex?}")]
|
||||||
[HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}")]
|
[HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> GetUserImage(
|
public async Task<ActionResult> GetUserImage(
|
||||||
|
@ -30,7 +30,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
private readonly IDtoService _dtoService;
|
private readonly IDtoService _dtoService;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger<ItemsController> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ItemsController"/> class.
|
/// Initializes a new instance of the <see cref="ItemsController"/> class.
|
||||||
@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
||||||
[HttpGet("/Items")]
|
[HttpGet("/Items")]
|
||||||
[HttpGet("/Users/{uId}/Items")]
|
[HttpGet("/Users/{uId}/Items", Name = "GetItems_2")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetItems(
|
public ActionResult<QueryResult<BaseItemDto>> GetItems(
|
||||||
[FromRoute] Guid? uId,
|
[FromRoute] Guid? uId,
|
||||||
|
@ -521,7 +521,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="tvdbId">The tvdbId.</param>
|
/// <param name="tvdbId">The tvdbId.</param>
|
||||||
/// <response code="204">Report success.</response>
|
/// <response code="204">Report success.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("/Library/Series/Added")]
|
[HttpPost("/Library/Series/Added", Name = "PostAddedSeries")]
|
||||||
[HttpPost("/Library/Series/Updated")]
|
[HttpPost("/Library/Series/Updated")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
@ -551,7 +551,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="imdbId">The imdbId.</param>
|
/// <param name="imdbId">The imdbId.</param>
|
||||||
/// <response code="204">Report success.</response>
|
/// <response code="204">Report success.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("/Library/Movies/Added")]
|
[HttpPost("/Library/Movies/Added", Name = "PostAddedMovies")]
|
||||||
[HttpPost("/Library/Movies/Updated")]
|
[HttpPost("/Library/Movies/Updated")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
@ -679,12 +679,12 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
|
||||||
/// <response code="200">Similar items returned.</response>
|
/// <response code="200">Similar items returned.</response>
|
||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
|
||||||
[HttpGet("/Artists/{itemId}/Similar")]
|
[HttpGet("/Artists/{itemId}/Similar", Name = "GetSimilarArtists2")]
|
||||||
[HttpGet("/Items/{itemId}/Similar")]
|
[HttpGet("/Items/{itemId}/Similar")]
|
||||||
[HttpGet("/Albums/{itemId}/Similar")]
|
[HttpGet("/Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")]
|
||||||
[HttpGet("/Shows/{itemId}/Similar")]
|
[HttpGet("/Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
|
||||||
[HttpGet("/Movies/{itemId}/Similar")]
|
[HttpGet("/Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
|
||||||
[HttpGet("/Trailers/{itemId}/Similar")]
|
[HttpGet("/Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
|
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
|
||||||
[FromRoute] Guid itemId,
|
[FromRoute] Guid itemId,
|
||||||
|
@ -249,7 +249,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult UpdateMediaPath(
|
public ActionResult UpdateMediaPath(
|
||||||
[FromQuery] string? name,
|
[FromQuery] string? name,
|
||||||
[FromQuery] MediaPathInfo? pathInfo)
|
[FromBody] MediaPathInfo? pathInfo)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
@ -320,7 +320,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult UpdateLibraryOptions(
|
public ActionResult UpdateLibraryOptions(
|
||||||
[FromQuery] string? id,
|
[FromQuery] string? id,
|
||||||
[FromQuery] LibraryOptions? libraryOptions)
|
[FromBody] LibraryOptions? libraryOptions)
|
||||||
{
|
{
|
||||||
var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
|
var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.LiveTv;
|
using MediaBrowser.Model.LiveTv;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
@ -128,7 +127,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpGet("Channels")]
|
[HttpGet("Channels")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetChannels(
|
public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
|
||||||
[FromQuery] ChannelType? type,
|
[FromQuery] ChannelType? type,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery] int? startIndex,
|
[FromQuery] int? startIndex,
|
||||||
@ -536,7 +535,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpGet("Programs")]
|
[HttpGet("Programs")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms(
|
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
|
||||||
[FromQuery] string? channelIds,
|
[FromQuery] string? channelIds,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery] DateTime? minStartDate,
|
[FromQuery] DateTime? minStartDate,
|
||||||
@ -934,7 +933,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[Obsolete("This endpoint is obsolete.")]
|
[Obsolete("This endpoint is obsolete.")]
|
||||||
public ActionResult<BaseItemDto> GetRecordingGroup([FromQuery] Guid? groupId)
|
public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute] Guid? groupId)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ using System.Text.Json;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
using Jellyfin.Api.Models.MediaInfoDtos;
|
||||||
using Jellyfin.Api.Models.VideoDtos;
|
using Jellyfin.Api.Models.VideoDtos;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@ -43,7 +44,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IAuthorizationContext _authContext;
|
private readonly IAuthorizationContext _authContext;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger<MediaInfoController> _logger;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -91,7 +92,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid? userId)
|
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
return await GetPlaybackInfoInternal(itemId, userId, null, null).ConfigureAwait(false);
|
return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -231,8 +232,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="subtitleStreamIndex">The subtitle stream index.</param>
|
/// <param name="subtitleStreamIndex">The subtitle stream index.</param>
|
||||||
/// <param name="maxAudioChannels">The maximum number of audio channels.</param>
|
/// <param name="maxAudioChannels">The maximum number of audio channels.</param>
|
||||||
/// <param name="itemId">The item id.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="deviceProfile">The device profile.</param>
|
/// <param name="openLiveStreamDto">The open live stream dto.</param>
|
||||||
/// <param name="directPlayProtocols">The direct play protocols. Default: <see cref="MediaProtocol.Http"/>.</param>
|
|
||||||
/// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
|
/// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
|
||||||
/// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
|
/// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
|
||||||
/// <response code="200">Media source opened.</response>
|
/// <response code="200">Media source opened.</response>
|
||||||
@ -249,8 +249,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] Guid? itemId,
|
[FromQuery] Guid? itemId,
|
||||||
[FromQuery] DeviceProfile? deviceProfile,
|
[FromBody] OpenLiveStreamDto openLiveStreamDto,
|
||||||
[FromQuery] MediaProtocol[] directPlayProtocols,
|
|
||||||
[FromQuery] bool enableDirectPlay = true,
|
[FromQuery] bool enableDirectPlay = true,
|
||||||
[FromQuery] bool enableDirectStream = true)
|
[FromQuery] bool enableDirectStream = true)
|
||||||
{
|
{
|
||||||
@ -265,10 +264,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
ItemId = itemId ?? Guid.Empty,
|
ItemId = itemId ?? Guid.Empty,
|
||||||
DeviceProfile = deviceProfile,
|
DeviceProfile = openLiveStreamDto?.DeviceProfile,
|
||||||
EnableDirectPlay = enableDirectPlay,
|
EnableDirectPlay = enableDirectPlay,
|
||||||
EnableDirectStream = enableDirectStream,
|
EnableDirectStream = enableDirectStream,
|
||||||
DirectPlayProtocols = directPlayProtocols ?? new[] { MediaProtocol.Http }
|
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
|
||||||
};
|
};
|
||||||
return await OpenMediaSource(request).ConfigureAwait(false);
|
return await OpenMediaSource(request).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
@ -241,7 +241,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="command">The command to send.</param>
|
/// <param name="command">The command to send.</param>
|
||||||
/// <response code="204">General command sent to session.</response>
|
/// <response code="204">General command sent to session.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("/Sessions/{sessionId}/Command/{Command}")]
|
[HttpPost("/Sessions/{sessionId}/Command/{command}")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult SendGeneralCommand(
|
public ActionResult SendGeneralCommand(
|
||||||
[FromRoute] string? sessionId,
|
[FromRoute] string? sessionId,
|
||||||
|
@ -106,7 +106,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Initial user retrieved.</response>
|
/// <response code="200">Initial user retrieved.</response>
|
||||||
/// <returns>The first user.</returns>
|
/// <returns>The first user.</returns>
|
||||||
[HttpGet("User")]
|
[HttpGet("User")]
|
||||||
[HttpGet("FirstUser")]
|
[HttpGet("FirstUser", Name = "GetFirstUser_2")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<StartupUserDto> GetFirstUser()
|
public async Task<StartupUserDto> GetFirstUser()
|
||||||
{
|
{
|
||||||
@ -131,7 +131,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpPost("User")]
|
[HttpPost("User")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto)
|
public async Task<ActionResult> UpdateStartupUser([FromForm] StartupUserDto startupUserDto)
|
||||||
{
|
{
|
||||||
var user = _userManager.Users.First();
|
var user = _userManager.Users.First();
|
||||||
|
|
||||||
|
@ -182,7 +182,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">File returned.</response>
|
/// <response code="200">File returned.</response>
|
||||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||||
[HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
|
[HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
|
||||||
[HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}")]
|
[HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult> GetSubtitle(
|
public async Task<ActionResult> GetSubtitle(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
|
@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("New")]
|
[HttpPost("New")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult CreateNewGroup()
|
public ActionResult SyncPlayCreateGroup()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
_syncPlayManager.NewGroup(currentSession, CancellationToken.None);
|
_syncPlayManager.NewGroup(currentSession, CancellationToken.None);
|
||||||
@ -62,7 +62,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Join")]
|
[HttpPost("Join")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult JoinGroup([FromQuery, Required] Guid groupId)
|
public ActionResult SyncPlayJoinGroup([FromQuery, Required] Guid groupId)
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Leave")]
|
[HttpPost("Leave")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult LeaveGroup()
|
public ActionResult SyncPlayLeaveGroup()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
_syncPlayManager.LeaveGroup(currentSession, CancellationToken.None);
|
_syncPlayManager.LeaveGroup(currentSession, CancellationToken.None);
|
||||||
@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>An <see cref="IEnumerable{GrouüInfoView}"/> containing the available SyncPlay groups.</returns>
|
/// <returns>An <see cref="IEnumerable{GrouüInfoView}"/> containing the available SyncPlay groups.</returns>
|
||||||
[HttpGet("List")]
|
[HttpGet("List")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<IEnumerable<GroupInfoView>> GetSyncPlayGroups([FromQuery] Guid? filterItemId)
|
public ActionResult<IEnumerable<GroupInfoView>> SyncPlayGetGroups([FromQuery] Guid? filterItemId)
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty));
|
return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty));
|
||||||
@ -110,7 +110,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Play")]
|
[HttpPost("Play")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult Play()
|
public ActionResult SyncPlayPlay()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
var syncPlayRequest = new PlaybackRequest()
|
var syncPlayRequest = new PlaybackRequest()
|
||||||
@ -128,7 +128,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Pause")]
|
[HttpPost("Pause")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult Pause()
|
public ActionResult SyncPlayPause()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
var syncPlayRequest = new PlaybackRequest()
|
var syncPlayRequest = new PlaybackRequest()
|
||||||
@ -147,7 +147,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Seek")]
|
[HttpPost("Seek")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult Seek([FromQuery] long positionTicks)
|
public ActionResult SyncPlaySeek([FromQuery] long positionTicks)
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
var syncPlayRequest = new PlaybackRequest()
|
var syncPlayRequest = new PlaybackRequest()
|
||||||
@ -169,7 +169,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Buffering")]
|
[HttpPost("Buffering")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult Buffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone)
|
public ActionResult SyncPlayBuffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone)
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
var syncPlayRequest = new PlaybackRequest()
|
var syncPlayRequest = new PlaybackRequest()
|
||||||
@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Ping")]
|
[HttpPost("Ping")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult Ping([FromQuery] double ping)
|
public ActionResult SyncPlayPing([FromQuery] double ping)
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
var syncPlayRequest = new PlaybackRequest()
|
var syncPlayRequest = new PlaybackRequest()
|
||||||
|
@ -85,8 +85,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Information retrieved.</response>
|
/// <response code="200">Information retrieved.</response>
|
||||||
/// <returns>The server name.</returns>
|
/// <returns>The server name.</returns>
|
||||||
[HttpGet("Ping")]
|
[HttpGet("Ping", Name = "GetPingSystem")]
|
||||||
[HttpPost("Ping")]
|
[HttpPost("Ping", Name = "PostPingSystem")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<string> PingSystem()
|
public ActionResult<string> PingSystem()
|
||||||
{
|
{
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using MediaBrowser.Controller.Dto;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Globalization;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers
|
||||||
{
|
{
|
||||||
@ -18,32 +14,15 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class TrailersController : BaseJellyfinApiController
|
public class TrailersController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IUserManager _userManager;
|
private readonly ItemsController _itemsController;
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly ILogger<ItemsController> _logger;
|
|
||||||
private readonly IDtoService _dtoService;
|
|
||||||
private readonly ILocalizationManager _localizationManager;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TrailersController"/> class.
|
/// Initializes a new instance of the <see cref="TrailersController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
/// <param name="itemsController">Instance of <see cref="ItemsController"/>.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
public TrailersController(ItemsController itemsController)
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
|
||||||
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
|
||||||
public TrailersController(
|
|
||||||
ILoggerFactory loggerFactory,
|
|
||||||
IUserManager userManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IDtoService dtoService,
|
|
||||||
ILocalizationManager localizationManager)
|
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_itemsController = itemsController;
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_dtoService = dtoService;
|
|
||||||
_localizationManager = localizationManager;
|
|
||||||
_logger = loggerFactory.CreateLogger<ItemsController>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -214,12 +193,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
var includeItemTypes = "Trailer";
|
var includeItemTypes = "Trailer";
|
||||||
|
|
||||||
return new ItemsController(
|
return _itemsController
|
||||||
_userManager,
|
|
||||||
_libraryManager,
|
|
||||||
_localizationManager,
|
|
||||||
_dtoService,
|
|
||||||
_logger)
|
|
||||||
.GetItems(
|
.GetItems(
|
||||||
userId,
|
userId,
|
||||||
userId,
|
userId,
|
||||||
|
@ -7,21 +7,12 @@ using System.Threading.Tasks;
|
|||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.Models.VideoDtos;
|
using Jellyfin.Api.Models.VideoDtos;
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
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.Controller.Net;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers
|
||||||
{
|
{
|
||||||
@ -30,72 +21,28 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class UniversalAudioController : BaseJellyfinApiController
|
public class UniversalAudioController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
|
||||||
private readonly IUserManager _userManager;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly IDeviceManager _deviceManager;
|
|
||||||
private readonly IDlnaManager _dlnaManager;
|
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
|
||||||
private readonly IAuthorizationContext _authorizationContext;
|
private readonly IAuthorizationContext _authorizationContext;
|
||||||
private readonly INetworkManager _networkManager;
|
private readonly MediaInfoController _mediaInfoController;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
private readonly DynamicHlsController _dynamicHlsController;
|
||||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
private readonly AudioController _audioController;
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
|
/// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
|
||||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
|
||||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
|
||||||
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
|
||||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
|
||||||
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
/// <param name="mediaInfoController">Instance of the <see cref="MediaInfoController"/>.</param>
|
||||||
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> interface.</param>
|
/// <param name="dynamicHlsController">Instance of the <see cref="DynamicHlsController"/>.</param>
|
||||||
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
|
/// <param name="audioController">Instance of the <see cref="AudioController"/>.</param>
|
||||||
/// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
|
|
||||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
|
||||||
public UniversalAudioController(
|
public UniversalAudioController(
|
||||||
ILoggerFactory loggerFactory,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IUserManager userManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IDlnaManager dlnaManager,
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
|
||||||
IAuthorizationContext authorizationContext,
|
IAuthorizationContext authorizationContext,
|
||||||
INetworkManager networkManager,
|
MediaInfoController mediaInfoController,
|
||||||
TranscodingJobHelper transcodingJobHelper,
|
DynamicHlsController dynamicHlsController,
|
||||||
IConfiguration configuration,
|
AudioController audioController)
|
||||||
ISubtitleEncoder subtitleEncoder,
|
|
||||||
IHttpClientFactory httpClientFactory)
|
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_mediaEncoder = mediaEncoder;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_dlnaManager = dlnaManager;
|
|
||||||
_deviceManager = deviceManager;
|
|
||||||
_mediaSourceManager = mediaSourceManager;
|
|
||||||
_authorizationContext = authorizationContext;
|
_authorizationContext = authorizationContext;
|
||||||
_networkManager = networkManager;
|
_mediaInfoController = mediaInfoController;
|
||||||
_loggerFactory = loggerFactory;
|
_dynamicHlsController = dynamicHlsController;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_audioController = audioController;
|
||||||
_transcodingJobHelper = transcodingJobHelper;
|
|
||||||
_configuration = configuration;
|
|
||||||
_subtitleEncoder = subtitleEncoder;
|
|
||||||
_httpClientFactory = httpClientFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -122,9 +69,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="302">Redirected to remote audio stream.</response>
|
/// <response code="302">Redirected to remote audio stream.</response>
|
||||||
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
|
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
|
||||||
[HttpGet("/Audio/{itemId}/universal")]
|
[HttpGet("/Audio/{itemId}/universal")]
|
||||||
[HttpGet("/Audio/{itemId}/{universal=universal}.{container?}")]
|
[HttpGet("/Audio/{itemId}/{universal=universal}.{container?}", Name = "GetUniversalAudioStream_2")]
|
||||||
[HttpHead("/Audio/{itemId}/universal")]
|
[HttpHead("/Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
|
||||||
[HttpHead("/Audio/{itemId}/{universal=universal}.{container?}")]
|
[HttpHead("/Audio/{itemId}/{universal=universal}.{container?}", Name = "HeadUniversalAudioStream_2")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||||
@ -151,8 +98,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
||||||
_authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
|
_authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
|
||||||
|
|
||||||
var mediaInfoController = new MediaInfoController(_mediaSourceManager, _deviceManager, _libraryManager, _networkManager, _mediaEncoder, _userManager, _authorizationContext, _loggerFactory.CreateLogger<MediaInfoController>(), _serverConfigurationManager);
|
var playbackInfoResult = await _mediaInfoController.GetPostedPlaybackInfo(
|
||||||
var playbackInfoResult = await mediaInfoController.GetPostedPlaybackInfo(
|
|
||||||
itemId,
|
itemId,
|
||||||
userId,
|
userId,
|
||||||
maxStreamingBitrate,
|
maxStreamingBitrate,
|
||||||
@ -180,21 +126,6 @@ namespace Jellyfin.Api.Controllers
|
|||||||
var isStatic = mediaSource.SupportsDirectStream;
|
var isStatic = mediaSource.SupportsDirectStream;
|
||||||
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var dynamicHlsController = new DynamicHlsController(
|
|
||||||
_libraryManager,
|
|
||||||
_userManager,
|
|
||||||
_dlnaManager,
|
|
||||||
_authorizationContext,
|
|
||||||
_mediaSourceManager,
|
|
||||||
_serverConfigurationManager,
|
|
||||||
_mediaEncoder,
|
|
||||||
_fileSystem,
|
|
||||||
_subtitleEncoder,
|
|
||||||
_configuration,
|
|
||||||
_deviceManager,
|
|
||||||
_transcodingJobHelper,
|
|
||||||
_networkManager,
|
|
||||||
_loggerFactory.CreateLogger<DynamicHlsController>());
|
|
||||||
var transcodingProfile = deviceProfile.TranscodingProfiles[0];
|
var transcodingProfile = deviceProfile.TranscodingProfiles[0];
|
||||||
|
|
||||||
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
|
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
|
||||||
@ -203,10 +134,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
if (isHeadRequest)
|
if (isHeadRequest)
|
||||||
{
|
{
|
||||||
dynamicHlsController.Request.Method = HttpMethod.Head.Method;
|
_dynamicHlsController.Request.Method = HttpMethod.Head.Method;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await dynamicHlsController.GetMasterHlsAudioPlaylist(
|
return await _dynamicHlsController.GetMasterHlsAudioPlaylist(
|
||||||
itemId,
|
itemId,
|
||||||
".m3u8",
|
".m3u8",
|
||||||
isStatic,
|
isStatic,
|
||||||
@ -261,27 +192,12 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var audioController = new AudioController(
|
|
||||||
_dlnaManager,
|
|
||||||
_userManager,
|
|
||||||
_authorizationContext,
|
|
||||||
_libraryManager,
|
|
||||||
_mediaSourceManager,
|
|
||||||
_serverConfigurationManager,
|
|
||||||
_mediaEncoder,
|
|
||||||
_fileSystem,
|
|
||||||
_subtitleEncoder,
|
|
||||||
_configuration,
|
|
||||||
_deviceManager,
|
|
||||||
_transcodingJobHelper,
|
|
||||||
_httpClientFactory);
|
|
||||||
|
|
||||||
if (isHeadRequest)
|
if (isHeadRequest)
|
||||||
{
|
{
|
||||||
audioController.Request.Method = HttpMethod.Head.Method;
|
_audioController.Request.Method = HttpMethod.Head.Method;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await audioController.GetAudioStream(
|
return await _audioController.GetAudioStream(
|
||||||
itemId,
|
itemId,
|
||||||
isStatic ? null : ("." + mediaSource.TranscodingContainer),
|
isStatic ? null : ("." + mediaSource.TranscodingContainer),
|
||||||
isStatic,
|
isStatic,
|
||||||
|
@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IDeviceManager _deviceManager;
|
private readonly IDeviceManager _deviceManager;
|
||||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger<VideoHlsController> _logger;
|
||||||
private readonly EncodingOptions _encodingOptions;
|
private readonly EncodingOptions _encodingOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -316,10 +316,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||||
/// <response code="200">Video stream returned.</response>
|
/// <response code="200">Video stream returned.</response>
|
||||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||||
[HttpGet("{itemId}/{stream=stream}.{container?}")]
|
[HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStream_2")]
|
||||||
[HttpGet("{itemId}/stream")]
|
[HttpGet("{itemId}/stream")]
|
||||||
[HttpHead("{itemId}/{stream=stream}.{container?}")]
|
[HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")]
|
||||||
[HttpHead("{itemId}/stream")]
|
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult> GetVideoStream(
|
public async Task<ActionResult> GetVideoStream(
|
||||||
[FromRoute] Guid itemId,
|
[FromRoute] Guid itemId,
|
||||||
|
@ -5,7 +5,6 @@ using System.Net;
|
|||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
24
Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
Normal file
24
Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using MediaBrowser.Model.Dlna;
|
||||||
|
using MediaBrowser.Model.MediaInfo;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Models.MediaInfoDtos
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Open live stream dto.
|
||||||
|
/// </summary>
|
||||||
|
public class OpenLiveStreamDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the device profile.
|
||||||
|
/// </summary>
|
||||||
|
public DeviceProfile? DeviceProfile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the device play protocols.
|
||||||
|
/// </summary>
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1819:DontReturnArrays", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "SA1011:ClosingBracketsSpace", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")]
|
||||||
|
public MediaProtocol[]? DirectPlayProtocols { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -5,34 +5,35 @@ using MediaBrowser.Model.Activity;
|
|||||||
using MediaBrowser.Model.Events;
|
using MediaBrowser.Model.Events;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.Api.System
|
namespace Jellyfin.Api.WebSocketListeners
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class SessionInfoWebSocketListener.
|
/// Class SessionInfoWebSocketListener.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
|
public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets the name.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The name.</value>
|
|
||||||
protected override string Name => "ActivityLogEntry";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _kernel.
|
/// The _kernel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IActivityManager _activityManager;
|
private readonly IActivityManager _activityManager;
|
||||||
|
|
||||||
public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger)
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ActivityLogWebSocketListener"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{ActivityLogWebSocketListener}"/> interface.</param>
|
||||||
|
/// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
|
||||||
|
public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager)
|
||||||
|
: base(logger)
|
||||||
{
|
{
|
||||||
_activityManager = activityManager;
|
_activityManager = activityManager;
|
||||||
_activityManager.EntryCreated += OnEntryCreated;
|
_activityManager.EntryCreated += OnEntryCreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
|
/// <summary>
|
||||||
{
|
/// Gets the name.
|
||||||
SendData(true);
|
/// </summary>
|
||||||
}
|
/// <value>The name.</value>
|
||||||
|
protected override string Name => "ActivityLogEntry";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the data to send.
|
/// Gets the data to send.
|
||||||
@ -50,5 +51,10 @@ namespace MediaBrowser.Api.System
|
|||||||
|
|
||||||
base.Dispose(dispose);
|
base.Dispose(dispose);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
|
||||||
|
{
|
||||||
|
SendData(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,7 +6,7 @@ using MediaBrowser.Model.Events;
|
|||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.Api.ScheduledTasks
|
namespace Jellyfin.Api.WebSocketListeners
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class ScheduledTasksWebSocketListener.
|
/// Class ScheduledTasksWebSocketListener.
|
||||||
@ -17,7 +17,21 @@ namespace MediaBrowser.Api.ScheduledTasks
|
|||||||
/// Gets or sets the task manager.
|
/// Gets or sets the task manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The task manager.</value>
|
/// <value>The task manager.</value>
|
||||||
private ITaskManager TaskManager { get; set; }
|
private readonly ITaskManager _taskManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ScheduledTasksWebSocketListener"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{ScheduledTasksWebSocketListener}"/> interface.</param>
|
||||||
|
/// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
|
||||||
|
public ScheduledTasksWebSocketListener(ILogger<ScheduledTasksWebSocketListener> logger, ITaskManager taskManager)
|
||||||
|
: base(logger)
|
||||||
|
{
|
||||||
|
_taskManager = taskManager;
|
||||||
|
|
||||||
|
_taskManager.TaskExecuting += OnTaskExecuting;
|
||||||
|
_taskManager.TaskCompleted += OnTaskCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the name.
|
/// Gets the name.
|
||||||
@ -25,53 +39,42 @@ namespace MediaBrowser.Api.ScheduledTasks
|
|||||||
/// <value>The name.</value>
|
/// <value>The name.</value>
|
||||||
protected override string Name => "ScheduledTasksInfo";
|
protected override string Name => "ScheduledTasksInfo";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ScheduledTasksWebSocketListener" /> class.
|
|
||||||
/// </summary>
|
|
||||||
public ScheduledTasksWebSocketListener(ILogger<ScheduledTasksWebSocketListener> logger, ITaskManager taskManager)
|
|
||||||
: base(logger)
|
|
||||||
{
|
|
||||||
TaskManager = taskManager;
|
|
||||||
|
|
||||||
TaskManager.TaskExecuting += TaskManager_TaskExecuting;
|
|
||||||
TaskManager.TaskCompleted += TaskManager_TaskCompleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskManager_TaskCompleted(object sender, TaskCompletionEventArgs e)
|
|
||||||
{
|
|
||||||
SendData(true);
|
|
||||||
e.Task.TaskProgress -= Argument_TaskProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskManager_TaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e)
|
|
||||||
{
|
|
||||||
SendData(true);
|
|
||||||
e.Argument.TaskProgress += Argument_TaskProgress;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Argument_TaskProgress(object sender, GenericEventArgs<double> e)
|
|
||||||
{
|
|
||||||
SendData(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the data to send.
|
/// Gets the data to send.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Task{IEnumerable{TaskInfo}}.</returns>
|
/// <returns>Task{IEnumerable{TaskInfo}}.</returns>
|
||||||
protected override Task<IEnumerable<TaskInfo>> GetDataToSend()
|
protected override Task<IEnumerable<TaskInfo>> GetDataToSend()
|
||||||
{
|
{
|
||||||
return Task.FromResult(TaskManager.ScheduledTasks
|
return Task.FromResult(_taskManager.ScheduledTasks
|
||||||
.OrderBy(i => i.Name)
|
.OrderBy(i => i.Name)
|
||||||
.Select(ScheduledTaskHelpers.GetTaskInfo)
|
.Select(ScheduledTaskHelpers.GetTaskInfo)
|
||||||
.Where(i => !i.IsHidden));
|
.Where(i => !i.IsHidden));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Dispose(bool dispose)
|
protected override void Dispose(bool dispose)
|
||||||
{
|
{
|
||||||
TaskManager.TaskExecuting -= TaskManager_TaskExecuting;
|
_taskManager.TaskExecuting -= OnTaskExecuting;
|
||||||
TaskManager.TaskCompleted -= TaskManager_TaskCompleted;
|
_taskManager.TaskCompleted -= OnTaskCompleted;
|
||||||
|
|
||||||
base.Dispose(dispose);
|
base.Dispose(dispose);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
|
||||||
|
{
|
||||||
|
SendData(true);
|
||||||
|
e.Task.TaskProgress -= OnTaskProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTaskExecuting(object sender, GenericEventArgs<IScheduledTaskWorker> e)
|
||||||
|
{
|
||||||
|
SendData(true);
|
||||||
|
e.Argument.TaskProgress += OnTaskProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTaskProgress(object sender, GenericEventArgs<double> e)
|
||||||
|
{
|
||||||
|
SendData(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,27 +5,20 @@ using MediaBrowser.Controller.Net;
|
|||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Sessions
|
namespace Jellyfin.Api.WebSocketListeners
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class SessionInfoWebSocketListener.
|
/// Class SessionInfoWebSocketListener.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
|
public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets the name.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The name.</value>
|
|
||||||
protected override string Name => "Sessions";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _kernel.
|
|
||||||
/// </summary>
|
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SessionInfoWebSocketListener"/> class.
|
/// Initializes a new instance of the <see cref="SessionInfoWebSocketListener"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{SessionInfoWebSocketListener}"/> interface.</param>
|
||||||
|
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||||
public SessionInfoWebSocketListener(ILogger<SessionInfoWebSocketListener> logger, ISessionManager sessionManager)
|
public SessionInfoWebSocketListener(ILogger<SessionInfoWebSocketListener> logger, ISessionManager sessionManager)
|
||||||
: base(logger)
|
: base(logger)
|
||||||
{
|
{
|
||||||
@ -40,6 +33,32 @@ namespace MediaBrowser.Api.Sessions
|
|||||||
_sessionManager.SessionActivity += OnSessionManagerSessionActivity;
|
_sessionManager.SessionActivity += OnSessionManagerSessionActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string Name => "Sessions";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the data to send.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Task{SystemInfo}.</returns>
|
||||||
|
protected override Task<IEnumerable<SessionInfo>> GetDataToSend()
|
||||||
|
{
|
||||||
|
return Task.FromResult(_sessionManager.Sessions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Dispose(bool dispose)
|
||||||
|
{
|
||||||
|
_sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
|
||||||
|
_sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
|
||||||
|
_sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
|
||||||
|
_sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
|
||||||
|
_sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
|
||||||
|
_sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
|
||||||
|
_sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
|
||||||
|
|
||||||
|
base.Dispose(dispose);
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnSessionManagerSessionActivity(object sender, SessionEventArgs e)
|
private async void OnSessionManagerSessionActivity(object sender, SessionEventArgs e)
|
||||||
{
|
{
|
||||||
await SendData(false).ConfigureAwait(false);
|
await SendData(false).ConfigureAwait(false);
|
||||||
@ -74,28 +93,5 @@ namespace MediaBrowser.Api.Sessions
|
|||||||
{
|
{
|
||||||
await SendData(true).ConfigureAwait(false);
|
await SendData(true).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the data to send.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Task{SystemInfo}.</returns>
|
|
||||||
protected override Task<IEnumerable<SessionInfo>> GetDataToSend()
|
|
||||||
{
|
|
||||||
return Task.FromResult(_sessionManager.Sessions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Dispose(bool dispose)
|
|
||||||
{
|
|
||||||
_sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
|
|
||||||
_sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
|
|
||||||
_sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
|
|
||||||
_sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
|
|
||||||
_sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
|
|
||||||
_sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
|
|
||||||
_sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
|
|
||||||
|
|
||||||
base.Dispose(dispose);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -198,8 +198,15 @@ namespace Jellyfin.Server.Extensions
|
|||||||
$"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}");
|
$"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}");
|
||||||
|
|
||||||
// Use method name as operationId
|
// Use method name as operationId
|
||||||
c.CustomOperationIds(description =>
|
c.CustomOperationIds(
|
||||||
description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null);
|
description =>
|
||||||
|
{
|
||||||
|
description.TryGetMethodInfo(out MethodInfo methodInfo);
|
||||||
|
// Attribute name, method name, none.
|
||||||
|
return description?.ActionDescriptor?.AttributeRouteInfo?.Name
|
||||||
|
?? methodInfo?.Name
|
||||||
|
?? null;
|
||||||
|
});
|
||||||
|
|
||||||
// TODO - remove when all types are supported in System.Text.Json
|
// TODO - remove when all types are supported in System.Text.Json
|
||||||
c.AddSwaggerTypeMappings();
|
c.AddSwaggerTypeMappings();
|
||||||
|
@ -1,678 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Api.Playback;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Controller.Plugins;
|
|
||||||
using MediaBrowser.Controller.Session;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Session;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class ServerEntryPoint.
|
|
||||||
/// </summary>
|
|
||||||
public class ApiEntryPoint : IServerEntryPoint
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The instance.
|
|
||||||
/// </summary>
|
|
||||||
public static ApiEntryPoint Instance;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The logger.
|
|
||||||
/// </summary>
|
|
||||||
private ILogger<ApiEntryPoint> _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The configuration manager.
|
|
||||||
/// </summary>
|
|
||||||
private IServerConfigurationManager _serverConfigurationManager;
|
|
||||||
|
|
||||||
private readonly ISessionManager _sessionManager;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The active transcoding jobs.
|
|
||||||
/// </summary>
|
|
||||||
private readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
|
|
||||||
|
|
||||||
private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks =
|
|
||||||
new Dictionary<string, SemaphoreSlim>();
|
|
||||||
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="sessionManager">The session manager.</param>
|
|
||||||
/// <param name="config">The configuration.</param>
|
|
||||||
/// <param name="fileSystem">The file system.</param>
|
|
||||||
/// <param name="mediaSourceManager">The media source manager.</param>
|
|
||||||
public ApiEntryPoint(
|
|
||||||
ILogger<ApiEntryPoint> logger,
|
|
||||||
ISessionManager sessionManager,
|
|
||||||
IServerConfigurationManager config,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IMediaSourceManager mediaSourceManager)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_sessionManager = sessionManager;
|
|
||||||
_serverConfigurationManager = config;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_mediaSourceManager = mediaSourceManager;
|
|
||||||
|
|
||||||
_sessionManager.PlaybackProgress += OnPlaybackProgress;
|
|
||||||
_sessionManager.PlaybackStart += OnPlaybackStart;
|
|
||||||
|
|
||||||
Instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string[] Split(string value, char separator, bool removeEmpty)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
return Array.Empty<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return removeEmpty
|
|
||||||
? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
: value.Split(separator);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SemaphoreSlim GetTranscodingLock(string outputPath)
|
|
||||||
{
|
|
||||||
lock (_transcodingLocks)
|
|
||||||
{
|
|
||||||
if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result))
|
|
||||||
{
|
|
||||||
result = new SemaphoreSlim(1, 1);
|
|
||||||
_transcodingLocks[outputPath] = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
|
|
||||||
{
|
|
||||||
PingTranscodingJob(e.PlaySessionId, e.IsPaused);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
|
|
||||||
{
|
|
||||||
PingTranscodingJob(e.PlaySessionId, e.IsPaused);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runs this instance.
|
|
||||||
/// </summary>
|
|
||||||
public Task RunAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
DeleteEncodedMediaCache();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting encoded media cache");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the encoded media cache.
|
|
||||||
/// </summary>
|
|
||||||
private void DeleteEncodedMediaCache()
|
|
||||||
{
|
|
||||||
var path = _serverConfigurationManager.GetTranscodePath();
|
|
||||||
if (!Directory.Exists(path))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var file in _fileSystem.GetFilePaths(path, true))
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases unmanaged and - optionally - managed resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
||||||
protected virtual void Dispose(bool dispose)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dispose)
|
|
||||||
{
|
|
||||||
// TODO: dispose
|
|
||||||
}
|
|
||||||
|
|
||||||
var jobs = _activeTranscodingJobs.ToList();
|
|
||||||
var jobCount = jobs.Count;
|
|
||||||
|
|
||||||
IEnumerable<Task> GetKillJobs()
|
|
||||||
{
|
|
||||||
foreach (var job in jobs)
|
|
||||||
{
|
|
||||||
yield return KillTranscodingJob(job, false, path => true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all processes to be killed
|
|
||||||
if (jobCount > 0)
|
|
||||||
{
|
|
||||||
Task.WaitAll(GetKillJobs().ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
_activeTranscodingJobs.Clear();
|
|
||||||
_transcodingLocks.Clear();
|
|
||||||
|
|
||||||
_sessionManager.PlaybackProgress -= OnPlaybackProgress;
|
|
||||||
_sessionManager.PlaybackStart -= OnPlaybackStart;
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when [transcode beginning].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <param name="playSessionId">The play session identifier.</param>
|
|
||||||
/// <param name="liveStreamId">The live stream identifier.</param>
|
|
||||||
/// <param name="transcodingJobId">The transcoding job identifier.</param>
|
|
||||||
/// <param name="type">The type.</param>
|
|
||||||
/// <param name="process">The process.</param>
|
|
||||||
/// <param name="deviceId">The device id.</param>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
|
||||||
/// <returns>TranscodingJob.</returns>
|
|
||||||
public TranscodingJob OnTranscodeBeginning(
|
|
||||||
string path,
|
|
||||||
string playSessionId,
|
|
||||||
string liveStreamId,
|
|
||||||
string transcodingJobId,
|
|
||||||
TranscodingJobType type,
|
|
||||||
Process process,
|
|
||||||
string deviceId,
|
|
||||||
StreamState state,
|
|
||||||
CancellationTokenSource cancellationTokenSource)
|
|
||||||
{
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
var job = new TranscodingJob(_logger)
|
|
||||||
{
|
|
||||||
Type = type,
|
|
||||||
Path = path,
|
|
||||||
Process = process,
|
|
||||||
ActiveRequestCount = 1,
|
|
||||||
DeviceId = deviceId,
|
|
||||||
CancellationTokenSource = cancellationTokenSource,
|
|
||||||
Id = transcodingJobId,
|
|
||||||
PlaySessionId = playSessionId,
|
|
||||||
LiveStreamId = liveStreamId,
|
|
||||||
MediaSource = state.MediaSource
|
|
||||||
};
|
|
||||||
|
|
||||||
_activeTranscodingJobs.Add(job);
|
|
||||||
|
|
||||||
ReportTranscodingProgress(job, state, null, null, null, null, null);
|
|
||||||
|
|
||||||
return job;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
|
|
||||||
{
|
|
||||||
var ticks = transcodingPosition?.Ticks;
|
|
||||||
|
|
||||||
if (job != null)
|
|
||||||
{
|
|
||||||
job.Framerate = framerate;
|
|
||||||
job.CompletionPercentage = percentComplete;
|
|
||||||
job.TranscodingPositionTicks = ticks;
|
|
||||||
job.BytesTranscoded = bytesTranscoded;
|
|
||||||
job.BitRate = bitRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
var deviceId = state.Request.DeviceId;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(deviceId))
|
|
||||||
{
|
|
||||||
var audioCodec = state.ActualOutputAudioCodec;
|
|
||||||
var videoCodec = state.ActualOutputVideoCodec;
|
|
||||||
|
|
||||||
_sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
|
|
||||||
{
|
|
||||||
Bitrate = bitRate ?? state.TotalOutputBitrate,
|
|
||||||
AudioCodec = audioCodec,
|
|
||||||
VideoCodec = videoCodec,
|
|
||||||
Container = state.OutputContainer,
|
|
||||||
Framerate = framerate,
|
|
||||||
CompletionPercentage = percentComplete,
|
|
||||||
Width = state.OutputWidth,
|
|
||||||
Height = state.OutputHeight,
|
|
||||||
AudioChannels = state.OutputAudioChannels,
|
|
||||||
IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
|
|
||||||
IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
|
|
||||||
TranscodeReasons = state.TranscodeReasons
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <summary>
|
|
||||||
/// The progressive.
|
|
||||||
/// </summary>
|
|
||||||
/// Called when [transcode failed to start].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <param name="type">The type.</param>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
|
|
||||||
{
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (job != null)
|
|
||||||
{
|
|
||||||
_activeTranscodingJobs.Remove(job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_transcodingLocks)
|
|
||||||
{
|
|
||||||
_transcodingLocks.Remove(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
|
|
||||||
{
|
|
||||||
_sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether [has active transcoding job] [the specified path].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <param name="type">The type.</param>
|
|
||||||
/// <returns><c>true</c> if [has active transcoding job] [the specified path]; otherwise, <c>false</c>.</returns>
|
|
||||||
public bool HasActiveTranscodingJob(string path, TranscodingJobType type)
|
|
||||||
{
|
|
||||||
return GetTranscodingJob(path, type) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
|
|
||||||
{
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public TranscodingJob GetTranscodingJob(string playSessionId)
|
|
||||||
{
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when [transcode begin request].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <param name="type">The type.</param>
|
|
||||||
public TranscodingJob OnTranscodeBeginRequest(string path, TranscodingJobType type)
|
|
||||||
{
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (job == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnTranscodeBeginRequest(job);
|
|
||||||
|
|
||||||
return job;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnTranscodeBeginRequest(TranscodingJob job)
|
|
||||||
{
|
|
||||||
job.ActiveRequestCount++;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
|
|
||||||
{
|
|
||||||
job.StopKillTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnTranscodeEndRequest(TranscodingJob job)
|
|
||||||
{
|
|
||||||
job.ActiveRequestCount--;
|
|
||||||
_logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
|
|
||||||
if (job.ActiveRequestCount <= 0)
|
|
||||||
{
|
|
||||||
PingTimer(job, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void PingTranscodingJob(string playSessionId, bool? isUserPaused)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(playSessionId))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(playSessionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
|
|
||||||
|
|
||||||
List<TranscodingJob> jobs;
|
|
||||||
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
// This is really only needed for HLS.
|
|
||||||
// Progressive streams can stop on their own reliably
|
|
||||||
jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var job in jobs)
|
|
||||||
{
|
|
||||||
if (isUserPaused.HasValue)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
|
|
||||||
job.IsUserPaused = isUserPaused.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
PingTimer(job, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
|
|
||||||
{
|
|
||||||
if (job.HasExited)
|
|
||||||
{
|
|
||||||
job.StopKillTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var timerDuration = 10000;
|
|
||||||
|
|
||||||
if (job.Type != TranscodingJobType.Progressive)
|
|
||||||
{
|
|
||||||
timerDuration = 60000;
|
|
||||||
}
|
|
||||||
|
|
||||||
job.PingTimeout = timerDuration;
|
|
||||||
job.LastPingDate = DateTime.UtcNow;
|
|
||||||
|
|
||||||
// Don't start the timer for playback checkins with progressive streaming
|
|
||||||
if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
|
|
||||||
{
|
|
||||||
job.StartKillTimer(OnTranscodeKillTimerStopped);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
job.ChangeKillTimerIfStarted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when [transcode kill timer stopped].
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
private async void OnTranscodeKillTimerStopped(object state)
|
|
||||||
{
|
|
||||||
var job = (TranscodingJob)state;
|
|
||||||
|
|
||||||
if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
|
|
||||||
{
|
|
||||||
var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
|
|
||||||
|
|
||||||
if (timeSinceLastPing < job.PingTimeout)
|
|
||||||
{
|
|
||||||
job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
|
|
||||||
|
|
||||||
await KillTranscodingJob(job, true, path => true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Kills the single transcoding job.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="deviceId">The device id.</param>
|
|
||||||
/// <param name="playSessionId">The play session identifier.</param>
|
|
||||||
/// <param name="deleteFiles">The delete files.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
internal Task KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
|
|
||||||
{
|
|
||||||
return KillTranscodingJobs(j => string.IsNullOrWhiteSpace(playSessionId)
|
|
||||||
? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
|
|
||||||
: string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase), deleteFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Kills the transcoding jobs.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="killJob">The kill job.</param>
|
|
||||||
/// <param name="deleteFiles">The delete files.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
private Task KillTranscodingJobs(Func<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles)
|
|
||||||
{
|
|
||||||
var jobs = new List<TranscodingJob>();
|
|
||||||
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
// This is really only needed for HLS.
|
|
||||||
// Progressive streams can stop on their own reliably
|
|
||||||
jobs.AddRange(_activeTranscodingJobs.Where(killJob));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jobs.Count == 0)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerable<Task> GetKillJobs()
|
|
||||||
{
|
|
||||||
foreach (var job in jobs)
|
|
||||||
{
|
|
||||||
yield return KillTranscodingJob(job, false, deleteFiles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.WhenAll(GetKillJobs());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Kills the transcoding job.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="job">The job.</param>
|
|
||||||
/// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
|
|
||||||
/// <param name="delete">The delete.</param>
|
|
||||||
private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
|
|
||||||
{
|
|
||||||
job.DisposeKillTimer();
|
|
||||||
|
|
||||||
_logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
|
|
||||||
|
|
||||||
lock (_activeTranscodingJobs)
|
|
||||||
{
|
|
||||||
_activeTranscodingJobs.Remove(job);
|
|
||||||
|
|
||||||
if (!job.CancellationTokenSource.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
job.CancellationTokenSource.Cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_transcodingLocks)
|
|
||||||
{
|
|
||||||
_transcodingLocks.Remove(job.Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (job.ProcessLock)
|
|
||||||
{
|
|
||||||
job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
var process = job.Process;
|
|
||||||
|
|
||||||
var hasExited = job.HasExited;
|
|
||||||
|
|
||||||
if (!hasExited)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
|
|
||||||
|
|
||||||
process.StandardInput.WriteLine("q");
|
|
||||||
|
|
||||||
// Need to wait because killing is asynchronous
|
|
||||||
if (!process.WaitForExit(5000))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
|
|
||||||
process.Kill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delete(job.Path))
|
|
||||||
{
|
|
||||||
await DeletePartialStreamFiles(job.Path, job.Type, 0, 1500).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
|
|
||||||
{
|
|
||||||
if (retryCount >= 10)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Deleting partial stream file(s) {Path}", path);
|
|
||||||
|
|
||||||
await Task.Delay(delayMs).ConfigureAwait(false);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (jobType == TranscodingJobType.Progressive)
|
|
||||||
{
|
|
||||||
DeleteProgressivePartialStreamFiles(path);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DeleteHlsPartialStreamFiles(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
|
|
||||||
|
|
||||||
await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the progressive partial stream files.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="outputFilePath">The output file path.</param>
|
|
||||||
private void DeleteProgressivePartialStreamFiles(string outputFilePath)
|
|
||||||
{
|
|
||||||
if (File.Exists(outputFilePath))
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(outputFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes the HLS partial stream files.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="outputFilePath">The output file path.</param>
|
|
||||||
private void DeleteHlsPartialStreamFiles(string outputFilePath)
|
|
||||||
{
|
|
||||||
var directory = Path.GetDirectoryName(outputFilePath);
|
|
||||||
var name = Path.GetFileNameWithoutExtension(outputFilePath);
|
|
||||||
|
|
||||||
var filesToDelete = _fileSystem.GetFilePaths(directory)
|
|
||||||
.Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
|
|
||||||
|
|
||||||
List<Exception> exs = null;
|
|
||||||
foreach (var file in filesToDelete)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Deleting HLS file {0}", file);
|
|
||||||
_fileSystem.DeleteFile(file);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
(exs ??= new List<Exception>(4)).Add(ex);
|
|
||||||
_logger.LogError(ex, "Error deleting HLS file {Path}", file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exs != null)
|
|
||||||
{
|
|
||||||
throw new AggregateException("Error deleting HLS files", exs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,416 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using Jellyfin.Data.Enums;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Dto;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Net;
|
|
||||||
using MediaBrowser.Controller.Session;
|
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class BaseApiService.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseApiService : IService, IRequiresRequest
|
|
||||||
{
|
|
||||||
public BaseApiService(
|
|
||||||
ILogger<BaseApiService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory)
|
|
||||||
{
|
|
||||||
Logger = logger;
|
|
||||||
ServerConfigurationManager = serverConfigurationManager;
|
|
||||||
ResultFactory = httpResultFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the logger.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The logger.</value>
|
|
||||||
protected ILogger<BaseApiService> Logger { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the server configuration manager.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The server configuration manager.</value>
|
|
||||||
protected IServerConfigurationManager ServerConfigurationManager { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the HTTP result factory.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The HTTP result factory.</value>
|
|
||||||
protected IHttpResultFactory ResultFactory { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the request context.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The request context.</value>
|
|
||||||
public IRequest Request { get; set; }
|
|
||||||
|
|
||||||
public string GetHeader(string name) => Request.Headers[name];
|
|
||||||
|
|
||||||
public static string[] SplitValue(string value, char delim)
|
|
||||||
{
|
|
||||||
return value == null
|
|
||||||
? Array.Empty<string>()
|
|
||||||
: value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Guid[] GetGuids(string value)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
return Array.Empty<Guid>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(i => new Guid(i))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// To the optimized result.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="result">The result.</param>
|
|
||||||
/// <returns>System.Object.</returns>
|
|
||||||
protected object ToOptimizedResult<T>(T result)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
return ResultFactory.GetResult(Request, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void AssertCanUpdateUser(IAuthorizationContext authContext, IUserManager userManager, Guid userId, bool restrictUserPreferences)
|
|
||||||
{
|
|
||||||
var auth = authContext.GetAuthorizationInfo(Request);
|
|
||||||
|
|
||||||
var authenticatedUser = auth.User;
|
|
||||||
|
|
||||||
// If they're going to update the record of another user, they must be an administrator
|
|
||||||
if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator))
|
|
||||||
|| (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess))
|
|
||||||
{
|
|
||||||
throw new SecurityException("Unauthorized access.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the session.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>SessionInfo.</returns>
|
|
||||||
protected SessionInfo GetSession(ISessionContext sessionContext)
|
|
||||||
{
|
|
||||||
var session = sessionContext.GetSession(Request);
|
|
||||||
|
|
||||||
if (session == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Session not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected DtoOptions GetDtoOptions(IAuthorizationContext authContext, object request)
|
|
||||||
{
|
|
||||||
var options = new DtoOptions();
|
|
||||||
|
|
||||||
if (request is IHasItemFields hasFields)
|
|
||||||
{
|
|
||||||
options.Fields = hasFields.GetItemFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.ContainsField(ItemFields.RecursiveItemCount)
|
|
||||||
|| !options.ContainsField(ItemFields.ChildCount))
|
|
||||||
{
|
|
||||||
var client = authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
|
|
||||||
if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
int oldLen = options.Fields.Length;
|
|
||||||
var arr = new ItemFields[oldLen + 1];
|
|
||||||
options.Fields.CopyTo(arr, 0);
|
|
||||||
arr[oldLen] = ItemFields.RecursiveItemCount;
|
|
||||||
options.Fields = arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
|
|
||||||
int oldLen = options.Fields.Length;
|
|
||||||
var arr = new ItemFields[oldLen + 1];
|
|
||||||
options.Fields.CopyTo(arr, 0);
|
|
||||||
arr[oldLen] = ItemFields.ChildCount;
|
|
||||||
options.Fields = arr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request is IHasDtoOptions hasDtoOptions)
|
|
||||||
{
|
|
||||||
options.EnableImages = hasDtoOptions.EnableImages ?? true;
|
|
||||||
|
|
||||||
if (hasDtoOptions.ImageTypeLimit.HasValue)
|
|
||||||
{
|
|
||||||
options.ImageTypeLimit = hasDtoOptions.ImageTypeLimit.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasDtoOptions.EnableUserData.HasValue)
|
|
||||||
{
|
|
||||||
options.EnableUserData = hasDtoOptions.EnableUserData.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
|
|
||||||
{
|
|
||||||
options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MusicArtist GetArtist(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (name.IndexOf(BaseItem.SlugChar) != -1)
|
|
||||||
{
|
|
||||||
var result = GetItemFromSlugName<MusicArtist>(libraryManager, name, dtoOptions);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return libraryManager.GetArtist(name, dtoOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Studio GetStudio(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (name.IndexOf(BaseItem.SlugChar) != -1)
|
|
||||||
{
|
|
||||||
var result = GetItemFromSlugName<Studio>(libraryManager, name, dtoOptions);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return libraryManager.GetStudio(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Genre GetGenre(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (name.IndexOf(BaseItem.SlugChar) != -1)
|
|
||||||
{
|
|
||||||
var result = GetItemFromSlugName<Genre>(libraryManager, name, dtoOptions);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return libraryManager.GetGenre(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MusicGenre GetMusicGenre(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (name.IndexOf(BaseItem.SlugChar) != -1)
|
|
||||||
{
|
|
||||||
var result = GetItemFromSlugName<MusicGenre>(libraryManager, name, dtoOptions);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return libraryManager.GetMusicGenre(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Person GetPerson(string name, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (name.IndexOf(BaseItem.SlugChar) != -1)
|
|
||||||
{
|
|
||||||
var result = GetItemFromSlugName<Person>(libraryManager, name, dtoOptions);
|
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return libraryManager.GetPerson(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
|
|
||||||
where T : BaseItem, new()
|
|
||||||
{
|
|
||||||
var result = libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
Name = name.Replace(BaseItem.SlugChar, '&'),
|
|
||||||
IncludeItemTypes = new[] { typeof(T).Name },
|
|
||||||
DtoOptions = dtoOptions
|
|
||||||
}).OfType<T>().FirstOrDefault();
|
|
||||||
|
|
||||||
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
Name = name.Replace(BaseItem.SlugChar, '/'),
|
|
||||||
IncludeItemTypes = new[] { typeof(T).Name },
|
|
||||||
DtoOptions = dtoOptions
|
|
||||||
}).OfType<T>().FirstOrDefault();
|
|
||||||
|
|
||||||
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
Name = name.Replace(BaseItem.SlugChar, '?'),
|
|
||||||
IncludeItemTypes = new[] { typeof(T).Name },
|
|
||||||
DtoOptions = dtoOptions
|
|
||||||
}).OfType<T>().FirstOrDefault();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the path segment at the specified index.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The index of the path segment.</param>
|
|
||||||
/// <returns>The path segment at the specified index.</returns>
|
|
||||||
/// <exception cref="IndexOutOfRangeException" >Path doesn't contain enough segments.</exception>
|
|
||||||
/// <exception cref="InvalidDataException" >Path doesn't start with the base url.</exception>
|
|
||||||
protected internal ReadOnlySpan<char> GetPathValue(int index)
|
|
||||||
{
|
|
||||||
static void ThrowIndexOutOfRangeException()
|
|
||||||
=> throw new IndexOutOfRangeException("Path doesn't contain enough segments.");
|
|
||||||
|
|
||||||
static void ThrowInvalidDataException()
|
|
||||||
=> throw new InvalidDataException("Path doesn't start with the base url.");
|
|
||||||
|
|
||||||
ReadOnlySpan<char> path = Request.PathInfo;
|
|
||||||
|
|
||||||
// Remove the protocol part from the url
|
|
||||||
int pos = path.LastIndexOf("://");
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(pos + 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the query string
|
|
||||||
pos = path.LastIndexOf('?');
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(0, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the domain
|
|
||||||
pos = path.IndexOf('/');
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove base url
|
|
||||||
string baseUrl = ServerConfigurationManager.Configuration.BaseUrl;
|
|
||||||
int baseUrlLen = baseUrl.Length;
|
|
||||||
if (baseUrlLen != 0)
|
|
||||||
{
|
|
||||||
if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
path = path.Slice(baseUrlLen);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// The path doesn't start with the base url,
|
|
||||||
// how did we get here?
|
|
||||||
ThrowInvalidDataException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove leading /
|
|
||||||
path = path.Slice(1);
|
|
||||||
|
|
||||||
// Backwards compatibility
|
|
||||||
const string Emby = "emby/";
|
|
||||||
if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
path = path.Slice(Emby.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const string MediaBrowser = "mediabrowser/";
|
|
||||||
if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
path = path.Slice(MediaBrowser.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip segments until we are at the right index
|
|
||||||
for (int i = 0; i < index; i++)
|
|
||||||
{
|
|
||||||
pos = path.IndexOf('/');
|
|
||||||
if (pos == -1)
|
|
||||||
{
|
|
||||||
ThrowIndexOutOfRangeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
path = path.Slice(pos + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the rest
|
|
||||||
pos = path.IndexOf('/');
|
|
||||||
if (pos != -1)
|
|
||||||
{
|
|
||||||
path = path.Slice(0, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the name of the item by.
|
|
||||||
/// </summary>
|
|
||||||
protected BaseItem GetItemByName(string name, string type, ILibraryManager libraryManager, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
if (type.Equals("Person", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetPerson(name, libraryManager, dtoOptions);
|
|
||||||
}
|
|
||||||
else if (type.Equals("Artist", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetArtist(name, libraryManager, dtoOptions);
|
|
||||||
}
|
|
||||||
else if (type.Equals("Genre", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetGenre(name, libraryManager, dtoOptions);
|
|
||||||
}
|
|
||||||
else if (type.Equals("MusicGenre", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetMusicGenre(name, libraryManager, dtoOptions);
|
|
||||||
}
|
|
||||||
else if (type.Equals("Studio", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetStudio(name, libraryManager, dtoOptions);
|
|
||||||
}
|
|
||||||
else if (type.Equals("Year", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return libraryManager.GetYear(int.Parse(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentException("Invalid type", nameof(type));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
public interface IHasDtoOptions : IHasItemFields
|
|
||||||
{
|
|
||||||
bool? EnableImages { get; set; }
|
|
||||||
|
|
||||||
bool? EnableUserData { get; set; }
|
|
||||||
|
|
||||||
int? ImageTypeLimit { get; set; }
|
|
||||||
|
|
||||||
string EnableImageTypes { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interface IHasItemFields.
|
|
||||||
/// </summary>
|
|
||||||
public interface IHasItemFields
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the fields.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The fields.</value>
|
|
||||||
string Fields { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Class ItemFieldsExtensions.
|
|
||||||
/// </summary>
|
|
||||||
public static class ItemFieldsExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the item fields.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <returns>IEnumerable{ItemFields}.</returns>
|
|
||||||
public static ItemFields[] GetItemFields(this IHasItemFields request)
|
|
||||||
{
|
|
||||||
var val = request.Fields;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(val))
|
|
||||||
{
|
|
||||||
return Array.Empty<ItemFields>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return val.Split(',').Select(v =>
|
|
||||||
{
|
|
||||||
if (Enum.TryParse(v, true, out ItemFields value))
|
|
||||||
{
|
|
||||||
return (ItemFields?)value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}).Where(i => i.HasValue).Select(i => i.Value).ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<ProjectGuid>{4FD51AC5-2C16-4308-A993-C3A84F3B4582}</ProjectGuid>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
|
||||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="..\SharedVersion.cs" />
|
|
||||||
<Compile Remove="Images\ImageService.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
File diff suppressed because it is too large
Load Diff
@ -1,344 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
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.IO;
|
|
||||||
using MediaBrowser.Model.Net;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback.Hls
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class BaseHlsService.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseHlsService : BaseStreamingService
|
|
||||||
{
|
|
||||||
public BaseHlsService(
|
|
||||||
ILogger<BaseHlsService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory,
|
|
||||||
IUserManager userManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IIsoManager isoManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IDlnaManager dlnaManager,
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IAuthorizationContext authorizationContext,
|
|
||||||
EncodingHelper encodingHelper)
|
|
||||||
: base(
|
|
||||||
logger,
|
|
||||||
serverConfigurationManager,
|
|
||||||
httpResultFactory,
|
|
||||||
userManager,
|
|
||||||
libraryManager,
|
|
||||||
isoManager,
|
|
||||||
mediaEncoder,
|
|
||||||
fileSystem,
|
|
||||||
dlnaManager,
|
|
||||||
deviceManager,
|
|
||||||
mediaSourceManager,
|
|
||||||
jsonSerializer,
|
|
||||||
authorizationContext,
|
|
||||||
encodingHelper)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the audio arguments.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract string GetAudioArguments(StreamState state, EncodingOptions encodingOptions);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the video arguments.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract string GetVideoArguments(StreamState state, EncodingOptions encodingOptions);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the segment file extension.
|
|
||||||
/// </summary>
|
|
||||||
protected string GetSegmentFileExtension(StreamRequest request)
|
|
||||||
{
|
|
||||||
var segmentContainer = request.SegmentContainer;
|
|
||||||
if (!string.IsNullOrWhiteSpace(segmentContainer))
|
|
||||||
{
|
|
||||||
return "." + segmentContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ".ts";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the type of the transcoding job.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The type of the transcoding job.</value>
|
|
||||||
protected override TranscodingJobType TranscodingJobType => TranscodingJobType.Hls;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes the request async.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <param name="isLive">if set to <c>true</c> [is live].</param>
|
|
||||||
/// <returns>Task{System.Object}.</returns>
|
|
||||||
/// <exception cref="ArgumentException">A video bitrate is required
|
|
||||||
/// or
|
|
||||||
/// An audio bitrate is required</exception>
|
|
||||||
protected async Task<object> ProcessRequestAsync(StreamRequest request, bool isLive)
|
|
||||||
{
|
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
TranscodingJob job = null;
|
|
||||||
var playlist = state.OutputFilePath;
|
|
||||||
|
|
||||||
if (!File.Exists(playlist))
|
|
||||||
{
|
|
||||||
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlist);
|
|
||||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!File.Exists(playlist))
|
|
||||||
{
|
|
||||||
// If the playlist doesn't already exist, startup ffmpeg
|
|
||||||
try
|
|
||||||
{
|
|
||||||
job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
|
|
||||||
job.IsLiveOutput = isLive;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
state.Dispose();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
var minSegments = state.MinSegments;
|
|
||||||
if (minSegments > 0)
|
|
||||||
{
|
|
||||||
await WaitForMinimumSegmentCount(playlist, minSegments, cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
transcodingLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLive)
|
|
||||||
{
|
|
||||||
job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
|
||||||
|
|
||||||
if (job != null)
|
|
||||||
{
|
|
||||||
ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResultFactory.GetResult(GetLivePlaylistText(playlist, state.SegmentLength), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioBitrate = state.OutputAudioBitrate ?? 0;
|
|
||||||
var videoBitrate = state.OutputVideoBitrate ?? 0;
|
|
||||||
|
|
||||||
var baselineStreamBitrate = 64000;
|
|
||||||
|
|
||||||
var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, baselineStreamBitrate);
|
|
||||||
|
|
||||||
job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
|
||||||
|
|
||||||
if (job != null)
|
|
||||||
{
|
|
||||||
ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetLivePlaylistText(string path, int segmentLength)
|
|
||||||
{
|
|
||||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
||||||
using var reader = new StreamReader(stream);
|
|
||||||
|
|
||||||
var text = reader.ReadToEnd();
|
|
||||||
|
|
||||||
text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT");
|
|
||||||
|
|
||||||
var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
|
|
||||||
// text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, int baselineStreamBitrate)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
|
|
||||||
builder.AppendLine("#EXTM3U");
|
|
||||||
|
|
||||||
// Pad a little to satisfy the apple hls validator
|
|
||||||
var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
|
|
||||||
|
|
||||||
// Main stream
|
|
||||||
builder.Append("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=")
|
|
||||||
.AppendLine(paddedBitrate.ToString(CultureInfo.InvariantCulture));
|
|
||||||
var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
|
|
||||||
builder.AppendLine(playlistUrl);
|
|
||||||
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist);
|
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
|
|
||||||
var fileStream = GetPlaylistFileStream(playlist);
|
|
||||||
await using (fileStream.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
using var reader = new StreamReader(fileStream);
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
while (!reader.EndOfStream)
|
|
||||||
{
|
|
||||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
count++;
|
|
||||||
if (count >= segmentCount)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
// May get an error if the file is locked
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Stream GetPlaylistFileStream(string path)
|
|
||||||
{
|
|
||||||
return new FileStream(
|
|
||||||
path,
|
|
||||||
FileMode.Open,
|
|
||||||
FileAccess.Read,
|
|
||||||
FileShare.ReadWrite,
|
|
||||||
IODefaults.FileStreamBufferSize,
|
|
||||||
FileOptions.SequentialScan);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
|
|
||||||
{
|
|
||||||
var itsOffsetMs = 0;
|
|
||||||
|
|
||||||
var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(CultureInfo.InvariantCulture));
|
|
||||||
|
|
||||||
var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
|
|
||||||
|
|
||||||
var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
|
|
||||||
|
|
||||||
var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
|
|
||||||
|
|
||||||
// If isEncoding is true we're actually starting ffmpeg
|
|
||||||
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(CultureInfo.InvariantCulture) : "0";
|
|
||||||
|
|
||||||
var baseUrlParam = string.Empty;
|
|
||||||
|
|
||||||
if (state.Request is GetLiveHlsStream)
|
|
||||||
{
|
|
||||||
baseUrlParam = string.Format(" -hls_base_url \"{0}/\"",
|
|
||||||
"hls/" + Path.GetFileNameWithoutExtension(outputPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
var useGenericSegmenter = true;
|
|
||||||
if (useGenericSegmenter)
|
|
||||||
{
|
|
||||||
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
|
|
||||||
|
|
||||||
var timeDeltaParam = string.Empty;
|
|
||||||
|
|
||||||
var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
|
|
||||||
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
segmentFormat = "mpegts";
|
|
||||||
}
|
|
||||||
|
|
||||||
baseUrlParam = string.Format("\"{0}/\"", "hls/" + Path.GetFileNameWithoutExtension(outputPath));
|
|
||||||
|
|
||||||
return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_entry_prefix {12} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
|
|
||||||
inputModifier,
|
|
||||||
EncodingHelper.GetInputArgument(state, encodingOptions),
|
|
||||||
threads,
|
|
||||||
EncodingHelper.GetMapArgs(state),
|
|
||||||
GetVideoArguments(state, encodingOptions),
|
|
||||||
GetAudioArguments(state, encodingOptions),
|
|
||||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
|
|
||||||
startNumberParam,
|
|
||||||
outputPath,
|
|
||||||
outputTsArg,
|
|
||||||
timeDeltaParam,
|
|
||||||
segmentFormat,
|
|
||||||
baseUrlParam
|
|
||||||
).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// add when stream copying?
|
|
||||||
// -avoid_negative_ts make_zero -fflags +genpts
|
|
||||||
|
|
||||||
var args = string.Format("{0} {1} {2} -map_metadata -1 -map_chapters -1 -threads {3} {4} {5} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero {6} -hls_time {7} -individual_header_trailer 0 -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
|
|
||||||
itsOffset,
|
|
||||||
inputModifier,
|
|
||||||
EncodingHelper.GetInputArgument(state, encodingOptions),
|
|
||||||
threads,
|
|
||||||
EncodingHelper.GetMapArgs(state),
|
|
||||||
GetVideoArguments(state, encodingOptions),
|
|
||||||
GetAudioArguments(state, encodingOptions),
|
|
||||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
|
|
||||||
startNumberParam,
|
|
||||||
state.HlsListSize.ToString(CultureInfo.InvariantCulture),
|
|
||||||
baseUrlParam,
|
|
||||||
outputPath
|
|
||||||
).Trim();
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetDefaultEncoderPreset()
|
|
||||||
{
|
|
||||||
return "veryfast";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual int GetStartNumber(StreamState state)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,126 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get various codec strings for use in HLS playlists.
|
|
||||||
/// </summary>
|
|
||||||
static class HlsCodecStringFactory
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a MP3 codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>MP3 codec string.</returns>
|
|
||||||
public static string GetMP3String()
|
|
||||||
{
|
|
||||||
return "mp4a.40.34";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets an AAC codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="profile">AAC profile.</param>
|
|
||||||
/// <returns>AAC codec string.</returns>
|
|
||||||
public static string GetAACString(string profile)
|
|
||||||
{
|
|
||||||
StringBuilder result = new StringBuilder("mp4a", 9);
|
|
||||||
|
|
||||||
if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.Append(".40.5");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Default to LC if profile is invalid
|
|
||||||
result.Append(".40.2");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a H.264 codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="profile">H.264 profile.</param>
|
|
||||||
/// <param name="level">H.264 level.</param>
|
|
||||||
/// <returns>H.264 string.</returns>
|
|
||||||
public static string GetH264String(string profile, int level)
|
|
||||||
{
|
|
||||||
StringBuilder result = new StringBuilder("avc1", 11);
|
|
||||||
|
|
||||||
if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.Append(".6400");
|
|
||||||
}
|
|
||||||
else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.Append(".4D40");
|
|
||||||
}
|
|
||||||
else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.Append(".42E0");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Default to constrained baseline if profile is invalid
|
|
||||||
result.Append(".4240");
|
|
||||||
}
|
|
||||||
|
|
||||||
string levelHex = level.ToString("X2");
|
|
||||||
result.Append(levelHex);
|
|
||||||
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a H.265 codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="profile">H.265 profile.</param>
|
|
||||||
/// <param name="level">H.265 level.</param>
|
|
||||||
/// <returns>H.265 string.</returns>
|
|
||||||
public static string GetH265String(string profile, int level)
|
|
||||||
{
|
|
||||||
// The h265 syntax is a bit of a mystery at the time this comment was written.
|
|
||||||
// This is what I've found through various sources:
|
|
||||||
// FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
|
|
||||||
StringBuilder result = new StringBuilder("hev1", 16);
|
|
||||||
|
|
||||||
if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.Append(".2.6");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Default to main if profile is invalid
|
|
||||||
result.Append(".1.6");
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Append(".L")
|
|
||||||
.Append(level * 3)
|
|
||||||
.Append(".B0");
|
|
||||||
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets an AC-3 codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>AC-3 codec string.</returns>
|
|
||||||
public static string GetAC3String()
|
|
||||||
{
|
|
||||||
return "mp4a.a5";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets an E-AC-3 codec string.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>E-AC-3 codec string.</returns>
|
|
||||||
public static string GetEAC3String()
|
|
||||||
{
|
|
||||||
return "mp4a.a6";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
namespace MediaBrowser.Api.Playback.Hls
|
|
||||||
{
|
|
||||||
public class GetLiveHlsStream : VideoStreamRequest
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,442 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
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.IO;
|
|
||||||
using MediaBrowser.Model.MediaInfo;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback.Progressive
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class BaseProgressiveStreamingService.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseProgressiveStreamingService : BaseStreamingService
|
|
||||||
{
|
|
||||||
protected IHttpClient HttpClient { get; private set; }
|
|
||||||
|
|
||||||
public BaseProgressiveStreamingService(
|
|
||||||
ILogger<BaseProgressiveStreamingService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory,
|
|
||||||
IHttpClient httpClient,
|
|
||||||
IUserManager userManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IIsoManager isoManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IDlnaManager dlnaManager,
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IAuthorizationContext authorizationContext,
|
|
||||||
EncodingHelper encodingHelper)
|
|
||||||
: base(
|
|
||||||
logger,
|
|
||||||
serverConfigurationManager,
|
|
||||||
httpResultFactory,
|
|
||||||
userManager,
|
|
||||||
libraryManager,
|
|
||||||
isoManager,
|
|
||||||
mediaEncoder,
|
|
||||||
fileSystem,
|
|
||||||
dlnaManager,
|
|
||||||
deviceManager,
|
|
||||||
mediaSourceManager,
|
|
||||||
jsonSerializer,
|
|
||||||
authorizationContext,
|
|
||||||
encodingHelper)
|
|
||||||
{
|
|
||||||
HttpClient = httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the output file extension.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
/// <returns>System.String.</returns>
|
|
||||||
protected override string GetOutputFileExtension(StreamState state)
|
|
||||||
{
|
|
||||||
var ext = base.GetOutputFileExtension(state);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ext))
|
|
||||||
{
|
|
||||||
return ext;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isVideoRequest = state.VideoRequest != null;
|
|
||||||
|
|
||||||
// Try to infer based on the desired video codec
|
|
||||||
if (isVideoRequest)
|
|
||||||
{
|
|
||||||
var videoCodec = state.VideoRequest.VideoCodec;
|
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return ".ts";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return ".ogv";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return ".webm";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return ".asf";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to infer based on the desired audio codec
|
|
||||||
if (!isVideoRequest)
|
|
||||||
{
|
|
||||||
var audioCodec = state.Request.AudioCodec;
|
|
||||||
|
|
||||||
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return ".aac";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return ".mp3";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return ".ogg";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return ".wma";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the type of the transcoding job.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The type of the transcoding job.</value>
|
|
||||||
protected override TranscodingJobType TranscodingJobType => TranscodingJobType.Progressive;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes the request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
protected async Task<object> ProcessRequest(StreamRequest request, bool isHeadRequest)
|
|
||||||
{
|
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var responseHeaders = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
if (request.Static && state.DirectStreamProvider != null)
|
|
||||||
{
|
|
||||||
AddDlnaHeaders(state, responseHeaders, true);
|
|
||||||
|
|
||||||
using (state)
|
|
||||||
{
|
|
||||||
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// TODO: Don't hardcode this
|
|
||||||
outputHeaders[HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file.ts");
|
|
||||||
|
|
||||||
return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, CancellationToken.None)
|
|
||||||
{
|
|
||||||
AllowEndOfFile = false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static remote stream
|
|
||||||
if (request.Static && state.InputProtocol == MediaProtocol.Http)
|
|
||||||
{
|
|
||||||
AddDlnaHeaders(state, responseHeaders, true);
|
|
||||||
|
|
||||||
using (state)
|
|
||||||
{
|
|
||||||
return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Static && state.InputProtocol != MediaProtocol.File)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(string.Format("Input protocol {0} cannot be streamed statically.", state.InputProtocol));
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputPath = state.OutputFilePath;
|
|
||||||
var outputPathExists = File.Exists(outputPath);
|
|
||||||
|
|
||||||
var transcodingJob = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
|
|
||||||
var isTranscodeCached = outputPathExists && transcodingJob != null;
|
|
||||||
|
|
||||||
AddDlnaHeaders(state, responseHeaders, request.Static || isTranscodeCached);
|
|
||||||
|
|
||||||
// Static stream
|
|
||||||
if (request.Static)
|
|
||||||
{
|
|
||||||
var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
|
|
||||||
|
|
||||||
using (state)
|
|
||||||
{
|
|
||||||
if (state.MediaSource.IsInfiniteStream)
|
|
||||||
{
|
|
||||||
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
[HeaderNames.ContentType] = contentType
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, Logger, CancellationToken.None)
|
|
||||||
{
|
|
||||||
AllowEndOfFile = false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeSpan? cacheDuration = null;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(request.Tag))
|
|
||||||
{
|
|
||||||
cacheDuration = TimeSpan.FromDays(365);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
|
|
||||||
{
|
|
||||||
ResponseHeaders = responseHeaders,
|
|
||||||
ContentType = contentType,
|
|
||||||
IsHeadRequest = isHeadRequest,
|
|
||||||
Path = state.MediaPath,
|
|
||||||
CacheDuration = cacheDuration
|
|
||||||
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//// Not static but transcode cache file exists
|
|
||||||
// if (isTranscodeCached && state.VideoRequest == null)
|
|
||||||
//{
|
|
||||||
// var contentType = state.GetMimeType(outputPath);
|
|
||||||
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// if (transcodingJob != null)
|
|
||||||
// {
|
|
||||||
// ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
|
|
||||||
// {
|
|
||||||
// ResponseHeaders = responseHeaders,
|
|
||||||
// ContentType = contentType,
|
|
||||||
// IsHeadRequest = isHeadRequest,
|
|
||||||
// Path = outputPath,
|
|
||||||
// FileShare = FileShare.ReadWrite,
|
|
||||||
// OnComplete = () =>
|
|
||||||
// {
|
|
||||||
// if (transcodingJob != null)
|
|
||||||
// {
|
|
||||||
// ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// }).ConfigureAwait(false);
|
|
||||||
// }
|
|
||||||
// finally
|
|
||||||
// {
|
|
||||||
// state.Dispose();
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
// Need to start ffmpeg
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await GetStreamResult(request, state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
state.Dispose();
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the static remote stream result.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
/// <param name="responseHeaders">The response headers.</param>
|
|
||||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
|
||||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
|
||||||
/// <returns>Task{System.Object}.</returns>
|
|
||||||
private async Task<object> GetStaticRemoteStreamResult(
|
|
||||||
StreamState state,
|
|
||||||
Dictionary<string, string> responseHeaders,
|
|
||||||
bool isHeadRequest,
|
|
||||||
CancellationTokenSource cancellationTokenSource)
|
|
||||||
{
|
|
||||||
var options = new HttpRequestOptions
|
|
||||||
{
|
|
||||||
Url = state.MediaPath,
|
|
||||||
BufferContent = false,
|
|
||||||
CancellationToken = cancellationTokenSource.Token
|
|
||||||
};
|
|
||||||
|
|
||||||
if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent))
|
|
||||||
{
|
|
||||||
options.UserAgent = useragent;
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
|
|
||||||
|
|
||||||
responseHeaders[HeaderNames.AcceptRanges] = "none";
|
|
||||||
|
|
||||||
// Seeing cases of -1 here
|
|
||||||
if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
|
|
||||||
{
|
|
||||||
responseHeaders[HeaderNames.ContentLength] = response.ContentLength.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isHeadRequest)
|
|
||||||
{
|
|
||||||
using (response)
|
|
||||||
{
|
|
||||||
return ResultFactory.GetResult(null, Array.Empty<byte>(), response.ContentType, responseHeaders);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new StaticRemoteStreamWriter(response);
|
|
||||||
|
|
||||||
result.Headers[HeaderNames.ContentType] = response.ContentType;
|
|
||||||
|
|
||||||
// Add the response headers to the result object
|
|
||||||
foreach (var header in responseHeaders)
|
|
||||||
{
|
|
||||||
result.Headers[header.Key] = header.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the stream result.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
/// <param name="responseHeaders">The response headers.</param>
|
|
||||||
/// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
|
|
||||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
|
||||||
/// <returns>Task{System.Object}.</returns>
|
|
||||||
private async Task<object> GetStreamResult(StreamRequest request, StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
|
|
||||||
{
|
|
||||||
// Use the command line args with a dummy playlist path
|
|
||||||
var outputPath = state.OutputFilePath;
|
|
||||||
|
|
||||||
responseHeaders[HeaderNames.AcceptRanges] = "none";
|
|
||||||
|
|
||||||
var contentType = state.GetMimeType(outputPath);
|
|
||||||
|
|
||||||
// TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
|
|
||||||
var contentLength = state.EstimateContentLength || isHeadRequest ? GetEstimatedContentLength(state) : null;
|
|
||||||
|
|
||||||
if (contentLength.HasValue)
|
|
||||||
{
|
|
||||||
responseHeaders[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers only
|
|
||||||
if (isHeadRequest)
|
|
||||||
{
|
|
||||||
var streamResult = ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders);
|
|
||||||
|
|
||||||
if (streamResult is IHasHeaders hasHeaders)
|
|
||||||
{
|
|
||||||
if (contentLength.HasValue)
|
|
||||||
{
|
|
||||||
hasHeaders.Headers[HeaderNames.ContentLength] = contentLength.Value.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
hasHeaders.Headers.Remove(HeaderNames.ContentLength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return streamResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
|
|
||||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
TranscodingJob job;
|
|
||||||
|
|
||||||
if (!File.Exists(outputPath))
|
|
||||||
{
|
|
||||||
job = await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
|
|
||||||
state.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
[HeaderNames.ContentType] = contentType
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Add the response headers to the result object
|
|
||||||
foreach (var item in responseHeaders)
|
|
||||||
{
|
|
||||||
outputHeaders[item.Key] = item.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, CancellationToken.None);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
transcodingLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the length of the estimated content.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">The state.</param>
|
|
||||||
/// <returns>System.Nullable{System.Int64}.</returns>
|
|
||||||
private long? GetEstimatedContentLength(StreamState state)
|
|
||||||
{
|
|
||||||
var totalBitrate = state.TotalOutputBitrate ?? 0;
|
|
||||||
|
|
||||||
if (totalBitrate > 0 && state.RunTimeTicks.HasValue)
|
|
||||||
{
|
|
||||||
return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,182 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using MediaBrowser.Model.System;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback.Progressive
|
|
||||||
{
|
|
||||||
public class ProgressiveFileCopier : IAsyncStreamWriter, IHasHeaders
|
|
||||||
{
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly TranscodingJob _job;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly string _path;
|
|
||||||
private readonly CancellationToken _cancellationToken;
|
|
||||||
private readonly Dictionary<string, string> _outputHeaders;
|
|
||||||
|
|
||||||
private long _bytesWritten = 0;
|
|
||||||
public long StartPosition { get; set; }
|
|
||||||
|
|
||||||
public bool AllowEndOfFile = true;
|
|
||||||
|
|
||||||
private readonly IDirectStreamProvider _directStreamProvider;
|
|
||||||
|
|
||||||
public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_path = path;
|
|
||||||
_outputHeaders = outputHeaders;
|
|
||||||
_job = job;
|
|
||||||
_logger = logger;
|
|
||||||
_cancellationToken = cancellationToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_directStreamProvider = directStreamProvider;
|
|
||||||
_outputHeaders = outputHeaders;
|
|
||||||
_job = job;
|
|
||||||
_logger = logger;
|
|
||||||
_cancellationToken = cancellationToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDictionary<string, string> Headers => _outputHeaders;
|
|
||||||
|
|
||||||
private Stream GetInputStream(bool allowAsyncFileRead)
|
|
||||||
{
|
|
||||||
var fileOptions = FileOptions.SequentialScan;
|
|
||||||
|
|
||||||
if (allowAsyncFileRead)
|
|
||||||
{
|
|
||||||
fileOptions |= FileOptions.Asynchronous;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_directStreamProvider != null)
|
|
||||||
{
|
|
||||||
await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var eofCount = 0;
|
|
||||||
|
|
||||||
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
|
|
||||||
var allowAsyncFileRead = OperatingSystem.Id != OperatingSystemId.Windows;
|
|
||||||
|
|
||||||
using (var inputStream = GetInputStream(allowAsyncFileRead))
|
|
||||||
{
|
|
||||||
if (StartPosition > 0)
|
|
||||||
{
|
|
||||||
inputStream.Position = StartPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (eofCount < 20 || !AllowEndOfFile)
|
|
||||||
{
|
|
||||||
int bytesRead;
|
|
||||||
if (allowAsyncFileRead)
|
|
||||||
{
|
|
||||||
bytesRead = await CopyToInternalAsync(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bytesRead = await CopyToInternalAsyncWithSyncRead(inputStream, outputStream, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// var position = fs.Position;
|
|
||||||
// _logger.LogDebug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
|
|
||||||
|
|
||||||
if (bytesRead == 0)
|
|
||||||
{
|
|
||||||
if (_job == null || _job.HasExited)
|
|
||||||
{
|
|
||||||
eofCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
eofCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (_job != null)
|
|
||||||
{
|
|
||||||
ApiEntryPoint.Instance.OnTranscodeEndRequest(_job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> CopyToInternalAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var array = new byte[IODefaults.CopyToBufferSize];
|
|
||||||
int bytesRead;
|
|
||||||
int totalBytesRead = 0;
|
|
||||||
|
|
||||||
while ((bytesRead = source.Read(array, 0, array.Length)) != 0)
|
|
||||||
{
|
|
||||||
var bytesToWrite = bytesRead;
|
|
||||||
|
|
||||||
if (bytesToWrite > 0)
|
|
||||||
{
|
|
||||||
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
_bytesWritten += bytesRead;
|
|
||||||
totalBytesRead += bytesRead;
|
|
||||||
|
|
||||||
if (_job != null)
|
|
||||||
{
|
|
||||||
_job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalBytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<int> CopyToInternalAsync(Stream source, Stream destination, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var array = new byte[IODefaults.CopyToBufferSize];
|
|
||||||
int bytesRead;
|
|
||||||
int totalBytesRead = 0;
|
|
||||||
|
|
||||||
while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
|
||||||
{
|
|
||||||
var bytesToWrite = bytesRead;
|
|
||||||
|
|
||||||
if (bytesToWrite > 0)
|
|
||||||
{
|
|
||||||
await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
_bytesWritten += bytesRead;
|
|
||||||
totalBytesRead += bytesRead;
|
|
||||||
|
|
||||||
if (_job != null)
|
|
||||||
{
|
|
||||||
_job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalBytesRead;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
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.IO;
|
|
||||||
using MediaBrowser.Model.Serialization;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback.Progressive
|
|
||||||
{
|
|
||||||
public class GetVideoStream : VideoStreamRequest
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Class VideoService.
|
|
||||||
/// </summary>
|
|
||||||
// TODO: In order to autheneticate this in the future, Dlna playback will require updating
|
|
||||||
//[Authenticated]
|
|
||||||
public class VideoService : BaseProgressiveStreamingService
|
|
||||||
{
|
|
||||||
public VideoService(
|
|
||||||
ILogger<VideoService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory,
|
|
||||||
IHttpClient httpClient,
|
|
||||||
IUserManager userManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IIsoManager isoManager,
|
|
||||||
IMediaEncoder mediaEncoder,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IDlnaManager dlnaManager,
|
|
||||||
IDeviceManager deviceManager,
|
|
||||||
IMediaSourceManager mediaSourceManager,
|
|
||||||
IJsonSerializer jsonSerializer,
|
|
||||||
IAuthorizationContext authorizationContext,
|
|
||||||
EncodingHelper encodingHelper)
|
|
||||||
: base(
|
|
||||||
logger,
|
|
||||||
serverConfigurationManager,
|
|
||||||
httpResultFactory,
|
|
||||||
httpClient,
|
|
||||||
userManager,
|
|
||||||
libraryManager,
|
|
||||||
isoManager,
|
|
||||||
mediaEncoder,
|
|
||||||
fileSystem,
|
|
||||||
dlnaManager,
|
|
||||||
deviceManager,
|
|
||||||
mediaSourceManager,
|
|
||||||
jsonSerializer,
|
|
||||||
authorizationContext,
|
|
||||||
encodingHelper)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the specified request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <returns>System.Object.</returns>
|
|
||||||
public Task<object> Get(GetVideoStream request)
|
|
||||||
{
|
|
||||||
return ProcessRequest(request, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Heads the specified request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request.</param>
|
|
||||||
/// <returns>System.Object.</returns>
|
|
||||||
public Task<object> Head(GetVideoStream request)
|
|
||||||
{
|
|
||||||
return ProcessRequest(request, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
|
|
||||||
{
|
|
||||||
return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultEncoderPreset());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class StaticRemoteStreamWriter.
|
|
||||||
/// </summary>
|
|
||||||
public class StaticRemoteStreamWriter : IAsyncStreamWriter, IHasHeaders
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The _input stream.
|
|
||||||
/// </summary>
|
|
||||||
private readonly HttpResponseInfo _response;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _options.
|
|
||||||
/// </summary>
|
|
||||||
private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
public StaticRemoteStreamWriter(HttpResponseInfo response)
|
|
||||||
{
|
|
||||||
_response = response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the options.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The options.</value>
|
|
||||||
public IDictionary<string, string> Headers => _options;
|
|
||||||
|
|
||||||
public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using (_response)
|
|
||||||
{
|
|
||||||
await _response.Content.CopyToAsync(responseStream, 81920, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class StreamRequest.
|
|
||||||
/// </summary>
|
|
||||||
public class StreamRequest : BaseEncodingJobOptions
|
|
||||||
{
|
|
||||||
[ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
|
||||||
public string DeviceProfileId { get; set; }
|
|
||||||
|
|
||||||
public string Params { get; set; }
|
|
||||||
|
|
||||||
public string PlaySessionId { get; set; }
|
|
||||||
|
|
||||||
public string Tag { get; set; }
|
|
||||||
|
|
||||||
public string SegmentContainer { get; set; }
|
|
||||||
|
|
||||||
public int? SegmentLength { get; set; }
|
|
||||||
|
|
||||||
public int? MinSegments { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class VideoStreamRequest : StreamRequest
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether this instance has fixed resolution.
|
|
||||||
/// </summary>
|
|
||||||
/// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
|
|
||||||
public bool HasFixedResolution => Width.HasValue || Height.HasValue;
|
|
||||||
|
|
||||||
public bool EnableSubtitlesInManifest { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,143 +0,0 @@
|
|||||||
using System;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Model.Dlna;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
public class StreamState : EncodingJobInfo, IDisposable
|
|
||||||
{
|
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
public string RequestedUrl { get; set; }
|
|
||||||
|
|
||||||
public StreamRequest Request
|
|
||||||
{
|
|
||||||
get => (StreamRequest)BaseRequest;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
BaseRequest = value;
|
|
||||||
|
|
||||||
IsVideoRequest = VideoRequest != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public TranscodingThrottler TranscodingThrottler { get; set; }
|
|
||||||
|
|
||||||
public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;
|
|
||||||
|
|
||||||
public IDirectStreamProvider DirectStreamProvider { get; set; }
|
|
||||||
|
|
||||||
public string WaitForPath { get; set; }
|
|
||||||
|
|
||||||
public bool IsOutputVideo => Request is VideoStreamRequest;
|
|
||||||
|
|
||||||
public int SegmentLength
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (Request.SegmentLength.HasValue)
|
|
||||||
{
|
|
||||||
return Request.SegmentLength.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
|
||||||
{
|
|
||||||
var userAgent = UserAgent ?? string.Empty;
|
|
||||||
|
|
||||||
if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
userAgent.IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
userAgent.IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
||||||
userAgent.IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
if (IsSegmentedLiveStream)
|
|
||||||
{
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsSegmentedLiveStream)
|
|
||||||
{
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int MinSegments
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (Request.MinSegments.HasValue)
|
|
||||||
{
|
|
||||||
return Request.MinSegments.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SegmentLength >= 10 ? 2 : 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string UserAgent { get; set; }
|
|
||||||
|
|
||||||
public bool EstimateContentLength { get; set; }
|
|
||||||
|
|
||||||
public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
|
|
||||||
|
|
||||||
public bool EnableDlnaHeaders { get; set; }
|
|
||||||
|
|
||||||
public DeviceProfile DeviceProfile { get; set; }
|
|
||||||
|
|
||||||
public TranscodingJob TranscodingJob { get; set; }
|
|
||||||
|
|
||||||
public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType)
|
|
||||||
: base(transcodingType)
|
|
||||||
{
|
|
||||||
_mediaSourceManager = mediaSourceManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
|
|
||||||
{
|
|
||||||
ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
// REVIEW: Is this the right place for this?
|
|
||||||
if (MediaSource.RequiresClosing
|
|
||||||
&& string.IsNullOrWhiteSpace(Request.LiveStreamId)
|
|
||||||
&& !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
|
|
||||||
{
|
|
||||||
_mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
TranscodingThrottler?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
TranscodingThrottler = null;
|
|
||||||
TranscodingJob = null;
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,175 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Model.Configuration;
|
|
||||||
using MediaBrowser.Model.IO;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback
|
|
||||||
{
|
|
||||||
public class TranscodingThrottler : IDisposable
|
|
||||||
{
|
|
||||||
private readonly TranscodingJob _job;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private Timer _timer;
|
|
||||||
private bool _isPaused;
|
|
||||||
private readonly IConfigurationManager _config;
|
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
|
|
||||||
public TranscodingThrottler(TranscodingJob job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem)
|
|
||||||
{
|
|
||||||
_job = job;
|
|
||||||
_logger = logger;
|
|
||||||
_config = config;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EncodingOptions GetOptions()
|
|
||||||
{
|
|
||||||
return _config.GetConfiguration<EncodingOptions>("encoding");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
_timer = new Timer(TimerCallback, null, 5000, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void TimerCallback(object state)
|
|
||||||
{
|
|
||||||
if (_job.HasExited)
|
|
||||||
{
|
|
||||||
DisposeTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = GetOptions();
|
|
||||||
|
|
||||||
if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds))
|
|
||||||
{
|
|
||||||
await PauseTranscoding();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await UnpauseTranscoding();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PauseTranscoding()
|
|
||||||
{
|
|
||||||
if (!_isPaused)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Sending pause command to ffmpeg");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _job.Process.StandardInput.WriteAsync("c");
|
|
||||||
_isPaused = true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error pausing transcoding");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UnpauseTranscoding()
|
|
||||||
{
|
|
||||||
if (_isPaused)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Sending resume command to ffmpeg");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _job.Process.StandardInput.WriteLineAsync();
|
|
||||||
_isPaused = false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error resuming transcoding");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsThrottleAllowed(TranscodingJob job, int thresholdSeconds)
|
|
||||||
{
|
|
||||||
var bytesDownloaded = job.BytesDownloaded ?? 0;
|
|
||||||
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
|
|
||||||
var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
|
|
||||||
|
|
||||||
var path = job.Path;
|
|
||||||
var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks;
|
|
||||||
|
|
||||||
if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
|
|
||||||
{
|
|
||||||
// HLS - time-based consideration
|
|
||||||
|
|
||||||
var targetGap = gapLengthInTicks;
|
|
||||||
var gap = transcodingPositionTicks - downloadPositionTicks;
|
|
||||||
|
|
||||||
if (gap < targetGap)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
|
|
||||||
{
|
|
||||||
// Progressive Streaming - byte-based consideration
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var bytesTranscoded = job.BytesTranscoded ?? _fileSystem.GetFileInfo(path).Length;
|
|
||||||
|
|
||||||
// Estimate the bytes the transcoder should be ahead
|
|
||||||
double gapFactor = gapLengthInTicks;
|
|
||||||
gapFactor /= transcodingPositionTicks;
|
|
||||||
var targetGap = bytesTranscoded * gapFactor;
|
|
||||||
|
|
||||||
var gap = bytesTranscoded - bytesDownloaded;
|
|
||||||
|
|
||||||
if (gap < targetGap)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error getting output size");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("No throttle data for " + path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Stop()
|
|
||||||
{
|
|
||||||
DisposeTimer();
|
|
||||||
await UnpauseTranscoding();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
DisposeTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisposeTimer()
|
|
||||||
{
|
|
||||||
if (_timer != null)
|
|
||||||
{
|
|
||||||
_timer.Dispose();
|
|
||||||
_timer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using System.Resources;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
// General Information about an assembly is controlled through the following
|
|
||||||
// set of attributes. Change these attribute values to modify the information
|
|
||||||
// associated with an assembly.
|
|
||||||
[assembly: AssemblyTitle("MediaBrowser.Api")]
|
|
||||||
[assembly: AssemblyDescription("")]
|
|
||||||
[assembly: AssemblyConfiguration("")]
|
|
||||||
[assembly: AssemblyCompany("Jellyfin Project")]
|
|
||||||
[assembly: AssemblyProduct("Jellyfin Server")]
|
|
||||||
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
|
|
||||||
[assembly: AssemblyTrademark("")]
|
|
||||||
[assembly: AssemblyCulture("")]
|
|
||||||
[assembly: NeutralResourcesLanguage("en")]
|
|
||||||
[assembly: InternalsVisibleTo("Jellyfin.Api.Tests")]
|
|
||||||
|
|
||||||
// Setting ComVisible to false makes the types in this assembly not visible
|
|
||||||
// to COM components. If you need to access a type in this assembly from
|
|
||||||
// COM, set the ComVisible attribute to true on that type.
|
|
||||||
[assembly: ComVisible(false)]
|
|
@ -1,26 +0,0 @@
|
|||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Net;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Service for testing path value.
|
|
||||||
/// </summary>
|
|
||||||
public class TestService : BaseApiService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Test service.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{TestService}"/> interface.</param>
|
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
|
||||||
/// <param name="httpResultFactory">Instance of the <see cref="IHttpResultFactory"/> interface.</param>
|
|
||||||
public TestService(
|
|
||||||
ILogger<TestService> logger,
|
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
|
||||||
IHttpResultFactory httpResultFactory)
|
|
||||||
: base(logger, serverConfigurationManager, httpResultFactory)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,165 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading;
|
|
||||||
using MediaBrowser.Api.Playback;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Class TranscodingJob.
|
|
||||||
/// </summary>
|
|
||||||
public class TranscodingJob
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the play session identifier.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The play session identifier.</value>
|
|
||||||
public string PlaySessionId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the live stream identifier.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The live stream identifier.</value>
|
|
||||||
public string LiveStreamId { get; set; }
|
|
||||||
|
|
||||||
public bool IsLiveOutput { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the path.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The path.</value>
|
|
||||||
public MediaSourceInfo MediaSource { get; set; }
|
|
||||||
|
|
||||||
public string Path { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the type.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The type.</value>
|
|
||||||
public TranscodingJobType Type { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the process.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The process.</value>
|
|
||||||
public Process Process { get; set; }
|
|
||||||
|
|
||||||
public ILogger Logger { get; private set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the active request count.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The active request count.</value>
|
|
||||||
public int ActiveRequestCount { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the kill timer.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The kill timer.</value>
|
|
||||||
private Timer KillTimer { get; set; }
|
|
||||||
|
|
||||||
public string DeviceId { get; set; }
|
|
||||||
|
|
||||||
public CancellationTokenSource CancellationTokenSource { get; set; }
|
|
||||||
|
|
||||||
public object ProcessLock = new object();
|
|
||||||
|
|
||||||
public bool HasExited { get; set; }
|
|
||||||
|
|
||||||
public bool IsUserPaused { get; set; }
|
|
||||||
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
public float? Framerate { get; set; }
|
|
||||||
|
|
||||||
public double? CompletionPercentage { get; set; }
|
|
||||||
|
|
||||||
public long? BytesDownloaded { get; set; }
|
|
||||||
|
|
||||||
public long? BytesTranscoded { get; set; }
|
|
||||||
|
|
||||||
public int? BitRate { get; set; }
|
|
||||||
|
|
||||||
public long? TranscodingPositionTicks { get; set; }
|
|
||||||
|
|
||||||
public long? DownloadPositionTicks { get; set; }
|
|
||||||
|
|
||||||
public TranscodingThrottler TranscodingThrottler { get; set; }
|
|
||||||
|
|
||||||
private readonly object _timerLock = new object();
|
|
||||||
|
|
||||||
public DateTime LastPingDate { get; set; }
|
|
||||||
|
|
||||||
public int PingTimeout { get; set; }
|
|
||||||
|
|
||||||
public TranscodingJob(ILogger logger)
|
|
||||||
{
|
|
||||||
Logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopKillTimer()
|
|
||||||
{
|
|
||||||
lock (_timerLock)
|
|
||||||
{
|
|
||||||
KillTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DisposeKillTimer()
|
|
||||||
{
|
|
||||||
lock (_timerLock)
|
|
||||||
{
|
|
||||||
if (KillTimer != null)
|
|
||||||
{
|
|
||||||
KillTimer.Dispose();
|
|
||||||
KillTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartKillTimer(Action<object> callback)
|
|
||||||
{
|
|
||||||
StartKillTimer(callback, PingTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartKillTimer(Action<object> callback, int intervalMs)
|
|
||||||
{
|
|
||||||
if (HasExited)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_timerLock)
|
|
||||||
{
|
|
||||||
if (KillTimer == null)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
|
||||||
KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
|
||||||
KillTimer.Change(intervalMs, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeKillTimerIfStarted()
|
|
||||||
{
|
|
||||||
if (HasExited)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_timerLock)
|
|
||||||
{
|
|
||||||
if (KillTimer != null)
|
|
||||||
{
|
|
||||||
var intervalMs = PingTimeout;
|
|
||||||
|
|
||||||
Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
|
||||||
KillTimer.Change(intervalMs, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,8 +6,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Common", "MediaBrowser.Common\MediaBrowser.Common.csproj", "{9142EEFA-7570-41E1-BFCC-468BB571AF2F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Model", "MediaBrowser.Model\MediaBrowser.Model.csproj", "{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}"
|
||||||
@ -80,10 +78,6 @@ Global
|
|||||||
{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.Build.0 = Release|Any CPU
|
{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
using MediaBrowser.Api;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Net;
|
|
||||||
using MediaBrowser.Model.Configuration;
|
|
||||||
using MediaBrowser.Model.Services;
|
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
using Moq;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Tests
|
|
||||||
{
|
|
||||||
public class GetPathValueTests
|
|
||||||
{
|
|
||||||
[Theory]
|
|
||||||
[InlineData("https://localhost:8096/ScheduledTasks/1234/Triggers", "", 1, "1234")]
|
|
||||||
[InlineData("https://localhost:8096/emby/ScheduledTasks/1234/Triggers", "", 1, "1234")]
|
|
||||||
[InlineData("https://localhost:8096/mediabrowser/ScheduledTasks/1234/Triggers", "", 1, "1234")]
|
|
||||||
[InlineData("https://localhost:8096/jellyfin/2/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
|
|
||||||
[InlineData("https://localhost:8096/jellyfin/2/emby/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
|
|
||||||
[InlineData("https://localhost:8096/jellyfin/2/mediabrowser/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
|
|
||||||
[InlineData("https://localhost:8096/JELLYFIN/2/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
|
|
||||||
[InlineData("https://localhost:8096/JELLYFIN/2/Emby/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
|
|
||||||
[InlineData("https://localhost:8096/JELLYFIN/2/MediaBrowser/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")]
|
|
||||||
public void GetPathValueTest(string path, string baseUrl, int index, string value)
|
|
||||||
{
|
|
||||||
var reqMock = Mock.Of<IRequest>(x => x.PathInfo == path);
|
|
||||||
var conf = new ServerConfiguration()
|
|
||||||
{
|
|
||||||
BaseUrl = baseUrl
|
|
||||||
};
|
|
||||||
|
|
||||||
var confManagerMock = Mock.Of<IServerConfigurationManager>(x => x.Configuration == conf);
|
|
||||||
|
|
||||||
var service = new TestService(
|
|
||||||
new NullLogger<TestService>(),
|
|
||||||
confManagerMock,
|
|
||||||
Mock.Of<IHttpResultFactory>())
|
|
||||||
{
|
|
||||||
Request = reqMock
|
|
||||||
};
|
|
||||||
|
|
||||||
Assert.Equal(value, service.GetPathValue(index).ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user