From 9aa76a7faabed64cc6257f59b03655de736ab81f Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Thu, 21 Mar 2024 09:51:06 -0400 Subject: [PATCH] Reuse HttpClient for AniDB Http Commands --- .../ImageDownload/ImageDownloadRequest.cs | 224 ------------------ .../ImageDownload/ImageDownloadResult.cs | 38 +++ .../AniDB/HTTP/AniDBHttpConnectionHandler.cs | 20 +- 3 files changed, 48 insertions(+), 234 deletions(-) delete mode 100644 Shoko.Server/ImageDownload/ImageDownloadRequest.cs create mode 100644 Shoko.Server/ImageDownload/ImageDownloadResult.cs diff --git a/Shoko.Server/ImageDownload/ImageDownloadRequest.cs b/Shoko.Server/ImageDownload/ImageDownloadRequest.cs deleted file mode 100644 index 5095f6261..000000000 --- a/Shoko.Server/ImageDownload/ImageDownloadRequest.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System.Net; -using System.IO; -using System.Threading; -using System.Net.Http; -using Shoko.Models.Server; -using Shoko.Server.Extensions; -using Shoko.Server.Models; -using Shoko.Server.Server; -using Shoko.Commons.Utils; -using Shoko.Server.Providers.AniDB; - -#nullable enable -namespace Shoko.Server.ImageDownload; - -/// -/// Represents the result of an image download operation. -/// -public enum ImageDownloadResult -{ - /// - /// The image was successfully downloaded and saved. - /// - Success = 1, - - /// - /// The image was not downloaded because it was already available in the cache. - /// - Cached = 2, - - /// - /// The image could not be downloaded due to not being able to get the - /// source or destination. - /// - Failure = 3, - - /// - /// The image was not downloaded because the resource has been removed or is - /// no longer available, but we could not remove the local entry because of - /// it's type. - /// - InvalidResource = 4, - - /// - /// The image was not downloaded because the resource has been removed or is - /// no longer available, and thus have also been removed from the local - /// database. - /// - RemovedResource = 5, -} - -public class ImageDownloadRequest -{ - - private object ImageData { get; } - - public bool ForceDownload { get; } - - private string ImageServerUrl { get; } - - private string? _filePath { get; set; } = null; - - public string FilePath - => _filePath != null ? _filePath : _filePath = ImageData switch - { - AniDB_Character character => character.GetPosterPath(), - AniDB_Seiyuu creator => creator.GetPosterPath(), - MovieDB_Fanart image => image.GetFullImagePath(), - MovieDB_Poster image => image.GetFullImagePath(), - SVR_AniDB_Anime anime => anime.PosterPath, - TvDB_Episode episode => episode.GetFullImagePath(), - TvDB_ImageFanart image => image.GetFullImagePath(), - TvDB_ImagePoster image => image.GetFullImagePath(), - TvDB_ImageWideBanner image => image.GetFullImagePath(), - _ => string.Empty, - }; - - private string? _downloadUrl { get; set; } = null; - - public string DownloadUrl - => _downloadUrl != null ? _downloadUrl : _downloadUrl = ImageData switch - { - AniDB_Character character => string.Format(ImageServerUrl, character.PicName), - AniDB_Seiyuu creator => string.Format(ImageServerUrl, creator.PicName), - MovieDB_Fanart movieFanart => string.Format(Constants.URLS.MovieDB_Images, movieFanart.URL), - MovieDB_Poster moviePoster => string.Format(Constants.URLS.MovieDB_Images, moviePoster.URL), - SVR_AniDB_Anime anime => string.Format(ImageServerUrl, anime.Picname), - TvDB_Episode ep => string.Format(Constants.URLS.TvDB_Episode_Images, ep.Filename), - TvDB_ImageFanart fanart => string.Format(Constants.URLS.TvDB_Images, fanart.BannerPath), - TvDB_ImagePoster poster => string.Format(Constants.URLS.TvDB_Images, poster.BannerPath), - TvDB_ImageWideBanner wideBanner => string.Format(Constants.URLS.TvDB_Images, wideBanner.BannerPath), - _ => string.Empty - }; - - public bool IsImageValid - => !string.IsNullOrEmpty(DownloadUrl) && !string.IsNullOrEmpty(FilePath) && File.Exists(FilePath) && Misc.IsImageValid(FilePath); - - private bool ShouldAniDBRateLimit - => ImageData switch - { - AniDB_Character => true, - AniDB_Seiyuu => true, - SVR_AniDB_Anime => true, - _ => false, - }; - - public ImageDownloadRequest(object data, bool forceDownload, string? imageServerUrl = null) - { - ImageData = data; - ForceDownload = forceDownload; - ImageServerUrl = imageServerUrl ?? ""; - } - - public ImageDownloadResult DownloadNow(int maxRetries = 5) - => RecursivelyRetryDownload(0, maxRetries); - - private ImageDownloadResult RecursivelyRetryDownload(int count, int maxRetries) - { - // Abort if the download url or final destination is not available. - if (string.IsNullOrEmpty(DownloadUrl) || string.IsNullOrEmpty(FilePath)) - return ImageDownloadResult.Failure; - - var imageValid = File.Exists(FilePath) && Misc.IsImageValid(FilePath); - if (imageValid && !ForceDownload) - return ImageDownloadResult.Cached; - - var tempPath = Path.Combine(ImageUtils.GetImagesTempFolder(), Path.GetFileName(FilePath)); - try - { - // Rate limit anidb image requests. - if (ShouldAniDBRateLimit) - AniDbImageRateLimiter.Instance.EnsureRate(); - - // Ignore all certificate failures. - ServicePointManager.Expect100Continue = true; - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; - ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; - - // Download the image. - using (var client = new HttpClient()) - { - // Download the image data. - client.DefaultRequestHeaders.Add("user-agent", "JMM"); - var bytes = client.GetByteArrayAsync(DownloadUrl) - .ConfigureAwait(false) - .GetAwaiter() - .GetResult(); - if (bytes.Length < 4) - throw new WebException( - "The image download stream returned less than 4 bytes (a valid image has 2-4 bytes in the header)"); - - // Check if the image format is valid. - if (Misc.GetImageFormat(bytes) == null) - throw new WebException("The image download stream returned an invalid image"); - - // Delete the existing (failed?) temporary file. - if (File.Exists(tempPath)) - File.Delete(tempPath); - - // Write the image data to the temp file. - using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write)) - fs.Write(bytes, 0, bytes.Length); - - // Make sure the directory structure exists. - var dirPath = Path.GetDirectoryName(FilePath); - if (!string.IsNullOrEmpty(dirPath) && !Directory.Exists(dirPath)) - Directory.CreateDirectory(dirPath); - - // Delete the existing file if we're re-downloading. - if (File.Exists(FilePath)) - { - File.Delete(FilePath); - } - - // Move the temp file to it's final destination. - File.Move(tempPath, FilePath); - - return ImageDownloadResult.Success; - } - } - catch (HttpRequestException ex) - { - // Mark the request as a failure if we received a 404 or 403. - if (ex.StatusCode.HasValue && (ex.StatusCode.Value == HttpStatusCode.Forbidden || ex.StatusCode.Value == HttpStatusCode.NotFound)) - { - var removed = RemoveResource(); - return removed ? ImageDownloadResult.RemovedResource : ImageDownloadResult.InvalidResource; - } - - throw; - } - catch (WebException) - { - if (count + 1 >= maxRetries) - throw; - - Thread.Sleep(1000); - return RecursivelyRetryDownload(count + 1, maxRetries); - } - } - - private bool RemoveResource() - { - switch (ImageData) - { - case MovieDB_Fanart movieFanart: - Repositories.RepoFactory.MovieDB_Fanart.Delete(movieFanart); - return true; - case MovieDB_Poster moviePoster: - Repositories.RepoFactory.MovieDB_Poster.Delete(moviePoster); - return true; - case TvDB_ImageFanart tvdbFanart: - Repositories.RepoFactory.TvDB_ImageFanart.Delete(tvdbFanart); - return true; - case TvDB_ImagePoster tvdbPoster: - Repositories.RepoFactory.TvDB_ImagePoster.Delete(tvdbPoster); - return true; - case TvDB_ImageWideBanner tvdbWideBanner: - Repositories.RepoFactory.TvDB_ImageWideBanner.Delete(tvdbWideBanner); - return true; - } - - return false; - } -} diff --git a/Shoko.Server/ImageDownload/ImageDownloadResult.cs b/Shoko.Server/ImageDownload/ImageDownloadResult.cs new file mode 100644 index 000000000..c054d0e07 --- /dev/null +++ b/Shoko.Server/ImageDownload/ImageDownloadResult.cs @@ -0,0 +1,38 @@ +#nullable enable +namespace Shoko.Server.ImageDownload; + +/// +/// Represents the result of an image download operation. +/// +public enum ImageDownloadResult +{ + /// + /// The image was successfully downloaded and saved. + /// + Success = 1, + + /// + /// The image was not downloaded because it was already available in the cache. + /// + Cached = 2, + + /// + /// The image could not be downloaded due to not being able to get the + /// source or destination. + /// + Failure = 3, + + /// + /// The image was not downloaded because the resource has been removed or is + /// no longer available, but we could not remove the local entry because of + /// its type. + /// + InvalidResource = 4, + + /// + /// The image was not downloaded because the resource has been removed or is + /// no longer available, and thus have also been removed from the local + /// database. + /// + RemovedResource = 5, +} diff --git a/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs b/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs index d2da1e0ce..f3b3cba76 100644 --- a/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs +++ b/Shoko.Server/Providers/AniDB/HTTP/AniDBHttpConnectionHandler.cs @@ -10,6 +10,7 @@ namespace Shoko.Server.Providers.AniDB.HTTP; public class AniDBHttpConnectionHandler : ConnectionHandler, IHttpConnectionHandler { + private readonly HttpClient _httpClient; public override double BanTimerResetLength => 12; public override string Type => "HTTP"; @@ -18,6 +19,14 @@ public class AniDBHttpConnectionHandler : ConnectionHandler, IHttpConnectionHand public AniDBHttpConnectionHandler(ILoggerFactory loggerFactory, HttpRateLimiter rateLimiter) : base(loggerFactory, rateLimiter) { + _httpClient = new HttpClient(new HttpClientHandler + { + AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate, + }); + _httpClient.Timeout = TimeSpan.FromSeconds(20); + _httpClient.DefaultRequestHeaders.AcceptEncoding.Add(new System.Net.Http.Headers.StringWithQualityHeaderValue("gzip")); + _httpClient.DefaultRequestHeaders.AcceptEncoding.Add(new System.Net.Http.Headers.StringWithQualityHeaderValue("deflate")); + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); } public async Task> GetHttp(string url) @@ -39,16 +48,7 @@ public async Task> GetHttpDirectly(string url) RateLimiter.EnsureRate(); - var client = new HttpClient(new HttpClientHandler - { - AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate, - }); - client.Timeout = TimeSpan.FromSeconds(20); - client.DefaultRequestHeaders.AcceptEncoding.Add(new System.Net.Http.Headers.StringWithQualityHeaderValue("gzip")); - client.DefaultRequestHeaders.AcceptEncoding.Add(new System.Net.Http.Headers.StringWithQualityHeaderValue("deflate")); - client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); - - using var response = await client.GetAsync(url); + using var response = await _httpClient.GetAsync(url); response.EnsureSuccessStatusCode(); var responseStream = await response.Content.ReadAsStreamAsync();