diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs new file mode 100644 index 0000000000..e474f2b23d --- /dev/null +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -0,0 +1,364 @@ +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.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Item lookup controller. + /// + [Authorize] + 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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, BindRequired] 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 Ok(); + } + + /// + /// 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) + { + var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false); + var ext = result.ContentType.Split('/').Last(); + var fullCachePath = GetFullCachePath(urlHash + "." + ext); + + Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); + await 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); + } +} \ No newline at end of file diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs deleted file mode 100644 index 0bbe7e1cfa..0000000000 --- a/MediaBrowser.Api/ItemLookupService.cs +++ /dev/null @@ -1,332 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -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.Net; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - [Route("/Items/{Id}/ExternalIdInfos", "GET", Summary = "Gets external id infos for an item")] - [Authenticated(Roles = "Admin")] - public class GetExternalIdInfos : IReturn> - { - /// - /// Gets or sets the id. - /// - /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid Id { get; set; } - } - - [Route("/Items/RemoteSearch/Movie", "POST")] - [Authenticated] - public class GetMovieRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/Trailer", "POST")] - [Authenticated] - public class GetTrailerRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/MusicVideo", "POST")] - [Authenticated] - public class GetMusicVideoRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/Series", "POST")] - [Authenticated] - public class GetSeriesRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/BoxSet", "POST")] - [Authenticated] - public class GetBoxSetRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/MusicArtist", "POST")] - [Authenticated] - public class GetMusicArtistRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/MusicAlbum", "POST")] - [Authenticated] - public class GetMusicAlbumRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/Person", "POST")] - [Authenticated(Roles = "Admin")] - public class GetPersonRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/Book", "POST")] - [Authenticated] - public class GetBookRemoteSearchResults : RemoteSearchQuery, IReturn> - { - } - - [Route("/Items/RemoteSearch/Image", "GET", Summary = "Gets a remote image")] - public class GetRemoteSearchImage - { - [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ImageUrl { get; set; } - - [ApiMember(Name = "ProviderName", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string ProviderName { get; set; } - } - - [Route("/Items/RemoteSearch/Apply/{Id}", "POST", Summary = "Applies search criteria to an item and refreshes metadata")] - [Authenticated(Roles = "Admin")] - public class ApplySearchCriteria : RemoteSearchResult, IReturnVoid - { - [ApiMember(Name = "Id", Description = "The item id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Id { get; set; } - - [ApiMember(Name = "ReplaceAllImages", Description = "Whether or not to replace all images", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] - public bool ReplaceAllImages { get; set; } - - public ApplySearchCriteria() - { - ReplaceAllImages = true; - } - } - - public class ItemLookupService : BaseApiService - { - private readonly IProviderManager _providerManager; - private readonly IServerApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - private readonly ILibraryManager _libraryManager; - private readonly IJsonSerializer _json; - - public ItemLookupService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IProviderManager providerManager, - IFileSystem fileSystem, - ILibraryManager libraryManager, - IJsonSerializer json) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _providerManager = providerManager; - _appPaths = serverConfigurationManager.ApplicationPaths; - _fileSystem = fileSystem; - _libraryManager = libraryManager; - _json = json; - } - - public object Get(GetExternalIdInfos request) - { - var item = _libraryManager.GetItemById(request.Id); - - var infos = _providerManager.GetExternalIdInfos(item).ToList(); - - return ToOptimizedResult(infos); - } - - public async Task Post(GetTrailerRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetBookRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetMovieRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetSeriesRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetBoxSetRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetMusicVideoRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetPersonRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetMusicAlbumRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public async Task Post(GetMusicArtistRemoteSearchResults request) - { - var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - - public Task Get(GetRemoteSearchImage request) - { - return GetRemoteImage(request); - } - - public Task Post(ApplySearchCriteria request) - { - var item = _libraryManager.GetItemById(new Guid(request.Id)); - - //foreach (var key in request.ProviderIds) - //{ - // var value = key.Value; - - // if (!string.IsNullOrWhiteSpace(value)) - // { - // item.SetProviderId(key.Key, value); - // } - //} - Logger.LogInformation("Setting provider id's to item {0}-{1}: {2}", item.Id, item.Name, _json.SerializeToString(request.ProviderIds)); - - // Since the refresh process won't erase provider Ids, we need to set this explicitly now. - item.ProviderIds = request.ProviderIds; - //item.ProductionYear = request.ProductionYear; - //item.Name = request.Name; - - return _providerManager.RefreshFullItem( - item, - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - ReplaceAllMetadata = true, - ReplaceAllImages = request.ReplaceAllImages, - SearchResult = request - }, - CancellationToken.None); - } - - /// - /// Gets the remote image. - /// - /// The request. - /// Task{System.Object}. - private async Task GetRemoteImage(GetRemoteSearchImage request) - { - var urlHash = request.ImageUrl.GetMD5(); - var pointerCachePath = GetFullCachePath(urlHash.ToString()); - - string contentPath; - - try - { - contentPath = File.ReadAllText(pointerCachePath); - - if (File.Exists(contentPath)) - { - return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); - } - } - catch (FileNotFoundException) - { - // Means the file isn't cached yet - } - catch (IOException) - { - // Means the file isn't cached yet - } - - await DownloadImage(request.ProviderName, request.ImageUrl, urlHash, pointerCachePath).ConfigureAwait(false); - - // Read the pointer file again - contentPath = File.ReadAllText(pointerCachePath); - - return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); - } - - /// - /// 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) - { - var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false); - - var ext = result.ContentType.Split('/').Last(); - - var fullCachePath = GetFullCachePath(urlHash + "." + ext); - - Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); - using (var stream = result.Content) - { - 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)); - File.WriteAllText(pointerCachePath, fullCachePath); - } - - /// - /// 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); - } -}