mirror of
https://github.com/C9Glax/tranga.git
synced 2025-10-11 05:09:49 +02:00
Use DelegatingHandler for RateLimits
Some checks failed
Docker Image CI / build (push) Has been cancelled
Some checks failed
Docker Image CI / build (push) Has been cancelled
This commit is contained in:
@@ -60,17 +60,6 @@ public class SettingsController() : Controller
|
|||||||
return TypedResults.Ok();
|
return TypedResults.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get all Request-Limits
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpGet("RequestLimits")]
|
|
||||||
[ProducesResponseType<Dictionary<RequestType,int>>(Status200OK, "application/json")]
|
|
||||||
public Ok<Dictionary<RequestType,int>> GetRequestLimits()
|
|
||||||
{
|
|
||||||
return TypedResults.Ok(Tranga.Settings.RequestLimits);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update all Request-Limits to new values
|
/// Update all Request-Limits to new values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -82,48 +71,6 @@ public class SettingsController() : Controller
|
|||||||
return TypedResults.StatusCode(Status501NotImplemented);
|
return TypedResults.StatusCode(Status501NotImplemented);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates a Request-Limit value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="RequestType">Type of Request</param>
|
|
||||||
/// <param name="requestLimit">New limit in Requests/Minute</param>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
/// <response code="400">Limit needs to be greater than 0</response>
|
|
||||||
[HttpPatch("RequestLimits/{RequestType}")]
|
|
||||||
[ProducesResponseType(Status200OK)]
|
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
|
||||||
public Results<Ok, BadRequest> SetRequestLimit(RequestType RequestType, [FromBody]int requestLimit)
|
|
||||||
{
|
|
||||||
if (requestLimit <= 0)
|
|
||||||
return TypedResults.BadRequest();
|
|
||||||
Tranga.Settings.SetRequestLimit(RequestType, requestLimit);
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reset Request-Limit
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpDelete("RequestLimits/{RequestType}")]
|
|
||||||
[ProducesResponseType<string>(Status200OK)]
|
|
||||||
public Ok ResetRequestLimits(RequestType RequestType)
|
|
||||||
{
|
|
||||||
Tranga.Settings.SetRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reset Request-Limit
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200"></response>
|
|
||||||
[HttpDelete("RequestLimits")]
|
|
||||||
[ProducesResponseType<string>(Status200OK)]
|
|
||||||
public Ok ResetRequestLimits()
|
|
||||||
{
|
|
||||||
Tranga.Settings.ResetRequestLimits();
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns Level of Image-Compression for Images
|
/// Returns Level of Image-Compression for Images
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -260,12 +207,12 @@ public class SettingsController() : Controller
|
|||||||
[HttpPost("FlareSolverr/Test")]
|
[HttpPost("FlareSolverr/Test")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
[ProducesResponseType(Status500InternalServerError)]
|
||||||
public Results<Ok, InternalServerError> TestFlareSolverrReachable()
|
public async Task<Results<Ok, InternalServerError>> TestFlareSolverrReachable()
|
||||||
{
|
{
|
||||||
const string knownProtectedUrl = "https://prowlarr.servarr.com/v1/ping";
|
const string knownProtectedUrl = "https://prowlarr.servarr.com/v1/ping";
|
||||||
FlareSolverrDownloadClient client = new();
|
FlareSolverrDownloadClient client = new(new ());
|
||||||
RequestResult result = client.MakeRequestInternal(knownProtectedUrl);
|
HttpResponseMessage result = await client.MakeRequest(knownProtectedUrl, RequestType.Default);
|
||||||
return (int)result.statusCode >= 200 && (int)result.statusCode < 300 ? TypedResults.Ok() : TypedResults.InternalServerError();
|
return (int)result.StatusCode >= 200 && (int)result.StatusCode < 300 ? TypedResults.Ok() : TypedResults.InternalServerError();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@@ -5,9 +5,7 @@ using API.MangaDownloadClients;
|
|||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using log4net;
|
using log4net;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
namespace API.MangaConnectors;
|
namespace API.MangaConnectors;
|
||||||
@@ -15,7 +13,7 @@ namespace API.MangaConnectors;
|
|||||||
[PrimaryKey("Name")]
|
[PrimaryKey("Name")]
|
||||||
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)
|
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)
|
||||||
{
|
{
|
||||||
[NotMapped] internal DownloadClient downloadClient { get; init; } = null!;
|
[NotMapped] internal IDownloadClient downloadClient { get; init; } = null!;
|
||||||
[NotMapped] protected ILog Log { get; init; } = LogManager.GetLogger(name);
|
[NotMapped] protected ILog Log { get; init; } = LogManager.GetLogger(name);
|
||||||
[StringLength(32)] public string Name { get; init; } = name;
|
[StringLength(32)] public string Name { get; init; } = name;
|
||||||
[StringLength(8)] public string[] SupportedLanguages { get; init; } = supportedLanguages;
|
[StringLength(8)] public string[] SupportedLanguages { get; init; } = supportedLanguages;
|
||||||
@@ -50,14 +48,14 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
|
|||||||
if (File.Exists(saveImagePath))
|
if (File.Exists(saveImagePath))
|
||||||
return filename;
|
return filename;
|
||||||
|
|
||||||
RequestResult coverResult = downloadClient.MakeRequest(mangaId.Obj.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
|
HttpResponseMessage coverResult = downloadClient.MakeRequest(mangaId.Obj.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}").Result;
|
||||||
if ((int)coverResult.statusCode < 200 || (int)coverResult.statusCode >= 300)
|
if ((int)coverResult.StatusCode < 200 || (int)coverResult.StatusCode >= 300)
|
||||||
return SaveCoverImageToCache(mangaId, --retries);
|
return SaveCoverImageToCache(mangaId, --retries);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using MemoryStream ms = new();
|
using MemoryStream ms = new();
|
||||||
coverResult.result.CopyTo(ms);
|
coverResult.Content.ReadAsStream().CopyTo(ms);
|
||||||
byte[] imageBytes = ms.ToArray();
|
byte[] imageBytes = ms.ToArray();
|
||||||
Directory.CreateDirectory(TrangaSettings.CoverImageCacheOriginal);
|
Directory.CreateDirectory(TrangaSettings.CoverImageCacheOriginal);
|
||||||
File.WriteAllBytes(saveImagePath, imageBytes);
|
File.WriteAllBytes(saveImagePath, imageBytes);
|
||||||
|
@@ -35,14 +35,14 @@ public class MangaDex : MangaConnector
|
|||||||
$"&includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
|
$"&includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
|
||||||
offset += Limit;
|
offset += Limit;
|
||||||
|
|
||||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
|
HttpResponseMessage result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed).Result;
|
||||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
if ((int)result.StatusCode < 200 || (int)result.StatusCode >= 300)
|
||||||
{
|
{
|
||||||
Log.Error("Request failed");
|
Log.Error("Request failed");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
using StreamReader sr = new (result.result);
|
using StreamReader sr = new (result.Content.ReadAsStream());
|
||||||
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
||||||
|
|
||||||
if (jObject.Value<string>("result") != "ok")
|
if (jObject.Value<string>("result") != "ok")
|
||||||
@@ -96,14 +96,14 @@ public class MangaDex : MangaConnector
|
|||||||
$"https://api.mangadex.org/manga/{mangaIdOnSite}" +
|
$"https://api.mangadex.org/manga/{mangaIdOnSite}" +
|
||||||
$"?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
|
$"?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
|
||||||
|
|
||||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
|
HttpResponseMessage result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed).Result;
|
||||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
if ((int)result.StatusCode < 200 || (int)result.StatusCode >= 300)
|
||||||
{
|
{
|
||||||
Log.Error("Request failed");
|
Log.Error("Request failed");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
using StreamReader sr = new (result.result);
|
using StreamReader sr = new (result.Content.ReadAsStream());
|
||||||
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
||||||
|
|
||||||
if (jObject.Value<string>("result") != "ok")
|
if (jObject.Value<string>("result") != "ok")
|
||||||
@@ -138,14 +138,14 @@ public class MangaDex : MangaConnector
|
|||||||
$"contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&includeFutureUpdates=0&includes%5B%5D=";
|
$"contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&includeFutureUpdates=0&includes%5B%5D=";
|
||||||
offset += Limit;
|
offset += Limit;
|
||||||
|
|
||||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
|
HttpResponseMessage result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed).Result;
|
||||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
if ((int)result.StatusCode < 200 || (int)result.StatusCode >= 300)
|
||||||
{
|
{
|
||||||
Log.Error("Request failed");
|
Log.Error("Request failed");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
using StreamReader sr = new (result.result);
|
using StreamReader sr = new (result.Content.ReadAsStream());
|
||||||
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
||||||
|
|
||||||
if (jObject.Value<string>("result") != "ok")
|
if (jObject.Value<string>("result") != "ok")
|
||||||
@@ -191,14 +191,14 @@ public class MangaDex : MangaConnector
|
|||||||
string id = match.Groups[1].Value;
|
string id = match.Groups[1].Value;
|
||||||
string requestUrl = $"https://api.mangadex.org/at-home/server/{id}";
|
string requestUrl = $"https://api.mangadex.org/at-home/server/{id}";
|
||||||
|
|
||||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
HttpResponseMessage result = downloadClient.MakeRequest(requestUrl, RequestType.Default).Result;
|
||||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
if ((int)result.StatusCode < 200 || (int)result.StatusCode >= 300)
|
||||||
{
|
{
|
||||||
Log.Error("Request failed");
|
Log.Error("Request failed");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
using StreamReader sr = new (result.result);
|
using StreamReader sr = new (result.Content.ReadAsStream());
|
||||||
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
||||||
|
|
||||||
if (jObject.Value<string>("result") != "ok")
|
if (jObject.Value<string>("result") != "ok")
|
||||||
|
@@ -36,7 +36,7 @@ public class MangaPark : MangaConnector
|
|||||||
for (int page = 1;; page++) // break; in loop
|
for (int page = 1;; page++) // break; in loop
|
||||||
{
|
{
|
||||||
Uri searchUri = new(baseUri, $"search?word={HttpUtility.UrlEncode(mangaSearchName)}&lang={Tranga.Settings.DownloadLanguage}&page={page}");
|
Uri searchUri = new(baseUri, $"search?word={HttpUtility.UrlEncode(mangaSearchName)}&lang={Tranga.Settings.DownloadLanguage}&page={page}");
|
||||||
if (downloadClient.MakeRequest(searchUri.ToString(), RequestType.Default) is { statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result)
|
if (downloadClient.MakeRequest(searchUri.ToString(), RequestType.Default).Result is { StatusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result)
|
||||||
{
|
{
|
||||||
HtmlDocument document = result.CreateDocument();
|
HtmlDocument document = result.CreateDocument();
|
||||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract HAP sucks with nullable types
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract HAP sucks with nullable types
|
||||||
@@ -73,8 +73,8 @@ public class MangaPark : MangaConnector
|
|||||||
|
|
||||||
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
|
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
|
||||||
{
|
{
|
||||||
if (downloadClient.MakeRequest(url, RequestType.Default) is
|
if (downloadClient.MakeRequest(url, RequestType.Default).Result is
|
||||||
{ statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result)
|
{ StatusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result)
|
||||||
{
|
{
|
||||||
HtmlDocument document= result.CreateDocument();
|
HtmlDocument document= result.CreateDocument();
|
||||||
|
|
||||||
@@ -145,8 +145,8 @@ public class MangaPark : MangaConnector
|
|||||||
|
|
||||||
List<(Chapter, MangaConnectorId<Chapter>)> ret = [];
|
List<(Chapter, MangaConnectorId<Chapter>)> ret = [];
|
||||||
|
|
||||||
if (downloadClient.MakeRequest(requestUri.ToString(), RequestType.Default) is
|
if (downloadClient.MakeRequest(requestUri.ToString(), RequestType.Default).Result is
|
||||||
{ statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result)
|
{ StatusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result)
|
||||||
{
|
{
|
||||||
HtmlDocument document= result.CreateDocument();
|
HtmlDocument document= result.CreateDocument();
|
||||||
|
|
||||||
@@ -220,8 +220,8 @@ public class MangaPark : MangaConnector
|
|||||||
Log.Debug($"Using domain {domain}");
|
Log.Debug($"Using domain {domain}");
|
||||||
Uri baseUri = new ($"https://{domain}/");
|
Uri baseUri = new ($"https://{domain}/");
|
||||||
Uri requestUri = new (baseUri, $"title/{chapterId.IdOnConnectorSite}");
|
Uri requestUri = new (baseUri, $"title/{chapterId.IdOnConnectorSite}");
|
||||||
if (downloadClient.MakeRequest(requestUri.ToString(), RequestType.Default) is
|
if (downloadClient.MakeRequest(requestUri.ToString(), RequestType.Default).Result is
|
||||||
{ statusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result)
|
{ StatusCode: >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous } result)
|
||||||
{
|
{
|
||||||
HtmlDocument document = result.CreateDocument();
|
HtmlDocument document = result.CreateDocument();
|
||||||
|
|
||||||
@@ -240,10 +240,10 @@ public class MangaPark : MangaConnector
|
|||||||
|
|
||||||
internal static class MangaParkHelper
|
internal static class MangaParkHelper
|
||||||
{
|
{
|
||||||
internal static HtmlDocument CreateDocument(this RequestResult result)
|
internal static HtmlDocument CreateDocument(this HttpResponseMessage result)
|
||||||
{
|
{
|
||||||
HtmlDocument document = new();
|
HtmlDocument document = new();
|
||||||
StreamReader sr = new (result.result);
|
StreamReader sr = new (result.Content.ReadAsStream());
|
||||||
string htmlStr = sr.ReadToEnd().Replace("q:key", "qkey");
|
string htmlStr = sr.ReadToEnd().Replace("q:key", "qkey");
|
||||||
document.LoadHtml(htmlStr);
|
document.LoadHtml(htmlStr);
|
||||||
|
|
||||||
|
@@ -30,11 +30,11 @@ public sealed class Mangaworld : MangaConnector
|
|||||||
Uri baseUri = new ("https://www.mangaworld.cx/");
|
Uri baseUri = new ("https://www.mangaworld.cx/");
|
||||||
Uri searchUrl = new (baseUri, "archive?keyword=" + HttpUtility.UrlEncode(mangaSearchName));
|
Uri searchUrl = new (baseUri, "archive?keyword=" + HttpUtility.UrlEncode(mangaSearchName));
|
||||||
|
|
||||||
RequestResult res = downloadClient.MakeRequest(searchUrl.ToString(), RequestType.Default);
|
HttpResponseMessage res = downloadClient.MakeRequest(searchUrl.ToString(), RequestType.Default).Result;
|
||||||
if ((int)res.statusCode < 200 || (int)res.statusCode >= 300)
|
if ((int)res.StatusCode < 200 || (int)res.StatusCode >= 300)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
using StreamReader sr = new (res.result);
|
using StreamReader sr = new (res.Content.ReadAsStream());
|
||||||
string html = sr.ReadToEnd();
|
string html = sr.ReadToEnd();
|
||||||
|
|
||||||
HtmlDocument doc = new ();
|
HtmlDocument doc = new ();
|
||||||
@@ -85,11 +85,11 @@ public sealed class Mangaworld : MangaConnector
|
|||||||
string slug = parts[1];
|
string slug = parts[1];
|
||||||
|
|
||||||
string url = $"https://www.mangaworld.cx/manga/{id}/{slug}/";
|
string url = $"https://www.mangaworld.cx/manga/{id}/{slug}/";
|
||||||
RequestResult res = downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
HttpResponseMessage res = downloadClient.MakeRequest(url, RequestType.MangaInfo).Result;
|
||||||
if ((int)res.statusCode < 200 || (int)res.statusCode >= 300)
|
if ((int)res.StatusCode < 200 || (int)res.StatusCode >= 300)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
using StreamReader sr = new (res.result);
|
using StreamReader sr = new (res.Content.ReadAsStream());
|
||||||
string html = sr.ReadToEnd();
|
string html = sr.ReadToEnd();
|
||||||
|
|
||||||
HtmlDocument doc = new ();
|
HtmlDocument doc = new ();
|
||||||
@@ -175,11 +175,11 @@ public sealed class Mangaworld : MangaConnector
|
|||||||
{
|
{
|
||||||
string url = EnsureListStyle(chapterId.WebsiteUrl ?? $"https://www.mangaworld.cx/manga/{chapterId.IdOnConnectorSite}");
|
string url = EnsureListStyle(chapterId.WebsiteUrl ?? $"https://www.mangaworld.cx/manga/{chapterId.IdOnConnectorSite}");
|
||||||
|
|
||||||
RequestResult res = downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
HttpResponseMessage res = downloadClient.MakeRequest(url, RequestType.MangaInfo).Result;
|
||||||
if ((int)res.statusCode < 200 || (int)res.statusCode >= 300)
|
if ((int)res.StatusCode < 200 || (int)res.StatusCode >= 300)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
using StreamReader sr = new (res.result);
|
using StreamReader sr = new (res.Content.ReadAsStream());
|
||||||
string html = sr.ReadToEnd();
|
string html = sr.ReadToEnd();
|
||||||
|
|
||||||
Uri baseUri = new (url);
|
Uri baseUri = new (url);
|
||||||
@@ -354,20 +354,20 @@ public sealed class Mangaworld : MangaConnector
|
|||||||
baseUri = new (seriesUrl);
|
baseUri = new (seriesUrl);
|
||||||
|
|
||||||
// 1) tenta client "Default"
|
// 1) tenta client "Default"
|
||||||
RequestResult res = downloadClient.MakeRequest(seriesUrl, RequestType.Default);
|
HttpResponseMessage res = downloadClient.MakeRequest(seriesUrl, RequestType.Default).Result;
|
||||||
if ((int)res.statusCode >= 200 && (int)res.statusCode < 300)
|
if ((int)res.StatusCode >= 200 && (int)res.StatusCode < 300)
|
||||||
{
|
{
|
||||||
using StreamReader sr = new (res.result);
|
using StreamReader sr = new (res.Content.ReadAsStream());
|
||||||
string html = sr.ReadToEnd();
|
string html = sr.ReadToEnd();
|
||||||
if (!LooksLikeChallenge(html))
|
if (!LooksLikeChallenge(html))
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) fallback: client “MangaInfo” (proxy/Flare se configurato)
|
// 2) fallback: client “MangaInfo” (proxy/Flare se configurato)
|
||||||
RequestResult res2 = downloadClient.MakeRequest(seriesUrl, RequestType.MangaInfo);
|
HttpResponseMessage res2 = downloadClient.MakeRequest(seriesUrl, RequestType.MangaInfo).Result;
|
||||||
if ((int)res2.statusCode >= 200 && (int)res2.statusCode < 300)
|
if ((int)res2.StatusCode >= 200 && (int)res2.StatusCode < 300)
|
||||||
{
|
{
|
||||||
using StreamReader sr2 = new StreamReader(res2.result);
|
using StreamReader sr2 = new StreamReader(res2.Content.ReadAsStream());
|
||||||
return sr2.ReadToEnd();
|
return sr2.ReadToEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,56 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
namespace API.MangaDownloadClients;
|
||||||
using System.Net;
|
|
||||||
using log4net;
|
|
||||||
|
|
||||||
namespace API.MangaDownloadClients;
|
public interface IDownloadClient
|
||||||
|
|
||||||
public abstract class DownloadClient
|
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<RequestType, DateTime> LastExecutedRateLimit = new();
|
internal Task<HttpResponseMessage> MakeRequest(string url, RequestType requestType, string? referrer = null);
|
||||||
protected ILog Log { get; init; }
|
|
||||||
|
|
||||||
protected DownloadClient()
|
|
||||||
{
|
|
||||||
this.Log = LogManager.GetLogger(GetType());
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Requests still go too fast across threads!
|
|
||||||
public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
|
|
||||||
{
|
|
||||||
Log.Debug($"Requesting {requestType} {url}");
|
|
||||||
|
|
||||||
// If we don't have a RequestLimit set for a Type, use the default one
|
|
||||||
if (!Tranga.Settings.RequestLimits.ContainsKey(requestType))
|
|
||||||
requestType = RequestType.Default;
|
|
||||||
|
|
||||||
int rateLimit = Tranga.Settings.RequestLimits[requestType];
|
|
||||||
// TODO this probably needs a better check whether the useragent matches...
|
|
||||||
// If the UserAgent is the default one, do not exceed the default request-limits.
|
|
||||||
if (Tranga.Settings.UserAgent == TrangaSettings.DefaultUserAgent && rateLimit > TrangaSettings.DefaultRequestLimits[requestType])
|
|
||||||
rateLimit = TrangaSettings.DefaultRequestLimits[requestType];
|
|
||||||
|
|
||||||
// Apply the delay
|
|
||||||
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
|
|
||||||
DateTime now = DateTime.UtcNow;
|
|
||||||
LastExecutedRateLimit.TryAdd(requestType, now.Subtract(timeBetweenRequests));
|
|
||||||
|
|
||||||
TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(now.Subtract(LastExecutedRateLimit[requestType]));
|
|
||||||
Log.Debug($"Request limit {requestType} {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff}");
|
|
||||||
|
|
||||||
if (rateLimitTimeout > TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
Log.Info($"Waiting {rateLimitTimeout} for {url}");
|
|
||||||
Thread.Sleep(rateLimitTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the request
|
|
||||||
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
|
|
||||||
|
|
||||||
// Update the time the last request was made
|
|
||||||
LastExecutedRateLimit[requestType] = DateTime.UtcNow;
|
|
||||||
Log.Debug($"Result {url}: {result}");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal abstract RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null);
|
|
||||||
}
|
}
|
@@ -1,25 +1,26 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
using log4net;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace API.MangaDownloadClients;
|
namespace API.MangaDownloadClients;
|
||||||
|
|
||||||
public class FlareSolverrDownloadClient : DownloadClient
|
public class FlareSolverrDownloadClient(HttpClient client) : IDownloadClient
|
||||||
{
|
{
|
||||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
private ILog Log { get; } = LogManager.GetLogger(typeof(FlareSolverrDownloadClient));
|
||||||
|
|
||||||
|
public async Task<HttpResponseMessage> MakeRequest(string url, RequestType requestType, string? referrer = null)
|
||||||
{
|
{
|
||||||
if (clickButton is not null)
|
Log.Debug($"Using {typeof(FlareSolverrDownloadClient).FullName} for {url}");
|
||||||
Log.Warn("Client can not click button");
|
|
||||||
if(referrer is not null)
|
if(referrer is not null)
|
||||||
Log.Warn("Client can not set referrer");
|
Log.Warn("Client can not set referrer");
|
||||||
if (Tranga.Settings.FlareSolverrUrl == string.Empty)
|
if (Tranga.Settings.FlareSolverrUrl == string.Empty)
|
||||||
{
|
{
|
||||||
Log.Error("FlareSolverr URL is empty");
|
Log.Error("FlareSolverr URL is empty");
|
||||||
return new(HttpStatusCode.InternalServerError, null, Stream.Null);
|
return new(HttpStatusCode.InternalServerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri flareSolverrUri = new (Tranga.Settings.FlareSolverrUrl);
|
Uri flareSolverrUri = new (Tranga.Settings.FlareSolverrUrl);
|
||||||
@@ -29,13 +30,6 @@ public class FlareSolverrDownloadClient : DownloadClient
|
|||||||
Path = "v1"
|
Path = "v1"
|
||||||
}.Uri;
|
}.Uri;
|
||||||
|
|
||||||
HttpClient client = new()
|
|
||||||
{
|
|
||||||
Timeout = TimeSpan.FromSeconds(10),
|
|
||||||
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
|
|
||||||
DefaultRequestHeaders = { { "User-Agent", Tranga.Settings.UserAgent } }
|
|
||||||
};
|
|
||||||
|
|
||||||
JObject requestObj = new()
|
JObject requestObj = new()
|
||||||
{
|
{
|
||||||
["cmd"] = "request.get",
|
["cmd"] = "request.get",
|
||||||
@@ -52,12 +46,12 @@ public class FlareSolverrDownloadClient : DownloadClient
|
|||||||
HttpResponseMessage? response;
|
HttpResponseMessage? response;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
response = client.Send(requestMessage);
|
response = await client.SendAsync(requestMessage);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException e)
|
catch (HttpRequestException e)
|
||||||
{
|
{
|
||||||
Log.Error(e);
|
Log.Error(e);
|
||||||
return new (HttpStatusCode.Unused, null, Stream.Null);
|
return new (HttpStatusCode.InternalServerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
@@ -74,7 +68,7 @@ public class FlareSolverrDownloadClient : DownloadClient
|
|||||||
$"{response.Version}\n" +
|
$"{response.Version}\n" +
|
||||||
$"Headers:\n\t{string.Join("\n\t", response.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
|
$"Headers:\n\t{string.Join("\n\t", response.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
|
||||||
$"{response.Content.ReadAsStringAsync().Result}");
|
$"{response.Content.ReadAsStringAsync().Result}");
|
||||||
return new (response.StatusCode, null, Stream.Null);
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
string responseString = response.Content.ReadAsStringAsync().Result;
|
string responseString = response.Content.ReadAsStringAsync().Result;
|
||||||
@@ -82,51 +76,45 @@ public class FlareSolverrDownloadClient : DownloadClient
|
|||||||
if (!IsInCorrectFormat(responseObj, out string? reason))
|
if (!IsInCorrectFormat(responseObj, out string? reason))
|
||||||
{
|
{
|
||||||
Log.Error($"Wrong format: {reason}");
|
Log.Error($"Wrong format: {reason}");
|
||||||
return new(HttpStatusCode.Unused, null, Stream.Null);
|
return new(HttpStatusCode.InternalServerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
string statusResponse = responseObj["status"]!.Value<string>()!;
|
string statusResponse = responseObj["status"]!.Value<string>()!;
|
||||||
if (statusResponse != "ok")
|
if (statusResponse != "ok")
|
||||||
{
|
{
|
||||||
Log.Debug($"Status is not ok: {statusResponse}");
|
Log.Debug($"Status is not ok: {statusResponse}");
|
||||||
return new(HttpStatusCode.Unused, null, Stream.Null);
|
return new(HttpStatusCode.InternalServerError);
|
||||||
}
|
}
|
||||||
JObject solution = (responseObj["solution"] as JObject)!;
|
JObject solution = (responseObj["solution"] as JObject)!;
|
||||||
|
|
||||||
if (!Enum.TryParse(solution["status"]!.Value<int>().ToString(), out HttpStatusCode statusCode))
|
if (!Enum.TryParse(solution["status"]!.Value<int>().ToString(), out HttpStatusCode statusCode))
|
||||||
{
|
{
|
||||||
Log.Error($"Wrong format: Cant parse status code: {solution["status"]!.Value<int>()}");
|
Log.Error($"Wrong format: Cant parse status code: {solution["status"]!.Value<int>()}");
|
||||||
return new(HttpStatusCode.Unused, null, Stream.Null);
|
return new(HttpStatusCode.InternalServerError);
|
||||||
}
|
}
|
||||||
if (statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.MultipleChoices)
|
if (statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.MultipleChoices)
|
||||||
{
|
{
|
||||||
Log.Debug($"Status is: {statusCode}");
|
Log.Debug($"Status is: {statusCode}");
|
||||||
return new(statusCode, null, Stream.Null);
|
return new (statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (solution["response"]!.Value<string>() is not { } htmlString)
|
if (solution["response"]!.Value<string>() is not { } htmlString)
|
||||||
{
|
{
|
||||||
Log.Error("Wrong format: Cant find response in solution");
|
Log.Error("Wrong format: Cant find response in solution");
|
||||||
return new(HttpStatusCode.Unused, null, Stream.Null);
|
return new(HttpStatusCode.InternalServerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsJson(htmlString, out HtmlDocument document, out string? json))
|
if (IsJson(htmlString, out string? json))
|
||||||
{
|
{
|
||||||
MemoryStream ms = new();
|
return new(statusCode) { Content = new StringContent(json) };
|
||||||
ms.Write(Encoding.UTF8.GetBytes(json));
|
|
||||||
ms.Position = 0;
|
|
||||||
return new(statusCode, document, ms);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
MemoryStream ms = new();
|
return new(statusCode) { Content = new StringContent(htmlString) };
|
||||||
ms.Write(Encoding.UTF8.GetBytes(htmlString));
|
|
||||||
ms.Position = 0;
|
|
||||||
return new(statusCode, document, ms);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsInCorrectFormat(JObject responseObj, [NotNullWhen(false)]out string? reason)
|
private static bool IsInCorrectFormat(JObject responseObj, [NotNullWhen(false)]out string? reason)
|
||||||
{
|
{
|
||||||
reason = null;
|
reason = null;
|
||||||
if (!responseObj.ContainsKey("status"))
|
if (!responseObj.ContainsKey("status"))
|
||||||
@@ -157,10 +145,10 @@ public class FlareSolverrDownloadClient : DownloadClient
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsJson(string htmlString, out HtmlDocument document, [NotNullWhen(true)]out string? jsonString)
|
private static bool IsJson(string htmlString, [NotNullWhen(true)]out string? jsonString)
|
||||||
{
|
{
|
||||||
jsonString = null;
|
jsonString = null;
|
||||||
document = new();
|
HtmlDocument document = new();
|
||||||
document.LoadHtml(htmlString);
|
document.LoadHtml(htmlString);
|
||||||
|
|
||||||
HtmlNode pre = document.DocumentNode.SelectSingleNode("//pre");
|
HtmlNode pre = document.DocumentNode.SelectSingleNode("//pre");
|
||||||
|
@@ -1,47 +1,41 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using HtmlAgilityPack;
|
using log4net;
|
||||||
|
|
||||||
namespace API.MangaDownloadClients;
|
namespace API.MangaDownloadClients;
|
||||||
|
|
||||||
internal class HttpDownloadClient : DownloadClient
|
internal class HttpDownloadClient : IDownloadClient
|
||||||
{
|
{
|
||||||
private static readonly FlareSolverrDownloadClient FlareSolverrDownloadClient = new();
|
private static readonly HttpClient Client = new(handler: Tranga.RateLimitHandler)
|
||||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
{
|
||||||
|
Timeout = TimeSpan.FromSeconds(10),
|
||||||
|
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
|
||||||
|
DefaultRequestHeaders = { { "User-Agent", Tranga.Settings.UserAgent } }
|
||||||
|
};
|
||||||
|
private static readonly FlareSolverrDownloadClient FlareSolverrDownloadClient = new(Client);
|
||||||
|
private ILog Log { get; } = LogManager.GetLogger(typeof(HttpDownloadClient));
|
||||||
|
|
||||||
|
public async Task<HttpResponseMessage> MakeRequest(string url, RequestType requestType, string? referrer = null)
|
||||||
{
|
{
|
||||||
Log.Debug($"Using {typeof(HttpDownloadClient).FullName} for {url}");
|
Log.Debug($"Using {typeof(HttpDownloadClient).FullName} for {url}");
|
||||||
if (clickButton is not null)
|
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
||||||
Log.Warn("Client can not click button");
|
|
||||||
HttpClient client = new();
|
|
||||||
client.Timeout = TimeSpan.FromSeconds(10);
|
|
||||||
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
|
|
||||||
client.DefaultRequestHeaders.Add("User-Agent", Tranga.Settings.UserAgent);
|
|
||||||
HttpResponseMessage? response;
|
|
||||||
Uri uri = new(url);
|
|
||||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, uri);
|
|
||||||
if (referrer is not null)
|
if (referrer is not null)
|
||||||
requestMessage.Headers.Referrer = new (referrer);
|
requestMessage.Headers.Referrer = new (referrer);
|
||||||
Log.Debug($"Requesting {url}");
|
Log.Debug($"Requesting {url}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
response = client.Send(requestMessage);
|
HttpResponseMessage response = await Client.SendAsync(requestMessage);
|
||||||
}
|
Log.Debug($"Request {url} returned {(int)response.StatusCode} {response.StatusCode}");
|
||||||
catch (HttpRequestException e)
|
if(response.IsSuccessStatusCode)
|
||||||
{
|
return response;
|
||||||
Log.Error(e);
|
|
||||||
return new (HttpStatusCode.Unused, null, Stream.Null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
Log.Debug($"Request returned status code {(int)response.StatusCode} {response.StatusCode}");
|
|
||||||
if (response.Headers.Server.Any(s =>
|
if (response.Headers.Server.Any(s =>
|
||||||
(s.Product?.Name ?? "").Contains("cloudflare", StringComparison.InvariantCultureIgnoreCase)))
|
(s.Product?.Name ?? "").Contains("cloudflare", StringComparison.InvariantCultureIgnoreCase)))
|
||||||
{
|
{
|
||||||
Log.Debug("Retrying with FlareSolverr!");
|
Log.Debug("Retrying with FlareSolverr!");
|
||||||
return FlareSolverrDownloadClient.MakeRequestInternal(url, referrer, clickButton);
|
return await FlareSolverrDownloadClient.MakeRequest(url, requestType, referrer);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
Log.Debug($"Request returned status code {(int)response.StatusCode} {response.StatusCode}:\n" +
|
Log.Debug($"Request returned status code {(int)response.StatusCode} {response.StatusCode}:\n" +
|
||||||
$"=====\n" +
|
$"=====\n" +
|
||||||
$"Request:\n" +
|
$"Request:\n" +
|
||||||
@@ -54,36 +48,12 @@ internal class HttpDownloadClient : DownloadClient
|
|||||||
$"{response.Version}\n" +
|
$"{response.Version}\n" +
|
||||||
$"Headers:\n\t{string.Join("\n\t", response.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
|
$"Headers:\n\t{string.Join("\n\t", response.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
|
||||||
$"{response.Content.ReadAsStringAsync().Result}");
|
$"{response.Content.ReadAsStringAsync().Result}");
|
||||||
|
return new(HttpStatusCode.InternalServerError);
|
||||||
}
|
}
|
||||||
}
|
catch (HttpRequestException e)
|
||||||
|
|
||||||
Stream stream;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
stream = response.Content.ReadAsStream();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
{
|
||||||
Log.Error(e);
|
Log.Error(e);
|
||||||
return new (HttpStatusCode.Unused, null, Stream.Null);
|
return new(HttpStatusCode.InternalServerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
HtmlDocument? document = null;
|
|
||||||
|
|
||||||
if (response.Content.Headers.ContentType?.MediaType == "text/html")
|
|
||||||
{
|
|
||||||
StreamReader reader = new (stream);
|
|
||||||
document = new ();
|
|
||||||
document.LoadHtml(reader.ReadToEnd());
|
|
||||||
stream.Position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request has been redirected to another page. For example, it redirects directly to the results when there is only 1 result
|
|
||||||
if (response.RequestMessage is not null && response.RequestMessage.RequestUri is not null && response.RequestMessage.RequestUri != uri)
|
|
||||||
{
|
|
||||||
return new (response.StatusCode, document, stream, true, response.RequestMessage.RequestUri.AbsoluteUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new (response.StatusCode, document, stream);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
31
API/MangaDownloadClients/RateLimitHandler.cs
Normal file
31
API/MangaDownloadClients/RateLimitHandler.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Threading.RateLimiting;
|
||||||
|
using log4net;
|
||||||
|
|
||||||
|
namespace API.MangaDownloadClients;
|
||||||
|
|
||||||
|
public class RateLimitHandler() : DelegatingHandler(new HttpClientHandler())
|
||||||
|
{
|
||||||
|
private ILog Log { get; init; } = LogManager.GetLogger(typeof(RateLimitHandler));
|
||||||
|
|
||||||
|
private readonly RateLimiter _limiter = new SlidingWindowRateLimiter(new ()
|
||||||
|
{
|
||||||
|
AutoReplenishment = true,
|
||||||
|
PermitLimit = 240,
|
||||||
|
QueueLimit = 120,
|
||||||
|
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||||
|
SegmentsPerWindow = 60,
|
||||||
|
Window = TimeSpan.FromSeconds(60)
|
||||||
|
});
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Log.Debug($"Requesting lease {request.RequestUri}");
|
||||||
|
using RateLimitLease lease = await _limiter.AcquireAsync(permitCount: 1, cancellationToken);
|
||||||
|
Log.Debug($"lease {lease.IsAcquired} {request.RequestUri}");
|
||||||
|
|
||||||
|
return lease.IsAcquired
|
||||||
|
? await base.SendAsync(request, cancellationToken)
|
||||||
|
: new (HttpStatusCode.TooManyRequests);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,33 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using HtmlAgilityPack;
|
|
||||||
|
|
||||||
namespace API.MangaDownloadClients;
|
|
||||||
|
|
||||||
public struct RequestResult
|
|
||||||
{
|
|
||||||
public HttpStatusCode statusCode { get; }
|
|
||||||
public Stream result { get; }
|
|
||||||
public bool hasBeenRedirected { get; }
|
|
||||||
public string? redirectedToUrl { get; }
|
|
||||||
public HtmlDocument? htmlDocument { get; }
|
|
||||||
|
|
||||||
public RequestResult(HttpStatusCode statusCode, HtmlDocument? htmlDocument, Stream result)
|
|
||||||
{
|
|
||||||
this.statusCode = statusCode;
|
|
||||||
this.htmlDocument = htmlDocument;
|
|
||||||
this.result = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RequestResult(HttpStatusCode statusCode, HtmlDocument? htmlDocument, Stream result, bool hasBeenRedirected, string redirectedTo)
|
|
||||||
: this(statusCode, htmlDocument, result)
|
|
||||||
{
|
|
||||||
this.hasBeenRedirected = hasBeenRedirected;
|
|
||||||
redirectedToUrl = redirectedTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return
|
|
||||||
$"{(int)statusCode} {statusCode.ToString()} {(hasBeenRedirected ? "Redirected: " : "")} {redirectedToUrl}";
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using API.MangaConnectors;
|
using API.MangaConnectors;
|
||||||
|
using API.MangaDownloadClients;
|
||||||
using API.Schema.LibraryContext;
|
using API.Schema.LibraryContext;
|
||||||
using API.Schema.MangaContext;
|
using API.Schema.MangaContext;
|
||||||
using API.Schema.MangaContext.MetadataFetchers;
|
using API.Schema.MangaContext.MetadataFetchers;
|
||||||
@@ -35,6 +36,8 @@ public static class Tranga
|
|||||||
internal static readonly CleanupMangaconnectorIdsWithoutConnector CleanupMangaconnectorIdsWithoutConnector = new();
|
internal static readonly CleanupMangaconnectorIdsWithoutConnector CleanupMangaconnectorIdsWithoutConnector = new();
|
||||||
// ReSharper restore MemberCanBePrivate.Global
|
// ReSharper restore MemberCanBePrivate.Global
|
||||||
|
|
||||||
|
internal static readonly RateLimitHandler RateLimitHandler = new();
|
||||||
|
|
||||||
internal static void StartupTasks()
|
internal static void StartupTasks()
|
||||||
{
|
{
|
||||||
AddWorker(SendNotificationsWorker);
|
AddWorker(SendNotificationsWorker);
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using API.MangaDownloadClients;
|
|
||||||
using API.Workers;
|
using API.Workers;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
@@ -38,17 +37,6 @@ public struct TrangaSettings()
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string ChapterNamingScheme { get; set; } = "%M - ?V(Vol.%V )Ch.%C?T( - %T)";
|
public string ChapterNamingScheme { get; set; } = "%M - ?V(Vol.%V )Ch.%C?T( - %T)";
|
||||||
public int WorkCycleTimeoutMs { get; set; } = 20000;
|
public int WorkCycleTimeoutMs { get; set; } = 20000;
|
||||||
[JsonIgnore]
|
|
||||||
internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
|
|
||||||
{
|
|
||||||
{RequestType.MangaInfo, 360},
|
|
||||||
{RequestType.MangaDexFeed, 360},
|
|
||||||
{RequestType.MangaDexImage, 60},
|
|
||||||
{RequestType.MangaImage, 240},
|
|
||||||
{RequestType.MangaCover, 60},
|
|
||||||
{RequestType.Default, 360}
|
|
||||||
};
|
|
||||||
public Dictionary<RequestType, int> RequestLimits { get; set; } = DefaultRequestLimits;
|
|
||||||
|
|
||||||
public string DownloadLanguage { get; set; } = "en";
|
public string DownloadLanguage { get; set; } = "en";
|
||||||
|
|
||||||
@@ -78,18 +66,6 @@ public struct TrangaSettings()
|
|||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetRequestLimit(RequestType type, int value)
|
|
||||||
{
|
|
||||||
this.RequestLimits[type] = value;
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResetRequestLimits()
|
|
||||||
{
|
|
||||||
this.RequestLimits = DefaultRequestLimits;
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateImageCompression(int value)
|
public void UpdateImageCompression(int value)
|
||||||
{
|
{
|
||||||
this.ImageCompression = value;
|
this.ImageCompression = value;
|
||||||
|
@@ -286,14 +286,14 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId<Chapter> c
|
|||||||
private Stream? DownloadImage(string imageUrl)
|
private Stream? DownloadImage(string imageUrl)
|
||||||
{
|
{
|
||||||
HttpDownloadClient downloadClient = new();
|
HttpDownloadClient downloadClient = new();
|
||||||
RequestResult requestResult = downloadClient.MakeRequest(imageUrl, RequestType.MangaImage);
|
HttpResponseMessage requestResult = downloadClient.MakeRequest(imageUrl, RequestType.MangaImage).Result;
|
||||||
|
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
if ((int)requestResult.StatusCode < 200 || (int)requestResult.StatusCode >= 300)
|
||||||
return null;
|
return null;
|
||||||
if (requestResult.result == Stream.Null)
|
if (requestResult.Content.ReadAsStream() == Stream.Null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return ProcessImage(requestResult.result, out Stream processedImage) ? processedImage : null;
|
return ProcessImage(requestResult.Content.ReadAsStream(), out Stream processedImage) ? processedImage : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => $"{base.ToString()} {_mangaConnectorIdId}";
|
public override string ToString() => $"{base.ToString()} {_mangaConnectorIdId}";
|
||||||
|
@@ -2764,51 +2764,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v2/Settings/RequestLimits": {
|
"/v2/Settings/RequestLimits": {
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"Settings"
|
|
||||||
],
|
|
||||||
"summary": "Get all Request-Limits",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"Default": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"MangaDexFeed": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"MangaImage": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"MangaCover": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"MangaDexImage": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"MangaInfo": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"patch": {
|
"patch": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Settings"
|
"Settings"
|
||||||
@@ -2820,145 +2775,6 @@
|
|||||||
"description": "Not Implemented"
|
"description": "Not Implemented"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"Settings"
|
|
||||||
],
|
|
||||||
"summary": "Reset Request-Limit",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"text/plain; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/json; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"text/json; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/v2/Settings/RequestLimits/{RequestType}": {
|
|
||||||
"patch": {
|
|
||||||
"tags": [
|
|
||||||
"Settings"
|
|
||||||
],
|
|
||||||
"summary": "Updates a Request-Limit value",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "RequestType",
|
|
||||||
"in": "path",
|
|
||||||
"description": "Type of Request",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/RequestType"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"description": "New limit in Requests/Minute",
|
|
||||||
"content": {
|
|
||||||
"application/json-patch+json; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/json; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"text/json; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/*+json; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": ""
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Limit needs to be greater than 0",
|
|
||||||
"content": {
|
|
||||||
"text/plain; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/json; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"text/json; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"Settings"
|
|
||||||
],
|
|
||||||
"summary": "Reset Request-Limit",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "RequestType",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/RequestType"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"text/plain; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"application/json; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"text/json; x-version=2.0": {
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v2/Settings/ImageCompressionLevel": {
|
"/v2/Settings/ImageCompressionLevel": {
|
||||||
@@ -4313,17 +4129,6 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": { }
|
"additionalProperties": { }
|
||||||
},
|
},
|
||||||
"RequestType": {
|
|
||||||
"enum": [
|
|
||||||
"Default",
|
|
||||||
"MangaDexFeed",
|
|
||||||
"MangaImage",
|
|
||||||
"MangaCover",
|
|
||||||
"MangaDexImage",
|
|
||||||
"MangaInfo"
|
|
||||||
],
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"TrangaSettings": {
|
"TrangaSettings": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -4356,37 +4161,6 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32"
|
||||||
},
|
},
|
||||||
"requestLimits": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"Default": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"MangaDexFeed": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"MangaImage": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"MangaCover": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"MangaDexImage": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"MangaInfo": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false,
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"downloadLanguage": {
|
"downloadLanguage": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
@@ -22,7 +22,7 @@ services:
|
|||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "5"
|
max-file: "5"
|
||||||
tranga-pg:
|
tranga-pg:
|
||||||
image: postgres:latest
|
image: postgres:17
|
||||||
container_name: tranga-pg
|
container_name: tranga-pg
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
Reference in New Issue
Block a user