using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net.Mime; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { /// /// Item lookup controller. /// [Route("")] [Authorize(Policy = Policies.DefaultAuthorization)] public class ItemLookupController : BaseJellyfinApiController { private readonly IProviderManager _providerManager; private readonly IServerApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public ItemLookupController( IProviderManager providerManager, IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILogger logger) { _providerManager = providerManager; _appPaths = serverConfigurationManager.ApplicationPaths; _fileSystem = fileSystem; _libraryManager = libraryManager; _logger = logger; } /// /// Get the item's external id info. /// /// Item id. /// External id info retrieved. /// Item not found. /// List of external id info. [HttpGet("Items/{itemId}/ExternalIdInfos")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetExternalIdInfos([FromRoute] Guid itemId) { var item = _libraryManager.GetItemById(itemId); if (item == null) { return NotFound(); } return Ok(_providerManager.GetExternalIdInfos(item)); } /// /// Get movie remote search. /// /// Remote search query. /// Movie remote search executed. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Movie")] public async Task>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); return Ok(results); } /// /// Get trailer remote search. /// /// Remote search query. /// Trailer remote search executed. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Trailer")] public async Task>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); return Ok(results); } /// /// Get music video remote search. /// /// Remote search query. /// Music video remote search executed. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/MusicVideo")] public async Task>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); return Ok(results); } /// /// Get series remote search. /// /// Remote search query. /// Series remote search executed. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Series")] public async Task>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); return Ok(results); } /// /// Get box set remote search. /// /// Remote search query. /// Box set remote search executed. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/BoxSet")] public async Task>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); return Ok(results); } /// /// Get music artist remote search. /// /// Remote search query. /// Music artist remote search executed. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/MusicArtist")] public async Task>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); return Ok(results); } /// /// Get music album remote search. /// /// Remote search query. /// Music album remote search executed. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/MusicAlbum")] public async Task>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); return Ok(results); } /// /// Get person remote search. /// /// Remote search query. /// Person remote search executed. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Person")] [Authorize(Policy = Policies.RequiresElevation)] public async Task>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); return Ok(results); } /// /// Get book remote search. /// /// Remote search query. /// Book remote search executed. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Book")] public async Task>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); return Ok(results); } /// /// Gets a remote image. /// /// The image url. /// The provider name. /// Remote image retrieved. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an containing the images file stream. /// [HttpGet("Items/RemoteSearch/Image")] public async Task GetRemoteSearchImage( [FromQuery, Required] string imageUrl, [FromQuery, Required] string providerName) { var urlHash = imageUrl.GetMD5(); var pointerCachePath = GetFullCachePath(urlHash.ToString()); try { var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false); if (System.IO.File.Exists(contentPath)) { await using var fileStreamExisting = System.IO.File.OpenRead(pointerCachePath); return new FileStreamResult(fileStreamExisting, MediaTypeNames.Application.Octet); } } catch (FileNotFoundException) { // Means the file isn't cached yet } catch (IOException) { // Means the file isn't cached yet } await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); // Read the pointer file again await using var fileStream = System.IO.File.OpenRead(pointerCachePath); return new FileStreamResult(fileStream, MediaTypeNames.Application.Octet); } /// /// Applies search criteria to an item and refreshes metadata. /// /// Item id. /// The remote search result. /// Optional. Whether or not to replace all images. Default: True. /// Item metadata refreshed. /// /// A that represents the asynchronous operation to get the remote search results. /// The task result contains an . /// [HttpPost("Items/RemoteSearch/Apply/{id}")] [Authorize(Policy = Policies.RequiresElevation)] public async Task ApplySearchCriteria( [FromRoute] Guid itemId, [FromBody, Required] RemoteSearchResult searchResult, [FromQuery] bool replaceAllImages = true) { var item = _libraryManager.GetItemById(itemId); _logger.LogInformation( "Setting provider id's to item {0}-{1}: {2}", item.Id, item.Name, JsonSerializer.Serialize(searchResult.ProviderIds)); // Since the refresh process won't erase provider Ids, we need to set this explicitly now. item.ProviderIds = searchResult.ProviderIds; await _providerManager.RefreshFullItem( item, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { MetadataRefreshMode = MetadataRefreshMode.FullRefresh, ImageRefreshMode = MetadataRefreshMode.FullRefresh, ReplaceAllMetadata = true, ReplaceAllImages = replaceAllImages, SearchResult = searchResult }, CancellationToken.None).ConfigureAwait(false); return NoContent(); } /// /// Downloads the image. /// /// Name of the provider. /// The URL. /// The URL hash. /// The pointer cache path. /// Task. private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath) { using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false); var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1]; var fullCachePath = GetFullCachePath(urlHash + "." + ext); Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); using (var stream = result.Content) { await using var fileStream = new FileStream( fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); await stream.CopyToAsync(fileStream).ConfigureAwait(false); } Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false); } /// /// Gets the full cache path. /// /// The filename. /// System.String. private string GetFullCachePath(string filename) => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); } }