From ecd2c2722fef26ed7e8c7e3e3ed526f46d83258a Mon Sep 17 00:00:00 2001 From: glax Date: Tue, 17 Jun 2025 18:28:18 +0200 Subject: [PATCH] Fix FlareSolverr, Flaresolverrsharp is broken --- API/API.csproj | 1 - API/Controllers/SettingsController.cs | 34 +--- API/MangaDownloadClients/DownloadClient.cs | 2 +- .../FlareSolverrDownloadClient.cs | 180 ++++++++++++++++++ .../HttpDownloadClient.cs | 7 +- README.md | 1 - 6 files changed, 187 insertions(+), 38 deletions(-) create mode 100644 API/MangaDownloadClients/FlareSolverrDownloadClient.cs diff --git a/API/API.csproj b/API/API.csproj index 6453ad5..24d1e9c 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -11,7 +11,6 @@ - diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 39233f7..2c060f3 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -4,7 +4,6 @@ using API.Schema; using API.Schema.Contexts; using API.Schema.Jobs; using Asp.Versioning; -using FlareSolverrSharp; using log4net; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; @@ -321,43 +320,18 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller } /// - /// + /// Test FlareSolverr /// /// FlareSolverr is working! - /// FlareSolverr URL is malformed /// FlareSolverr is not working - /// FlareSolverr could not be reached [HttpPost("FlareSolverr/Test")] [ProducesResponseType(Status200OK)] - [ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status500InternalServerError)] - [ProducesResponseType(Status503ServiceUnavailable)] public IActionResult TestFlareSolverrReachable() { const string knownProtectedUrl = "https://prowlarr.servarr.com/v1/ping"; - HttpClient client = new(); - if (!Uri.TryCreate(new(TrangaSettings.flareSolverrUrl), "v1", out Uri? uri)) - return BadRequest(); - HttpRequestMessage request = new(HttpMethod.Post, uri); - JObject data = new() - { - ["cmd"] = "request.get", - ["url"] = knownProtectedUrl - }; - request.Content = new StringContent(JsonConvert.SerializeObject(data)); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - HttpResponseMessage response = client.Send(request); - if (!response.IsSuccessStatusCode) - return StatusCode(Status503ServiceUnavailable); - client = new(new ClearanceHandler(TrangaSettings.flareSolverrUrl)); - try - { - client.GetStringAsync(knownProtectedUrl).Wait(); - return Ok(); - } - catch (Exception e) - { - return StatusCode(Status500InternalServerError); - } + FlareSolverrDownloadClient client = new(); + RequestResult result = client.MakeRequestInternal(knownProtectedUrl); + return (int)result.statusCode >= 200 && (int)result.statusCode < 300 ? Ok() : StatusCode(500, result.statusCode); } } \ No newline at end of file diff --git a/API/MangaDownloadClients/DownloadClient.cs b/API/MangaDownloadClients/DownloadClient.cs index 542d552..076937f 100644 --- a/API/MangaDownloadClients/DownloadClient.cs +++ b/API/MangaDownloadClients/DownloadClient.cs @@ -3,7 +3,7 @@ using log4net; namespace API.MangaDownloadClients; -internal abstract class DownloadClient +public abstract class DownloadClient { private static readonly Dictionary LastExecutedRateLimit = new(); protected ILog Log { get; init; } diff --git a/API/MangaDownloadClients/FlareSolverrDownloadClient.cs b/API/MangaDownloadClients/FlareSolverrDownloadClient.cs new file mode 100644 index 0000000..8b66bd4 --- /dev/null +++ b/API/MangaDownloadClients/FlareSolverrDownloadClient.cs @@ -0,0 +1,180 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using HtmlAgilityPack; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace API.MangaDownloadClients; + +public class FlareSolverrDownloadClient : DownloadClient +{ + + + internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null) + { + if (clickButton is not null) + Log.Warn("Client can not click button"); + if(referrer is not null) + Log.Warn("Client can not set referrer"); + if (TrangaSettings.flareSolverrUrl == string.Empty) + { + Log.Error("FlareSolverr URL is empty"); + return new(HttpStatusCode.InternalServerError, null, Stream.Null); + } + + Uri flareSolverrUri = new (TrangaSettings.flareSolverrUrl); + if (flareSolverrUri.Segments.Last() != "v1") + flareSolverrUri = new UriBuilder(flareSolverrUri) + { + Path = "v1" + }.Uri; + + HttpClient client = new() + { + Timeout = TimeSpan.FromSeconds(10), + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher, + DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } } + }; + + JObject requestObj = new() + { + ["cmd"] = "request.get", + ["url"] = url + }; + + HttpRequestMessage requestMessage = new(HttpMethod.Post, flareSolverrUri) + { + Content = new StringContent(JsonConvert.SerializeObject(requestObj)), + }; + requestMessage.Content.Headers.ContentType = new ("application/json"); + Log.Debug($"Requesting {url}"); + + HttpResponseMessage? response; + try + { + response = client.Send(requestMessage); + } + catch (HttpRequestException e) + { + Log.Error(e); + return new (HttpStatusCode.Unused, null, Stream.Null); + } + + if (!response.IsSuccessStatusCode) + { + Log.Debug($"Request returned status code {(int)response.StatusCode} {response.StatusCode}:\n" + + $"=====\n" + + $"Request:\n" + + $"{requestMessage.Method} {requestMessage.RequestUri}\n" + + $"{requestMessage.Version} {requestMessage.VersionPolicy}\n" + + $"Headers:\n\t{string.Join("\n\t", requestMessage.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" + + $"{requestMessage.Content?.ReadAsStringAsync().Result}" + + $"=====\n" + + $"Response:\n" + + $"{response.Version}\n" + + $"Headers:\n\t{string.Join("\n\t", response.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" + + $"{response.Content.ReadAsStringAsync().Result}"); + return new (response.StatusCode, null, Stream.Null); + } + + string responseString = response.Content.ReadAsStringAsync().Result; + JObject responseObj = JObject.Parse(responseString); + if (!IsInCorrectFormat(responseObj, out string? reason)) + { + Log.Error($"Wrong format: {reason}"); + return new(HttpStatusCode.Unused, null, Stream.Null); + } + + string statusResponse = responseObj["status"]!.Value()!; + if (statusResponse != "ok") + { + Log.Debug($"Status is not ok: {statusResponse}"); + return new(HttpStatusCode.Unused, null, Stream.Null); + } + JObject solution = (responseObj["solution"] as JObject)!; + + if (!Enum.TryParse(solution["status"]!.Value().ToString(), out HttpStatusCode statusCode)) + { + Log.Error($"Wrong format: Cant parse status code: {solution["status"]!.Value()}"); + return new(HttpStatusCode.Unused, null, Stream.Null); + } + if (statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.MultipleChoices) + { + Log.Debug($"Status is: {statusCode}"); + return new(statusCode, null, Stream.Null); + } + + if (solution["response"]!.Value() is not { } htmlString) + { + Log.Error("Wrong format: Cant find response in solution"); + return new(HttpStatusCode.Unused, null, Stream.Null); + } + + if (IsJson(htmlString, out HtmlDocument document, out string? json)) + { + MemoryStream ms = new(); + ms.Write(Encoding.UTF8.GetBytes(json)); + ms.Position = 0; + return new(statusCode, document, ms); + } + else + { + MemoryStream ms = new(); + ms.Write(Encoding.UTF8.GetBytes(htmlString)); + ms.Position = 0; + return new(statusCode, document, ms); + } + } + + private bool IsInCorrectFormat(JObject responseObj, [NotNullWhen(false)]out string? reason) + { + reason = null; + if (!responseObj.ContainsKey("status")) + { + reason = "Cant find status on response"; + return false; + } + + if (responseObj["solution"] is not JObject solution) + { + reason = "Cant find solution"; + return false; + } + + if (!solution.ContainsKey("status")) + { + reason = "Wrong format: Cant find status in solution"; + return false; + } + + if (!solution.ContainsKey("response")) + { + + reason = "Wrong format: Cant find response in solution"; + return false; + } + + return true; + } + + private bool IsJson(string htmlString, out HtmlDocument document, [NotNullWhen(true)]out string? jsonString) + { + jsonString = null; + document = new(); + document.LoadHtml(htmlString); + + HtmlNode pre = document.DocumentNode.SelectSingleNode("//pre"); + try + { + JObject.Parse(pre.InnerText); + jsonString = pre.InnerText; + return true; + } + catch (JsonReaderException) + { + return false; + } + } +} \ No newline at end of file diff --git a/API/MangaDownloadClients/HttpDownloadClient.cs b/API/MangaDownloadClients/HttpDownloadClient.cs index cf9bedf..e2d55da 100644 --- a/API/MangaDownloadClients/HttpDownloadClient.cs +++ b/API/MangaDownloadClients/HttpDownloadClient.cs @@ -1,5 +1,4 @@ using System.Net; -using FlareSolverrSharp; using HtmlAgilityPack; namespace API.MangaDownloadClients; @@ -10,9 +9,7 @@ internal class HttpDownloadClient : DownloadClient { if (clickButton is not null) Log.Warn("Client can not click button"); - HttpClient client = TrangaSettings.flareSolverrUrl == string.Empty - ? new () - : new (new ClearanceHandler(TrangaSettings.flareSolverrUrl)); + HttpClient client = new(); client.Timeout = TimeSpan.FromSeconds(10); client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; client.DefaultRequestHeaders.Add("User-Agent", TrangaSettings.userAgent); @@ -46,7 +43,7 @@ internal class HttpDownloadClient : DownloadClient $"{response.Version}\n" + $"Headers:\n\t{string.Join("\n\t", response.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" + $"{response.Content.ReadAsStringAsync().Result}"); - return new (response.StatusCode, null, Stream.Null); + return new FlareSolverrDownloadClient().MakeRequestInternal(url, referrer, clickButton); } Stream stream; diff --git a/README.md b/README.md index 0120c11..a68ff62 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,6 @@ Endpoints are documented in Swagger. Just spin up an instance, and go to `http:/ - [Ngpsql](https://github.com/npgsql/npgsql/blob/main/LICENSE) - [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md) - [PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp/blob/master/LICENSE) -- [FlareSolverrSharp](https://github.com/FlareSolverr/FlareSolverrSharp) - [Html Agility Pack (HAP)](https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE) - [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch/blob/main/LICENSE) - [Sixlabors.ImageSharp](https://docs-v2.sixlabors.com/articles/imagesharp/index.html#license)