diff --git a/frontend/src/Movie/MovieImage.js b/frontend/src/Movie/MovieImage.js index ab587e6a7..36dff1c06 100644 --- a/frontend/src/Movie/MovieImage.js +++ b/frontend/src/Movie/MovieImage.js @@ -160,6 +160,7 @@ class MovieImage extends Component { src={url} onError={this.onError} onLoad={this.onLoad} + rel="noreferrer" /> ); diff --git a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs index 65972c007..6cb904ad4 100644 --- a/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs +++ b/src/NzbDrone.Core/Extras/Metadata/MetadataService.cs @@ -280,6 +280,10 @@ namespace NzbDrone.Core.Extras.Metadata _mediaFileAttributeService.SetFilePermissions(fullPath); } + catch (HttpException ex) + { + _logger.Warn(ex, "Couldn't download image {0} for {1}. {2}", image.Url, movie, ex.Message); + } catch (WebException ex) { _logger.Warn(ex, "Couldn't download image {0} for {1}. {2}", image.Url, movie, ex.Message); diff --git a/src/NzbDrone.Core/MediaCover/MediaCover.cs b/src/NzbDrone.Core/MediaCover/MediaCover.cs index 459fa6373..acea3ab42 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCover.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCover.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.MediaCover { public MediaCoverTypes CoverType { get; set; } public string Url { get; 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 aba2e17fa..3128df70e 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.MediaCover IHandleAsync, IMapCoversToLocal { + private readonly IMediaCoverProxy _mediaCoverProxy; private readonly IImageResizer _resizer; private readonly IHttpClient _httpClient; private readonly IDiskProvider _diskProvider; @@ -40,7 +41,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, IHttpClient httpClient, IDiskProvider diskProvider, IAppFolderInfo appFolderInfo, @@ -49,6 +51,7 @@ namespace NzbDrone.Core.MediaCover IEventAggregator eventAggregator, Logger logger) { + _mediaCoverProxy = mediaCoverProxy; _resizer = resizer; _httpClient = httpClient; _diskProvider = diskProvider; @@ -69,16 +72,29 @@ namespace NzbDrone.Core.MediaCover public void ConvertToLocalUrls(int movieId, IEnumerable covers) { - foreach (var mediaCover in covers) + if (movieId == 0) { - var filePath = GetCoverPath(movieId, mediaCover.CoverType); - - mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/" + movieId + "/" + mediaCover.CoverType.ToString().ToLower() + ".jpg"; - - if (_diskProvider.FileExists(filePath)) + // Movie isn't in Radarr yet, map via a proxy to circument referrer issues + foreach (var mediaCover in covers) { - var lastWrite = _diskProvider.FileGetLastWrite(filePath); - mediaCover.Url += "?lastWrite=" + lastWrite.Ticks; + mediaCover.RemoteUrl = mediaCover.Url; + mediaCover.Url = _mediaCoverProxy.RegisterUrl(mediaCover.RemoteUrl); + } + } + else + { + foreach (var mediaCover in covers) + { + var filePath = GetCoverPath(movieId, mediaCover.CoverType); + + mediaCover.RemoteUrl = mediaCover.Url; + mediaCover.Url = _configFileProvider.UrlBase + @"/MediaCover/" + movieId + "/" + mediaCover.CoverType.ToString().ToLower() + ".jpg"; + + if (_diskProvider.FileExists(filePath)) + { + var lastWrite = _diskProvider.FileGetLastWrite(filePath); + mediaCover.Url += "?lastWrite=" + lastWrite.Ticks; + } } } } @@ -106,6 +122,10 @@ namespace NzbDrone.Core.MediaCover updated = true; } } + catch (HttpException e) + { + _logger.Warn("Couldn't download media cover for {0}. {1}", movie, e.Message); + } catch (WebException e) { _logger.Warn("Couldn't download media cover for {0}. {1}", movie, e.Message); diff --git a/src/Radarr.Api.V3/Movies/MovieLookupModule.cs b/src/Radarr.Api.V3/Movies/MovieLookupModule.cs index d175cf3c6..12fa2c356 100644 --- a/src/Radarr.Api.V3/Movies/MovieLookupModule.cs +++ b/src/Radarr.Api.V3/Movies/MovieLookupModule.cs @@ -17,17 +17,20 @@ namespace Radarr.Api.V3.Movies private readonly ISearchForNewMovie _searchProxy; private readonly IProvideMovieInfo _movieInfo; private readonly IBuildFileNames _fileNameBuilder; + private readonly IMapCoversToLocal _coverMapper; private readonly IConfigService _configService; public MovieLookupModule(ISearchForNewMovie searchProxy, IProvideMovieInfo movieInfo, IBuildFileNames fileNameBuilder, + IMapCoversToLocal coverMapper, IConfigService configService) : base("/movie/lookup") { _movieInfo = movieInfo; _searchProxy = searchProxy; _fileNameBuilder = fileNameBuilder; + _coverMapper = coverMapper; _configService = configService; Get("/", x => Search()); Get("/tmdb", x => SearchByTmdbId()); @@ -69,10 +72,13 @@ namespace Radarr.Api.V3.Movies { var translation = currentMovie.Translations.FirstOrDefault(t => t.Language == (Language)_configService.MovieInfoLanguage); var resource = currentMovie.ToResource(translation); + + _coverMapper.ConvertToLocalUrls(resource.Id, resource.Images); + var poster = currentMovie.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); if (poster != null) { - resource.RemotePoster = poster.Url; + resource.RemotePoster = poster.RemoteUrl; } resource.Folder = _fileNameBuilder.GetMovieFolder(currentMovie); diff --git a/src/Radarr.Http/Frontend/Mappers/MediaCoverMapper.cs b/src/Radarr.Http/Frontend/Mappers/MediaCoverMapper.cs index 4303de73a..8522edf62 100644 --- a/src/Radarr.Http/Frontend/Mappers/MediaCoverMapper.cs +++ b/src/Radarr.Http/Frontend/Mappers/MediaCoverMapper.cs @@ -43,7 +43,7 @@ namespace Radarr.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/Radarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs b/src/Radarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs new file mode 100644 index 000000000..23e2876ca --- /dev/null +++ b/src/Radarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using System.Text.RegularExpressions; +using Nancy; +using Nancy.Responses; +using NzbDrone.Core.MediaCover; + +namespace Radarr.Http.Frontend.Mappers +{ + public class MediaCoverProxyMapper : IMapHttpRequestsToDisk + { + private readonly Regex _regex = new Regex(@"/MediaCoverProxy/(?\w+)/(?(.+)\.(jpg|png|gif))"); + + private readonly IMediaCoverProxy _mediaCoverProxy; + + public MediaCoverProxyMapper(IMediaCoverProxy mediaCoverProxy) + { + _mediaCoverProxy = mediaCoverProxy; + } + + public string Map(string resourceUrl) + { + return null; + } + + public bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/MediaCoverProxy/", StringComparison.InvariantCultureIgnoreCase); + } + + public Response GetResponse(string resourceUrl) + { + var match = _regex.Match(resourceUrl); + + if (!match.Success) + { + return new NotFoundResponse(); + } + + var hash = match.Groups["hash"].Value; + var filename = match.Groups["filename"].Value; + + var imageData = _mediaCoverProxy.GetImage(hash); + + return new StreamResponse(() => new MemoryStream(imageData), MimeTypes.GetMimeType(filename)); + } + } +}