2020-04-20 20:21:06 +00:00
using System ;
2020-05-19 15:23:28 +00:00
using System.Collections.Generic ;
2020-08-06 14:17:45 +00:00
using System.ComponentModel.DataAnnotations ;
2020-04-20 20:21:06 +00:00
using System.IO ;
using System.Linq ;
2020-08-13 21:50:19 +00:00
using System.Net.Http ;
2020-05-20 13:18:51 +00:00
using System.Net.Mime ;
2020-04-20 20:21:06 +00:00
using System.Threading ;
using System.Threading.Tasks ;
2020-09-01 23:31:31 +00:00
using Jellyfin.Api.Attributes ;
2020-06-22 13:44:11 +00:00
using Jellyfin.Api.Constants ;
2020-04-20 20:21:06 +00:00
using MediaBrowser.Common.Extensions ;
2020-08-31 17:05:21 +00:00
using MediaBrowser.Common.Net ;
2020-04-20 20:21:06 +00:00
using MediaBrowser.Controller ;
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Providers ;
using MediaBrowser.Model.Entities ;
using MediaBrowser.Model.IO ;
using MediaBrowser.Model.Net ;
using MediaBrowser.Model.Providers ;
2020-05-19 15:23:28 +00:00
using Microsoft.AspNetCore.Authorization ;
2020-04-20 20:21:06 +00:00
using Microsoft.AspNetCore.Http ;
using Microsoft.AspNetCore.Mvc ;
2020-06-18 13:11:46 +00:00
namespace Jellyfin.Api.Controllers
2020-04-20 20:21:06 +00:00
{
/// <summary>
/// Remote Images Controller.
/// </summary>
2020-08-05 19:57:01 +00:00
[Route("")]
2020-04-20 20:21:06 +00:00
public class RemoteImageController : BaseJellyfinApiController
{
private readonly IProviderManager _providerManager ;
private readonly IServerApplicationPaths _applicationPaths ;
2020-08-13 21:50:19 +00:00
private readonly IHttpClientFactory _httpClientFactory ;
2020-04-20 20:21:06 +00:00
private readonly ILibraryManager _libraryManager ;
/// <summary>
/// Initializes a new instance of the <see cref="RemoteImageController"/> class.
/// </summary>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
2020-08-13 21:50:19 +00:00
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
2020-04-20 20:21:06 +00:00
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public RemoteImageController (
IProviderManager providerManager ,
IServerApplicationPaths applicationPaths ,
2020-08-13 21:50:19 +00:00
IHttpClientFactory httpClientFactory ,
2020-04-20 20:21:06 +00:00
ILibraryManager libraryManager )
{
_providerManager = providerManager ;
_applicationPaths = applicationPaths ;
2020-08-13 21:50:19 +00:00
_httpClientFactory = httpClientFactory ;
2020-04-20 20:21:06 +00:00
_libraryManager = libraryManager ;
}
/// <summary>
/// Gets available remote images for an item.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="itemId">Item Id.</param>
2020-04-20 20:21:06 +00:00
/// <param name="type">The image type.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="providerName">Optional. The image provider to use.</param>
2020-05-19 15:23:28 +00:00
/// <param name="includeAllLanguages">Optional. Include all languages.</param>
/// <response code="200">Remote Images returned.</response>
/// <response code="404">Item not found.</response>
2020-04-20 20:21:06 +00:00
/// <returns>Remote Image Result.</returns>
2020-08-05 19:57:01 +00:00
[HttpGet("Items/{itemId}/RemoteImages")]
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-04-21 20:09:06 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
2020-04-20 20:21:06 +00:00
[ProducesResponseType(StatusCodes.Status404NotFound)]
2020-04-21 20:09:06 +00:00
public async Task < ActionResult < RemoteImageResult > > GetRemoteImages (
2020-09-06 15:07:27 +00:00
[FromRoute, Required] Guid itemId ,
2020-04-20 20:21:06 +00:00
[FromQuery] ImageType ? type ,
[FromQuery] int? startIndex ,
[FromQuery] int? limit ,
2020-08-05 19:57:01 +00:00
[FromQuery] string? providerName ,
2020-07-07 15:10:51 +00:00
[FromQuery] bool includeAllLanguages = false )
2020-04-20 20:21:06 +00:00
{
2020-06-21 00:02:07 +00:00
var item = _libraryManager . GetItemById ( itemId ) ;
2020-04-21 13:58:54 +00:00
if ( item = = null )
2020-04-20 20:21:06 +00:00
{
2020-04-21 13:58:54 +00:00
return NotFound ( ) ;
}
2020-04-20 20:21:06 +00:00
2020-04-21 13:58:54 +00:00
var images = await _providerManager . GetAvailableRemoteImages (
item ,
2020-08-05 19:57:01 +00:00
new RemoteImageQuery ( providerName ? ? string . Empty )
2020-04-21 13:58:54 +00:00
{
IncludeAllLanguages = includeAllLanguages ,
IncludeDisabledProviders = true ,
ImageType = type
} , CancellationToken . None )
. ConfigureAwait ( false ) ;
var imageArray = images . ToArray ( ) ;
var allProviders = _providerManager . GetRemoteImageProviderInfo ( item ) ;
if ( type . HasValue )
{
allProviders = allProviders . Where ( o = > o . SupportedImages . Contains ( type . Value ) ) ;
}
2020-04-20 20:21:06 +00:00
2020-04-21 13:58:54 +00:00
var result = new RemoteImageResult
{
TotalRecordCount = imageArray . Length ,
Providers = allProviders . Select ( o = > o . Name )
. Distinct ( StringComparer . OrdinalIgnoreCase )
. ToArray ( )
} ;
2020-04-20 20:21:06 +00:00
2020-04-21 13:58:54 +00:00
if ( startIndex . HasValue )
{
imageArray = imageArray . Skip ( startIndex . Value ) . ToArray ( ) ;
2020-04-20 20:21:06 +00:00
}
2020-04-21 13:58:54 +00:00
if ( limit . HasValue )
2020-04-20 20:21:06 +00:00
{
2020-04-21 13:58:54 +00:00
imageArray = imageArray . Take ( limit . Value ) . ToArray ( ) ;
2020-04-20 20:21:06 +00:00
}
2020-04-21 13:58:54 +00:00
result . Images = imageArray ;
2020-05-19 15:23:28 +00:00
return result ;
2020-04-20 20:21:06 +00:00
}
/// <summary>
/// Gets available remote image providers for an item.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="itemId">Item Id.</param>
2020-05-19 15:23:28 +00:00
/// <response code="200">Returned remote image providers.</response>
/// <response code="404">Item not found.</response>
/// <returns>List of remote image providers.</returns>
2020-08-05 19:57:01 +00:00
[HttpGet("Items/{itemId}/RemoteImages/Providers")]
[Authorize(Policy = Policies.DefaultAuthorization)]
2020-04-21 20:09:06 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
2020-04-20 20:21:06 +00:00
[ProducesResponseType(StatusCodes.Status404NotFound)]
2020-09-06 15:07:27 +00:00
public ActionResult < IEnumerable < ImageProviderInfo > > GetRemoteImageProviders ( [ FromRoute , Required ] Guid itemId )
2020-04-20 20:21:06 +00:00
{
2020-06-21 00:02:07 +00:00
var item = _libraryManager . GetItemById ( itemId ) ;
2020-04-21 13:58:54 +00:00
if ( item = = null )
2020-04-20 20:21:06 +00:00
{
2020-04-21 13:58:54 +00:00
return NotFound ( ) ;
2020-04-20 20:21:06 +00:00
}
2020-04-21 13:58:54 +00:00
2020-05-19 15:23:28 +00:00
return Ok ( _providerManager . GetRemoteImageProviderInfo ( item ) ) ;
2020-04-20 20:21:06 +00:00
}
/// <summary>
/// Gets a remote image.
/// </summary>
/// <param name="imageUrl">The image url.</param>
2020-05-19 15:23:28 +00:00
/// <response code="200">Remote image returned.</response>
/// <response code="404">Remote image not found.</response>
2020-04-20 20:21:06 +00:00
/// <returns>Image Stream.</returns>
2020-08-05 19:57:01 +00:00
[HttpGet("Images/Remote")]
2020-05-20 13:18:51 +00:00
[Produces(MediaTypeNames.Application.Octet)]
2020-04-21 20:09:06 +00:00
[ProducesResponseType(StatusCodes.Status200OK)]
2020-04-20 20:21:06 +00:00
[ProducesResponseType(StatusCodes.Status404NotFound)]
2020-09-01 23:31:31 +00:00
[ProducesImageFile]
2020-11-13 16:04:31 +00:00
public async Task < ActionResult > GetRemoteImage ( [ FromQuery , Required ] Uri imageUrl )
2020-04-20 20:21:06 +00:00
{
2020-11-13 16:04:31 +00:00
var urlHash = imageUrl . ToString ( ) . GetMD5 ( ) ;
2020-04-21 13:58:54 +00:00
var pointerCachePath = GetFullCachePath ( urlHash . ToString ( ) ) ;
2020-04-20 20:21:06 +00:00
2020-04-21 13:58:54 +00:00
string? contentPath = null ;
2020-05-19 15:23:28 +00:00
var hasFile = false ;
2020-04-20 20:21:06 +00:00
2020-04-21 13:58:54 +00:00
try
{
contentPath = await System . IO . File . ReadAllTextAsync ( pointerCachePath ) . ConfigureAwait ( false ) ;
if ( System . IO . File . Exists ( contentPath ) )
2020-04-20 20:21:06 +00:00
{
2020-04-21 13:58:54 +00:00
hasFile = true ;
2020-04-20 20:21:06 +00:00
}
2020-04-21 13:58:54 +00:00
}
catch ( FileNotFoundException )
{
2020-05-19 15:23:28 +00:00
// The file isn't cached yet
2020-04-21 13:58:54 +00:00
}
catch ( IOException )
{
2020-05-19 15:23:28 +00:00
// The file isn't cached yet
2020-04-21 13:58:54 +00:00
}
2020-04-20 20:21:06 +00:00
2020-04-21 13:58:54 +00:00
if ( ! hasFile )
{
await DownloadImage ( imageUrl , urlHash , pointerCachePath ) . ConfigureAwait ( false ) ;
contentPath = await System . IO . File . ReadAllTextAsync ( pointerCachePath ) . ConfigureAwait ( false ) ;
2020-04-20 20:21:06 +00:00
}
2020-04-21 13:58:54 +00:00
if ( string . IsNullOrEmpty ( contentPath ) )
2020-04-20 20:21:06 +00:00
{
2020-04-21 13:58:54 +00:00
return NotFound ( ) ;
2020-04-20 20:21:06 +00:00
}
2020-04-21 13:58:54 +00:00
var contentType = MimeTypes . GetMimeType ( contentPath ) ;
2020-09-01 23:31:31 +00:00
return PhysicalFile ( contentPath , contentType ) ;
2020-04-20 20:21:06 +00:00
}
/// <summary>
/// Downloads a remote image for an item.
/// </summary>
2020-06-21 00:02:07 +00:00
/// <param name="itemId">Item Id.</param>
2020-04-20 20:21:06 +00:00
/// <param name="type">The image type.</param>
/// <param name="imageUrl">The image url.</param>
2020-06-21 00:02:07 +00:00
/// <response code="204">Remote image downloaded.</response>
2020-05-19 15:23:28 +00:00
/// <response code="404">Remote image not found.</response>
/// <returns>Download status.</returns>
2020-08-05 19:57:01 +00:00
[HttpPost("Items/{itemId}/RemoteImages/Download")]
[Authorize(Policy = Policies.RequiresElevation)]
2020-06-21 00:02:07 +00:00
[ProducesResponseType(StatusCodes.Status204NoContent)]
2020-04-20 20:21:06 +00:00
[ProducesResponseType(StatusCodes.Status404NotFound)]
2020-04-21 20:09:06 +00:00
public async Task < ActionResult > DownloadRemoteImage (
2020-09-06 15:07:27 +00:00
[FromRoute, Required] Guid itemId ,
2020-08-06 14:17:45 +00:00
[FromQuery, Required] ImageType type ,
2020-06-27 16:50:44 +00:00
[FromQuery] string? imageUrl )
2020-04-20 20:21:06 +00:00
{
2020-06-21 00:02:07 +00:00
var item = _libraryManager . GetItemById ( itemId ) ;
2020-04-21 13:58:54 +00:00
if ( item = = null )
2020-04-20 20:21:06 +00:00
{
2020-04-21 13:58:54 +00:00
return NotFound ( ) ;
}
2020-04-20 20:21:06 +00:00
2020-04-21 13:58:54 +00:00
await _providerManager . SaveImage ( item , imageUrl , type , null , CancellationToken . None )
. ConfigureAwait ( false ) ;
2020-04-20 20:21:06 +00:00
2020-08-21 20:01:19 +00:00
await item . UpdateToRepositoryAsync ( ItemUpdateType . ImageUpdate , CancellationToken . None ) . ConfigureAwait ( false ) ;
2020-06-21 00:02:07 +00:00
return NoContent ( ) ;
2020-04-20 20:21:06 +00:00
}
/// <summary>
/// Gets the full cache path.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
private string GetFullCachePath ( string filename )
{
return Path . Combine ( _applicationPaths . CachePath , "remote-images" , filename . Substring ( 0 , 1 ) , filename ) ;
}
/// <summary>
/// Downloads the image.
/// </summary>
/// <param name="url">The URL.</param>
/// <param name="urlHash">The URL hash.</param>
/// <param name="pointerCachePath">The pointer cache path.</param>
/// <returns>Task.</returns>
2020-11-13 16:04:31 +00:00
private async Task DownloadImage ( Uri url , Guid urlHash , string pointerCachePath )
2020-04-20 20:21:06 +00:00
{
2020-08-31 17:05:21 +00:00
var httpClient = _httpClientFactory . CreateClient ( NamedClient . Default ) ;
2020-08-16 16:57:01 +00:00
using var response = await httpClient . GetAsync ( url ) . ConfigureAwait ( false ) ;
2020-11-13 16:04:31 +00:00
if ( response . Content . Headers . ContentType ? . MediaType = = null )
{
2020-11-13 18:14:44 +00:00
throw new ResourceNotFoundException ( nameof ( response . Content . Headers . ContentType ) ) ;
2020-11-13 16:04:31 +00:00
}
2020-11-14 14:54:50 +00:00
var ext = response . Content . Headers . ContentType . MediaType . Split ( '/' ) [ ^ 1 ] ;
2020-04-20 20:21:06 +00:00
var fullCachePath = GetFullCachePath ( urlHash + "." + ext ) ;
2020-11-14 01:04:06 +00:00
var fullCacheDirectory = Path . GetDirectoryName ( fullCachePath ) ? ? throw new ResourceNotFoundException ( $"Provided path ({fullCachePath}) is not valid." ) ;
2020-11-13 16:04:31 +00:00
Directory . CreateDirectory ( fullCacheDirectory ) ;
2021-03-07 13:43:28 +00:00
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var fileStream = new FileStream ( fullCachePath , FileMode . Create , FileAccess . Write , FileShare . None , IODefaults . FileStreamBufferSize , true ) ;
2020-08-13 21:50:19 +00:00
await response . Content . CopyToAsync ( fileStream ) . ConfigureAwait ( false ) ;
2020-11-13 16:04:31 +00:00
2020-11-14 01:04:06 +00:00
var pointerCacheDirectory = Path . GetDirectoryName ( pointerCachePath ) ? ? throw new ArgumentException ( $"Provided path ({pointerCachePath}) is not valid." , nameof ( pointerCachePath ) ) ;
2020-11-13 16:04:31 +00:00
Directory . CreateDirectory ( pointerCacheDirectory ) ;
2020-04-20 20:21:06 +00:00
await System . IO . File . WriteAllTextAsync ( pointerCachePath , fullCachePath , CancellationToken . None )
. ConfigureAwait ( false ) ;
}
}
}