From 498368e8c5454228a8b8dfc350f510079a23af09 Mon Sep 17 00:00:00 2001 From: Qstick Date: Wed, 13 Apr 2022 23:51:12 -0500 Subject: [PATCH] MediaCover Pulls and Fixes --- .../Albums/AlbumLookupController.cs | 11 +++- .../Artist/ArtistLookupController.cs | 11 +++- .../Mappers/IMapHttpRequestsToDisk.cs | 2 +- .../Frontend/Mappers/MediaCoverMapper.cs | 2 +- .../Frontend/Mappers/MediaCoverProxyMapper.cs | 55 ++++++++++++++++ .../Mappers/StaticResourceMapperBase.cs | 2 +- .../Frontend/StaticResourceController.cs | 2 +- src/NzbDrone.Core/MediaCover/MediaCover.cs | 1 + .../MediaCover/MediaCoverProxy.cs | 64 +++++++++++++++++++ .../MediaCover/MediaCoverService.cs | 47 ++++++++++---- .../TrackImport/ImportApprovedTracks.cs | 2 +- 11 files changed, 175 insertions(+), 24 deletions(-) create mode 100644 src/Lidarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs create mode 100644 src/NzbDrone.Core/MediaCover/MediaCoverProxy.cs diff --git a/src/Lidarr.Api.V1/Albums/AlbumLookupController.cs b/src/Lidarr.Api.V1/Albums/AlbumLookupController.cs index 99eb63193..cd4e6b54a 100644 --- a/src/Lidarr.Api.V1/Albums/AlbumLookupController.cs +++ b/src/Lidarr.Api.V1/Albums/AlbumLookupController.cs @@ -11,10 +11,12 @@ namespace Lidarr.Api.V1.Albums public class AlbumLookupController : Controller { private readonly ISearchForNewAlbum _searchProxy; + private readonly IMapCoversToLocal _coverMapper; - public AlbumLookupController(ISearchForNewAlbum searchProxy) + public AlbumLookupController(ISearchForNewAlbum searchProxy, IMapCoversToLocal coverMapper) { _searchProxy = searchProxy; + _coverMapper = coverMapper; } [HttpGet] @@ -24,15 +26,18 @@ namespace Lidarr.Api.V1.Albums return MapToResource(searchResults).ToList(); } - private static IEnumerable MapToResource(IEnumerable albums) + private IEnumerable MapToResource(IEnumerable albums) { foreach (var currentAlbum in albums) { var resource = currentAlbum.ToResource(); + + _coverMapper.ConvertToLocalUrls(resource.Id, MediaCoverEntity.Album, resource.Images); + var cover = currentAlbum.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Cover); if (cover != null) { - resource.RemoteCover = cover.Url; + resource.RemoteCover = cover.RemoteUrl; } yield return resource; diff --git a/src/Lidarr.Api.V1/Artist/ArtistLookupController.cs b/src/Lidarr.Api.V1/Artist/ArtistLookupController.cs index 2c83a86df..598239ee9 100644 --- a/src/Lidarr.Api.V1/Artist/ArtistLookupController.cs +++ b/src/Lidarr.Api.V1/Artist/ArtistLookupController.cs @@ -11,10 +11,12 @@ namespace Lidarr.Api.V1.Artist public class ArtistLookupController : Controller { private readonly ISearchForNewArtist _searchProxy; + private readonly IMapCoversToLocal _coverMapper; - public ArtistLookupController(ISearchForNewArtist searchProxy) + public ArtistLookupController(ISearchForNewArtist searchProxy, IMapCoversToLocal coverMapper) { _searchProxy = searchProxy; + _coverMapper = coverMapper; } [HttpGet] @@ -24,15 +26,18 @@ namespace Lidarr.Api.V1.Artist return MapToResource(searchResults).ToList(); } - private static IEnumerable MapToResource(IEnumerable artist) + private IEnumerable MapToResource(IEnumerable artist) { foreach (var currentArtist in artist) { var resource = currentArtist.ToResource(); + + _coverMapper.ConvertToLocalUrls(resource.Id, MediaCoverEntity.Artist, resource.Images); + var poster = currentArtist.Metadata.Value.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); if (poster != null) { - resource.RemotePoster = poster.Url; + resource.RemotePoster = poster.RemoteUrl; } yield return resource; diff --git a/src/Lidarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs b/src/Lidarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs index 79cfd8336..0edecbd4c 100644 --- a/src/Lidarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs +++ b/src/Lidarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs @@ -6,6 +6,6 @@ namespace Lidarr.Http.Frontend.Mappers { string Map(string resourceUrl); bool CanHandle(string resourceUrl); - FileStreamResult GetResponse(string resourceUrl); + IActionResult GetResponse(string resourceUrl); } } diff --git a/src/Lidarr.Http/Frontend/Mappers/MediaCoverMapper.cs b/src/Lidarr.Http/Frontend/Mappers/MediaCoverMapper.cs index b794ad686..1099b5666 100644 --- a/src/Lidarr.Http/Frontend/Mappers/MediaCoverMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/MediaCoverMapper.cs @@ -43,7 +43,7 @@ namespace Lidarr.Http.Frontend.Mappers public override bool CanHandle(string resourceUrl) { - return resourceUrl.StartsWith("/MediaCover", StringComparison.InvariantCultureIgnoreCase); + return resourceUrl.StartsWith("/MediaCover/", StringComparison.InvariantCultureIgnoreCase); } } } diff --git a/src/Lidarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs b/src/Lidarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs new file mode 100644 index 000000000..5b07560e9 --- /dev/null +++ b/src/Lidarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs @@ -0,0 +1,55 @@ +using System; +using System.Net; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; +using NzbDrone.Core.MediaCover; + +namespace Lidarr.Http.Frontend.Mappers +{ + public class MediaCoverProxyMapper : IMapHttpRequestsToDisk + { + private readonly Regex _regex = new Regex(@"/MediaCoverProxy/(?\w+)/(?(.+)\.(jpg|png|gif))"); + + private readonly IMediaCoverProxy _mediaCoverProxy; + private readonly IContentTypeProvider _mimeTypeProvider; + + public MediaCoverProxyMapper(IMediaCoverProxy mediaCoverProxy) + { + _mediaCoverProxy = mediaCoverProxy; + _mimeTypeProvider = new FileExtensionContentTypeProvider(); + } + + public string Map(string resourceUrl) + { + return null; + } + + public bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/MediaCoverProxy/", StringComparison.InvariantCultureIgnoreCase); + } + + public IActionResult GetResponse(string resourceUrl) + { + var match = _regex.Match(resourceUrl); + + if (!match.Success) + { + return new StatusCodeResult((int)HttpStatusCode.NotFound); + } + + var hash = match.Groups["hash"].Value; + var filename = match.Groups["filename"].Value; + + var imageData = _mediaCoverProxy.GetImage(hash); + + if (!_mimeTypeProvider.TryGetContentType(filename, out var contentType)) + { + contentType = "application/octet-stream"; + } + + return new FileContentResult(imageData, contentType); + } + } +} diff --git a/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs index 9c518aee3..664259ede 100644 --- a/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs @@ -28,7 +28,7 @@ namespace Lidarr.Http.Frontend.Mappers public abstract bool CanHandle(string resourceUrl); - public FileStreamResult GetResponse(string resourceUrl) + public IActionResult GetResponse(string resourceUrl) { var filePath = Map(resourceUrl); diff --git a/src/Lidarr.Http/Frontend/StaticResourceController.cs b/src/Lidarr.Http/Frontend/StaticResourceController.cs index b373fc331..d5d293746 100644 --- a/src/Lidarr.Http/Frontend/StaticResourceController.cs +++ b/src/Lidarr.Http/Frontend/StaticResourceController.cs @@ -59,7 +59,7 @@ namespace Lidarr.Http.Frontend if (result != null) { - if (result.ContentType == "text/html") + if ((result as FileResult)?.ContentType == "text/html") { Response.Headers.DisableCache(); } diff --git a/src/NzbDrone.Core/MediaCover/MediaCover.cs b/src/NzbDrone.Core/MediaCover/MediaCover.cs index d991a14ec..bd2ff1524 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCover.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCover.cs @@ -45,6 +45,7 @@ namespace NzbDrone.Core.MediaCover public MediaCoverTypes CoverType { get; set; } public string Extension { get; private set; } + public string RemoteUrl { get; set; } public MediaCover() { diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverProxy.cs b/src/NzbDrone.Core/MediaCover/MediaCoverProxy.cs new file mode 100644 index 000000000..8f6ed8c9a --- /dev/null +++ b/src/NzbDrone.Core/MediaCover/MediaCoverProxy.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.IO; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.MediaCover +{ + public interface IMediaCoverProxy + { + string RegisterUrl(string url); + + string GetUrl(string hash); + byte[] GetImage(string hash); + } + + public class MediaCoverProxy : IMediaCoverProxy + { + private readonly IHttpClient _httpClient; + private readonly IConfigFileProvider _configFileProvider; + private readonly ICached _cache; + + public MediaCoverProxy(IHttpClient httpClient, IConfigFileProvider configFileProvider, ICacheManager cacheManager) + { + _httpClient = httpClient; + _configFileProvider = configFileProvider; + _cache = cacheManager.GetCache(GetType()); + } + + public string RegisterUrl(string url) + { + var hash = url.SHA256Hash(); + + _cache.Set(hash, url, TimeSpan.FromHours(24)); + + _cache.ClearExpired(); + + var fileName = Path.GetFileName(url); + return _configFileProvider.UrlBase + @"/MediaCoverProxy/" + hash + "/" + fileName; + } + + public string GetUrl(string hash) + { + var result = _cache.Find(hash); + + if (result == null) + { + throw new KeyNotFoundException("Url no longer in cache"); + } + + return result; + } + + public byte[] GetImage(string hash) + { + var url = GetUrl(hash); + + var request = new HttpRequest(url); + + return _httpClient.Get(request).ResponseData; + } + } +} diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs index b55910e0e..a36c93e01 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs @@ -30,6 +30,7 @@ namespace NzbDrone.Core.MediaCover IHandleAsync, IMapCoversToLocal { + private readonly IMediaCoverProxy _mediaCoverProxy; private readonly IImageResizer _resizer; private readonly IAlbumService _albumService; private readonly IHttpClient _httpClient; @@ -45,7 +46,8 @@ namespace NzbDrone.Core.MediaCover // So limit the number of concurrent resizing tasks private static SemaphoreSlim _semaphore = new SemaphoreSlim((int)Math.Ceiling(Environment.ProcessorCount / 2.0)); - public MediaCoverService(IImageResizer resizer, + public MediaCoverService(IMediaCoverProxy mediaCoverProxy, + IImageResizer resizer, IAlbumService albumService, IHttpClient httpClient, IDiskProvider diskProvider, @@ -55,6 +57,7 @@ namespace NzbDrone.Core.MediaCover IEventAggregator eventAggregator, Logger logger) { + _mediaCoverProxy = mediaCoverProxy; _resizer = resizer; _albumService = albumService; _httpClient = httpClient; @@ -83,23 +86,37 @@ namespace NzbDrone.Core.MediaCover public void ConvertToLocalUrls(int entityId, MediaCoverEntity coverEntity, IEnumerable covers) { - foreach (var mediaCover in covers) + if (entityId == 0) { - var filePath = GetCoverPath(entityId, coverEntity, mediaCover.CoverType, mediaCover.Extension, null); - - if (coverEntity == MediaCoverEntity.Album) + // Artist isn't in Lidarr yet, map via a proxy to circument referrer issues + foreach (var mediaCover in covers) { - mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/Albums/" + entityId + "/" + mediaCover.CoverType.ToString().ToLower() + mediaCover.Extension; + mediaCover.RemoteUrl = mediaCover.Url; + mediaCover.Url = _mediaCoverProxy.RegisterUrl(mediaCover.RemoteUrl); } - else + } + else + { + foreach (var mediaCover in covers) { - mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/" + entityId + "/" + mediaCover.CoverType.ToString().ToLower() + mediaCover.Extension; - } + var filePath = GetCoverPath(entityId, coverEntity, mediaCover.CoverType, mediaCover.Extension, null); - if (_diskProvider.FileExists(filePath)) - { - var lastWrite = _diskProvider.FileGetLastWrite(filePath); - mediaCover.Url += "?lastWrite=" + lastWrite.Ticks; + mediaCover.RemoteUrl = mediaCover.Url; + + if (coverEntity == MediaCoverEntity.Album) + { + mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/Albums/" + entityId + "/" + mediaCover.CoverType.ToString().ToLower() + mediaCover.Extension; + } + else + { + mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/" + entityId + "/" + mediaCover.CoverType.ToString().ToLower() + mediaCover.Extension; + } + + if (_diskProvider.FileExists(filePath)) + { + var lastWrite = _diskProvider.FileGetLastWrite(filePath); + mediaCover.Url += "?lastWrite=" + lastWrite.Ticks; + } } } } @@ -136,6 +153,10 @@ namespace NzbDrone.Core.MediaCover updated = true; } } + catch (HttpException e) + { + _logger.Warn("Couldn't download media cover for {0}. {1}", artist, e.Message); + } catch (WebException e) { _logger.Warn("Couldn't download media cover for {0}. {1}", artist, e.Message); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs index 28bfdbbf0..aecf64d3a 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs @@ -291,7 +291,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport foreach (var albumImport in albumImports) { var release = albumImport.First().ImportDecision.Item.Release; - var album = albumImport.First().ImportDecision.Item.Album; + var album = _albumService.GetAlbum(albumImport.First().ImportDecision.Item.Album.Id); var artist = albumImport.First().ImportDecision.Item.Artist; if (albumImport.Where(e => e.Errors.Count == 0).ToList().Count > 0 && artist != null && album != null)