mirror of
https://github.com/C9Glax/tranga.git
synced 2025-07-07 11:21:35 +02:00
Compare commits
44 Commits
9659f2a68a
...
postgres-S
Author | SHA1 | Date | |
---|---|---|---|
e063cf1fd9 | |||
8170e1d762 | |||
254383b006 | |||
df431e533a | |||
9a4cc0cbaf | |||
861cf7e166 | |||
7e34b3b91e | |||
29d36484f9 | |||
2c6e8e4d16 | |||
fab2886684 | |||
d9ccf71b21 | |||
f36f34f212 | |||
ff10432c79 | |||
776e1e4890 | |||
db0643fa19 | |||
3eeb563ca1 | |||
7a88b1f7ee | |||
b5411e9c6c | |||
07b260dea6 | |||
71ad32de31 | |||
ecd2c2722f | |||
ff1e467ada | |||
24f68b4a8e | |||
e51e90aabc | |||
dc2c27f4bd | |||
406d8eef51 | |||
1fba599c79 | |||
a668a16035 | |||
f89b8e1977 | |||
11290062c0 | |||
f46910fac6 | |||
f974c5ddd1 | |||
a01963a125 | |||
8a877ee465 | |||
c370e656f1 | |||
58ed976737 | |||
1b6af73a0c | |||
70fe23857b | |||
0027af2d36 | |||
1a8f70f501 | |||
aa67c11050 | |||
7b38d0aa2b | |||
64e31fad54 | |||
49a70e2341 |
@ -5,6 +5,7 @@ using API.Schema.Jobs;
|
|||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using log4net;
|
using log4net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
@ -326,6 +327,7 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
|
|||||||
/// Starts the Job with the requested ID
|
/// Starts the Job with the requested ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="JobId">Job-ID</param>
|
/// <param name="JobId">Job-ID</param>
|
||||||
|
/// <param name="startDependencies">Start Jobs necessary for execution</param>
|
||||||
/// <response code="202">Job started</response>
|
/// <response code="202">Job started</response>
|
||||||
/// <response code="404">Job with ID not found</response>
|
/// <response code="404">Job with ID not found</response>
|
||||||
/// <response code="409">Job was already running</response>
|
/// <response code="409">Job was already running</response>
|
||||||
@ -335,16 +337,22 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
|
|||||||
[ProducesResponseType(Status404NotFound)]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType(Status409Conflict)]
|
[ProducesResponseType(Status409Conflict)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
public IActionResult StartJob(string JobId)
|
public IActionResult StartJob(string JobId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]bool startDependencies = false)
|
||||||
{
|
{
|
||||||
Job? ret = context.Jobs.Find(JobId);
|
Job? ret = context.Jobs.Find(JobId);
|
||||||
if (ret is null)
|
if (ret is null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
List<Job> dependencies = startDependencies ? ret.GetDependenciesAndSelf() : [ret];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (ret.state >= JobState.Running && ret.state < JobState.Completed)
|
if(dependencies.Any(d => d.state >= JobState.Running && d.state < JobState.Completed))
|
||||||
return new ConflictResult();
|
return new ConflictResult();
|
||||||
ret.LastExecution = DateTime.UnixEpoch;
|
dependencies.ForEach(d =>
|
||||||
|
{
|
||||||
|
d.LastExecution = DateTime.UnixEpoch;
|
||||||
|
d.state = JobState.CompletedWaiting;
|
||||||
|
});
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
return Accepted();
|
return Accepted();
|
||||||
}
|
}
|
||||||
@ -366,4 +374,25 @@ public class JobController(PgsqlContext context, ILog Log) : Controller
|
|||||||
{
|
{
|
||||||
return StatusCode(Status501NotImplemented);
|
return StatusCode(Status501NotImplemented);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes failed and completed Jobs (that are not recurring)
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="202">Job started</response>
|
||||||
|
/// <response code="500">Error during Database Operation</response>
|
||||||
|
[HttpPost("Cleanup")]
|
||||||
|
public IActionResult CleanupJobs()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Failed || j.state == JobState.Completed));
|
||||||
|
context.SaveChanges();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(e);
|
||||||
|
return StatusCode(500, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ using API.Schema.Jobs;
|
|||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using log4net;
|
using log4net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
@ -139,7 +140,9 @@ public class MangaController(PgsqlContext context, ILog Log) : Controller
|
|||||||
|
|
||||||
using MemoryStream ms = new();
|
using MemoryStream ms = new();
|
||||||
image.Save(ms, new JpegEncoder(){Quality = 100});
|
image.Save(ms, new JpegEncoder(){Quality = 100});
|
||||||
return File(ms.GetBuffer(), "image/jpeg");
|
DateTime lastModified = new FileInfo(m.CoverFileNameInCache).LastWriteTime;
|
||||||
|
HttpContext.Response.Headers.CacheControl = "public";
|
||||||
|
return File(ms.GetBuffer(), "image/jpeg", new DateTimeOffset(lastModified), EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -78,12 +78,11 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
|
|||||||
/// <param name="url">Manga-Page URL</param>
|
/// <param name="url">Manga-Page URL</param>
|
||||||
/// <response code="200"></response>
|
/// <response code="200"></response>
|
||||||
/// <response code="300">Multiple connectors found for URL</response>
|
/// <response code="300">Multiple connectors found for URL</response>
|
||||||
/// <response code="400">No Manga at URL</response>
|
/// <response code="404">Manga not found</response>
|
||||||
/// <response code="404">No connector found for URL</response>
|
|
||||||
/// <response code="500">Error during Database Operation</response>
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPost("Url")]
|
[HttpPost("Url")]
|
||||||
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status400BadRequest)]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
public IActionResult GetMangaFromUrl([FromBody]string url)
|
public IActionResult GetMangaFromUrl([FromBody]string url)
|
||||||
{
|
{
|
||||||
@ -91,7 +90,7 @@ public class SearchController(PgsqlContext context, ILog Log) : Controller
|
|||||||
return StatusCode(Status500InternalServerError, "Could not find Global Connector.");
|
return StatusCode(Status500InternalServerError, "Could not find Global Connector.");
|
||||||
|
|
||||||
if(connector.GetMangaFromUrl(url) is not { } manga)
|
if(connector.GetMangaFromUrl(url) is not { } manga)
|
||||||
return BadRequest();
|
return NotFound();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(AddMangaToContext(manga) is { } add)
|
if(AddMangaToContext(manga) is { } add)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
using API.MangaDownloadClients;
|
using System.Net.Http.Headers;
|
||||||
|
using API.MangaDownloadClients;
|
||||||
using API.Schema;
|
using API.Schema;
|
||||||
using API.Schema.Contexts;
|
using API.Schema.Contexts;
|
||||||
using API.Schema.Jobs;
|
using API.Schema.Jobs;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using log4net;
|
using log4net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
|
|
||||||
@ -291,4 +293,45 @@ public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
|||||||
return StatusCode(500, e);
|
return StatusCode(500, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the FlareSolverr-URL
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="flareSolverrUrl">URL of FlareSolverr-Instance</param>
|
||||||
|
/// <response code="200"></response>
|
||||||
|
[HttpPost("FlareSolverr/Url")]
|
||||||
|
[ProducesResponseType(Status200OK)]
|
||||||
|
public IActionResult SetFlareSolverrUrl([FromBody]string flareSolverrUrl)
|
||||||
|
{
|
||||||
|
TrangaSettings.UpdateFlareSolverrUrl(flareSolverrUrl);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the FlareSolverr-URL (HttpClient does not use FlareSolverr anymore)
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200"></response>
|
||||||
|
[HttpDelete("FlareSolverr/Url")]
|
||||||
|
[ProducesResponseType(Status200OK)]
|
||||||
|
public IActionResult ClearFlareSolverrUrl()
|
||||||
|
{
|
||||||
|
TrangaSettings.UpdateFlareSolverrUrl(string.Empty);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test FlareSolverr
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">FlareSolverr is working!</response>
|
||||||
|
/// <response code="500">FlareSolverr is not working</response>
|
||||||
|
[HttpPost("FlareSolverr/Test")]
|
||||||
|
[ProducesResponseType(Status200OK)]
|
||||||
|
[ProducesResponseType(Status500InternalServerError)]
|
||||||
|
public IActionResult TestFlareSolverrReachable()
|
||||||
|
{
|
||||||
|
const string knownProtectedUrl = "https://prowlarr.servarr.com/v1/ping";
|
||||||
|
FlareSolverrDownloadClient client = new();
|
||||||
|
RequestResult result = client.MakeRequestInternal(knownProtectedUrl);
|
||||||
|
return (int)result.statusCode >= 200 && (int)result.statusCode < 300 ? Ok() : StatusCode(500, result.statusCode);
|
||||||
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ using log4net;
|
|||||||
|
|
||||||
namespace API.MangaDownloadClients;
|
namespace API.MangaDownloadClients;
|
||||||
|
|
||||||
internal abstract class DownloadClient
|
public abstract class DownloadClient
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<RequestType, DateTime> LastExecutedRateLimit = new();
|
private static readonly Dictionary<RequestType, DateTime> LastExecutedRateLimit = new();
|
||||||
protected ILog Log { get; init; }
|
protected ILog Log { get; init; }
|
||||||
|
180
API/MangaDownloadClients/FlareSolverrDownloadClient.cs
Normal file
180
API/MangaDownloadClients/FlareSolverrDownloadClient.cs
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
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<string>()!;
|
||||||
|
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<int>().ToString(), out HttpStatusCode statusCode))
|
||||||
|
{
|
||||||
|
Log.Error($"Wrong format: Cant parse status code: {solution["status"]!.Value<int>()}");
|
||||||
|
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<string>() 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
|
||||||
|
{
|
||||||
|
using JsonDocument _ = JsonDocument.Parse(pre.InnerText);
|
||||||
|
jsonString = pre.InnerText;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (JsonReaderException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,46 +5,54 @@ namespace API.MangaDownloadClients;
|
|||||||
|
|
||||||
internal class HttpDownloadClient : DownloadClient
|
internal class HttpDownloadClient : DownloadClient
|
||||||
{
|
{
|
||||||
private static readonly HttpClient Client = new()
|
|
||||||
{
|
|
||||||
Timeout = TimeSpan.FromSeconds(10)
|
|
||||||
};
|
|
||||||
|
|
||||||
public HttpDownloadClient()
|
|
||||||
{
|
|
||||||
Client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", TrangaSettings.userAgent);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||||
{
|
{
|
||||||
if (clickButton is not null)
|
if (clickButton is not null)
|
||||||
Log.Warn("Can not click button on static site.");
|
Log.Warn("Client can not click button");
|
||||||
HttpResponseMessage? response = null;
|
HttpClient client = new();
|
||||||
while (response is null)
|
client.Timeout = TimeSpan.FromSeconds(10);
|
||||||
{
|
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
|
||||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
client.DefaultRequestHeaders.Add("User-Agent", TrangaSettings.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 Uri(referrer);
|
requestMessage.Headers.Referrer = new (referrer);
|
||||||
Log.Debug($"Requesting {url}");
|
Log.Debug($"Requesting {url}");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
response = Client.Send(requestMessage);
|
response = client.Send(requestMessage);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (HttpRequestException e)
|
||||||
{
|
{
|
||||||
switch (e)
|
Log.Error(e);
|
||||||
{
|
return new (HttpStatusCode.Unused, null, Stream.Null);
|
||||||
case TaskCanceledException:
|
|
||||||
return new RequestResult(HttpStatusCode.RequestTimeout, null, Stream.Null);
|
|
||||||
case HttpRequestException:
|
|
||||||
return new RequestResult(HttpStatusCode.BadRequest, null, Stream.Null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
return new RequestResult(response.StatusCode, null, Stream.Null);
|
Log.Debug($"Request returned status code {(int)response.StatusCode} {response.StatusCode}");
|
||||||
|
if (response.Headers.Server.Any(s =>
|
||||||
|
(s.Product?.Name ?? "").Contains("cloudflare", StringComparison.InvariantCultureIgnoreCase)))
|
||||||
|
{
|
||||||
|
Log.Debug("Retrying with FlareSolverr!");
|
||||||
|
return new FlareSolverrDownloadClient().MakeRequestInternal(url, referrer, clickButton);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream stream;
|
Stream stream;
|
||||||
@ -55,7 +63,7 @@ internal class HttpDownloadClient : DownloadClient
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.Error(e);
|
Log.Error(e);
|
||||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
return new (HttpStatusCode.Unused, null, Stream.Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
HtmlDocument? document = null;
|
HtmlDocument? document = null;
|
||||||
@ -69,12 +77,11 @@ internal class HttpDownloadClient : DownloadClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Request has been redirected to another page. For example, it redirects directly to the results when there is only 1 result
|
// 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)
|
if (response.RequestMessage is not null && response.RequestMessage.RequestUri is not null && response.RequestMessage.RequestUri != uri)
|
||||||
{
|
{
|
||||||
return new RequestResult(response.StatusCode, document, stream, true,
|
return new (response.StatusCode, document, stream, true, response.RequestMessage.RequestUri.AbsoluteUri);
|
||||||
response.RequestMessage.RequestUri.AbsoluteUri);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RequestResult(response.StatusCode, document, stream);
|
return new (response.StatusCode, document, stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -187,6 +187,8 @@ public class Chapter : IComparable<Chapter>
|
|||||||
comicInfo.Add(new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))));
|
comicInfo.Add(new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))));
|
||||||
if(ParentManga.OriginalLanguage is not null)
|
if(ParentManga.OriginalLanguage is not null)
|
||||||
comicInfo.Add(new XElement("LanguageISO", ParentManga.OriginalLanguage));
|
comicInfo.Add(new XElement("LanguageISO", ParentManga.OriginalLanguage));
|
||||||
|
if(ParentManga.Description != string.Empty)
|
||||||
|
comicInfo.Add(new XElement("Summary", ParentManga.Description));
|
||||||
return comicInfo.ToString();
|
return comicInfo.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +45,11 @@ public class DownloadSingleChapterJob : Job
|
|||||||
|
|
||||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||||
{
|
{
|
||||||
|
if (Chapter.Downloaded)
|
||||||
|
{
|
||||||
|
Log.Info("Chapter was already downloaded.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
|
string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
|
||||||
if (imageUrls.Length < 1)
|
if (imageUrls.Length < 1)
|
||||||
{
|
{
|
||||||
@ -129,21 +134,39 @@ public class DownloadSingleChapterJob : Job
|
|||||||
{
|
{
|
||||||
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
|
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
|
||||||
{
|
{
|
||||||
Log.Debug($"No processing requested for image");
|
Log.Debug("No processing requested for image");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug($"Processing image: {imagePath}");
|
Log.Debug($"Processing image: {imagePath}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
using Image image = Image.Load(imagePath);
|
using Image image = Image.Load(imagePath);
|
||||||
File.Delete(imagePath);
|
if (TrangaSettings.bwImages)
|
||||||
if(TrangaSettings.bwImages)
|
|
||||||
image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor()));
|
image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor()));
|
||||||
|
File.Delete(imagePath);
|
||||||
image.SaveAsJpeg(imagePath, new JpegEncoder()
|
image.SaveAsJpeg(imagePath, new JpegEncoder()
|
||||||
{
|
{
|
||||||
Quality = TrangaSettings.compression
|
Quality = TrangaSettings.compression
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (e is UnknownImageFormatException or NotSupportedException)
|
||||||
|
{
|
||||||
|
//If the Image-Format is not processable by ImageSharp, we can't modify it.
|
||||||
|
Log.Debug($"Unable to process {imagePath}: Not supported image format");
|
||||||
|
}else if (e is InvalidImageContentException)
|
||||||
|
{
|
||||||
|
Log.Debug($"Unable to process {imagePath}: Invalid Content");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void CopyCoverFromCacheToDownloadLocation(Manga manga)
|
private void CopyCoverFromCacheToDownloadLocation(Manga manga)
|
||||||
{
|
{
|
||||||
|
@ -118,6 +118,17 @@ public abstract class Job
|
|||||||
|
|
||||||
protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);
|
protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);
|
||||||
|
|
||||||
|
public List<Job> GetDependenciesAndSelf()
|
||||||
|
{
|
||||||
|
List<Job> ret = new ();
|
||||||
|
foreach (Job job in DependsOnJobs)
|
||||||
|
{
|
||||||
|
ret.AddRange(job.GetDependenciesAndSelf());
|
||||||
|
}
|
||||||
|
ret.Add(this);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{JobId}";
|
return $"{JobId}";
|
||||||
|
@ -78,7 +78,7 @@ public class ComickIo : MangaConnector
|
|||||||
public override Chapter[] GetChapters(Manga manga, string? language = null)
|
public override Chapter[] GetChapters(Manga manga, string? language = null)
|
||||||
{
|
{
|
||||||
Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
|
Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
|
||||||
List<string> chapterHids = new();
|
List<Chapter> chapters = new();
|
||||||
int page = 1;
|
int page = 1;
|
||||||
while(page < 50)
|
while(page < 50)
|
||||||
{
|
{
|
||||||
@ -95,16 +95,13 @@ public class ComickIo : MangaConnector
|
|||||||
JToken data = JToken.Parse(sr.ReadToEnd());
|
JToken data = JToken.Parse(sr.ReadToEnd());
|
||||||
JArray? chaptersArray = data["chapters"] as JArray;
|
JArray? chaptersArray = data["chapters"] as JArray;
|
||||||
|
|
||||||
if (chaptersArray?.Count < 1)
|
if (chaptersArray is null || chaptersArray.Count < 1)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
chapterHids.AddRange(chaptersArray?.Select(token => token.Value<string>("hid")!)!);
|
chapters.AddRange(ParseChapters(manga, chaptersArray));
|
||||||
|
|
||||||
page++;
|
page++;
|
||||||
}
|
}
|
||||||
Log.Debug($"Getting chapters for {manga.Name} yielded {chapterHids.Count} hids. Requesting chapters now...");
|
|
||||||
|
|
||||||
List<Chapter> chapters = chapterHids.Select(hid => ChapterFromHid(manga, hid)).ToList();
|
|
||||||
|
|
||||||
return chapters.ToArray();
|
return chapters.ToArray();
|
||||||
}
|
}
|
||||||
@ -219,29 +216,23 @@ public class ComickIo : MangaConnector
|
|||||||
year: year, originalLanguage: originalLanguage);
|
year: year, originalLanguage: originalLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Chapter ChapterFromHid(Manga parentManga, string hid)
|
private List<Chapter> ParseChapters(Manga parentManga, JArray chaptersArray)
|
||||||
{
|
{
|
||||||
string requestUrl = $"https://api.comick.fun/chapter/{hid}";
|
List<Chapter> chapters = new ();
|
||||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
foreach (JToken chapter in chaptersArray)
|
||||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
|
||||||
{
|
{
|
||||||
Log.Error("Request failed");
|
string? chapterNum = chapter.Value<string>("chap");
|
||||||
throw new Exception("Request failed");
|
string? volumeNumStr = chapter.Value<string>("vol");
|
||||||
}
|
|
||||||
|
|
||||||
using StreamReader sr = new (result.result);
|
|
||||||
JToken data = JToken.Parse(sr.ReadToEnd());
|
|
||||||
|
|
||||||
string? canonical = data.Value<string>("canonical");
|
|
||||||
string? chapterNum = data["chapter"]?.Value<string>("chap");
|
|
||||||
string? volumeNumStr = data["chapter"]?.Value<string>("vol");
|
|
||||||
int? volumeNum = volumeNumStr is null ? null : int.Parse(volumeNumStr);
|
int? volumeNum = volumeNumStr is null ? null : int.Parse(volumeNumStr);
|
||||||
string? title = data["chapter"]?.Value<string>("title");
|
string? title = chapter.Value<string>("title");
|
||||||
|
string? hid = chapter.Value<string>("hid");
|
||||||
|
string url = $"https://comick.io/comic/{parentManga.IdOnConnectorSite}/{hid}";
|
||||||
|
|
||||||
if(chapterNum is null)
|
if(chapterNum is null || hid is null)
|
||||||
throw new Exception("chapterNum is null");
|
continue;
|
||||||
|
|
||||||
string url = $"https://comick.io{canonical}";
|
chapters.Add(new (parentManga, url, chapterNum, volumeNum, hid, title));
|
||||||
return new Chapter(parentManga, url, chapterNum, volumeNum, hid, title);
|
}
|
||||||
|
return chapters;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -226,22 +226,25 @@ public class MangaDex : MangaConnector
|
|||||||
private Manga ParseMangaFromJToken(JToken jToken)
|
private Manga ParseMangaFromJToken(JToken jToken)
|
||||||
{
|
{
|
||||||
string? id = jToken.Value<string>("id");
|
string? id = jToken.Value<string>("id");
|
||||||
|
if(id is null)
|
||||||
|
throw new Exception("jToken was not in expected format");
|
||||||
|
|
||||||
JObject? attributes = jToken["attributes"] as JObject;
|
JObject? attributes = jToken["attributes"] as JObject;
|
||||||
string? name = attributes?["title"]?.Value<string>("en") ?? attributes?["title"]?.First?.First?.Value<string>();
|
if(attributes is null)
|
||||||
string? description = attributes?["description"]?.Value<string>("en")??attributes?["description"]?.First?.First?.Value<string>();
|
throw new Exception("jToken was not in expected format");
|
||||||
string? status = attributes?["status"]?.Value<string>();
|
string? name = attributes["title"]?.Value<string>("en") ?? attributes["title"]?.First?.First?.Value<string>();
|
||||||
uint? year = attributes?["year"]?.Value<uint?>();
|
string description = attributes["description"]?.Value<string>("en")??attributes["description"]?.First?.First?.Value<string>()??"";
|
||||||
string? originalLanguage = attributes?["originalLanguage"]?.Value<string>();
|
string? status = attributes["status"]?.Value<string>();
|
||||||
JArray? altTitlesJArray = attributes?["altTitles"] as JArray;
|
uint? year = attributes["year"]?.Value<uint?>();
|
||||||
JArray? tagsJArray = attributes?["tags"] as JArray;
|
string? originalLanguage = attributes["originalLanguage"]?.Value<string>();
|
||||||
|
JArray? altTitlesJArray = attributes.TryGetValue("altTitles", out JToken? altTitlesArray) ? altTitlesArray as JArray : null;
|
||||||
|
JArray? tagsJArray = attributes.TryGetValue("tags", out JToken? tagsArray) ? tagsArray as JArray : null;
|
||||||
JArray? relationships = jToken["relationships"] as JArray;
|
JArray? relationships = jToken["relationships"] as JArray;
|
||||||
string? coverFileName =
|
if (name is null || status is null || relationships is null)
|
||||||
relationships?.FirstOrDefault(r => r["type"]?.Value<string>() == "cover_art")?["attributes"]?.Value<string>("fileName");
|
throw new Exception("jToken was not in expected format");
|
||||||
|
|
||||||
if (id is null || attributes is null || name is null || description is null || status is null ||
|
string? coverFileName = relationships.FirstOrDefault(r => r["type"]?.Value<string>() == "cover_art")?["attributes"]?.Value<string>("fileName");
|
||||||
altTitlesJArray is null || tagsJArray is null || relationships is null || coverFileName is null)
|
if(coverFileName is null)
|
||||||
throw new Exception("jToken was not in expected format");
|
throw new Exception("jToken was not in expected format");
|
||||||
|
|
||||||
List<Link> links = attributes["links"]?
|
List<Link> links = attributes["links"]?
|
||||||
@ -276,7 +279,7 @@ public class MangaDex : MangaConnector
|
|||||||
return new Link(key, url);
|
return new Link(key, url);
|
||||||
}).ToList()!;
|
}).ToList()!;
|
||||||
|
|
||||||
List<MangaAltTitle> altTitles = altTitlesJArray
|
List<MangaAltTitle> altTitles = (altTitlesJArray??[])
|
||||||
.Select(t =>
|
.Select(t =>
|
||||||
{
|
{
|
||||||
JObject? j = t as JObject;
|
JObject? j = t as JObject;
|
||||||
@ -286,7 +289,7 @@ public class MangaDex : MangaConnector
|
|||||||
return new MangaAltTitle(p.Name, p.Value.ToString());
|
return new MangaAltTitle(p.Name, p.Value.ToString());
|
||||||
}).Where(x => x is not null).ToList()!;
|
}).Where(x => x is not null).ToList()!;
|
||||||
|
|
||||||
List<MangaTag> tags = tagsJArray
|
List<MangaTag> tags = (tagsJArray??[])
|
||||||
.Where(t => t.Value<string>("type") == "tag")
|
.Where(t => t.Value<string>("type") == "tag")
|
||||||
.Select(t => t["attributes"]?["name"]?.Value<string>("en")??t["attributes"]?["name"]?.First?.First?.Value<string>())
|
.Select(t => t["attributes"]?["name"]?.Value<string>("en")??t["attributes"]?["name"]?.First?.First?.Value<string>())
|
||||||
.Select(str => str is not null ? new MangaTag(str) : null)
|
.Select(str => str is not null ? new MangaTag(str) : null)
|
||||||
|
216
API/Tranga.cs
216
API/Tranga.cs
@ -34,7 +34,9 @@ public static class Tranga
|
|||||||
|
|
||||||
internal static void RemoveStaleFiles(PgsqlContext context)
|
internal static void RemoveStaleFiles(PgsqlContext context)
|
||||||
{
|
{
|
||||||
Log.Info($"Removing stale files...");
|
Log.Info("Removing stale files...");
|
||||||
|
if (!Directory.Exists(TrangaSettings.coverImageCache))
|
||||||
|
return;
|
||||||
string[] usedFiles = context.Mangas.Select(m => m.CoverFileNameInCache).Where(s => s != null).ToArray()!;
|
string[] usedFiles = context.Mangas.Select(m => m.CoverFileNameInCache).Where(s => s != null).ToArray()!;
|
||||||
string[] extraneousFiles = new DirectoryInfo(TrangaSettings.coverImageCache).GetFiles()
|
string[] extraneousFiles = new DirectoryInfo(TrangaSettings.coverImageCache).GetFiles()
|
||||||
.Where(f => usedFiles.Contains(f.FullName) == false)
|
.Where(f => usedFiles.Contains(f.FullName) == false)
|
||||||
@ -125,39 +127,28 @@ public static class Tranga
|
|||||||
using IServiceScope scope = serviceProvider.CreateScope();
|
using IServiceScope scope = serviceProvider.CreateScope();
|
||||||
PgsqlContext cycleContext = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
PgsqlContext cycleContext = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
||||||
|
|
||||||
List<Job> runningJobs = cycleContext.Jobs.Where(j => j.state == JobState.Running).ToList();
|
//Get Running Jobs
|
||||||
|
List<Job> runningJobs = cycleContext.Jobs.GetRunningJobs();
|
||||||
|
|
||||||
DateTime filterStart = DateTime.UtcNow;
|
DateTime filterStart = DateTime.UtcNow;
|
||||||
Log.Debug("Filtering Jobs...");
|
Log.Debug("Filtering Jobs...");
|
||||||
List<MangaConnector> busyConnectors = GetBusyConnectors(runningJobs);
|
|
||||||
|
|
||||||
List<Job> waitingJobs = cycleContext.Jobs.Where(j => j.state == JobState.CompletedWaiting || j.state == JobState.FirstExecution).ToList();
|
List<Job> waitingJobs = cycleContext.Jobs.GetWaitingJobs();
|
||||||
List<Job> dueJobs = FilterDueJobs(waitingJobs);
|
List<Job> dueJobs = waitingJobs.FilterDueJobs();
|
||||||
List<Job> jobsWithoutBusyConnectors = FilterJobWithBusyConnectors(dueJobs, busyConnectors);
|
List<Job> jobsWithoutDependencies = dueJobs.FilterJobDependencies();
|
||||||
List<Job> jobsWithoutMissingDependencies = FilterJobDependencies(jobsWithoutBusyConnectors);
|
|
||||||
|
|
||||||
List<Job> jobsWithoutDownloading =
|
List<Job> jobsWithoutDownloading = jobsWithoutDependencies.FilterJobsWithoutDownloading();
|
||||||
jobsWithoutMissingDependencies
|
|
||||||
.Where(j => j.JobType != JobType.DownloadSingleChapterJob)
|
|
||||||
.DistinctBy(j => j.JobType)
|
|
||||||
.ToList();
|
|
||||||
List<Job> firstChapterPerConnector =
|
|
||||||
jobsWithoutMissingDependencies
|
|
||||||
.Where(j => j.JobType == JobType.DownloadSingleChapterJob)
|
|
||||||
.AsEnumerable()
|
|
||||||
.OrderBy(j =>
|
|
||||||
{
|
|
||||||
DownloadSingleChapterJob dscj = (DownloadSingleChapterJob)j;
|
|
||||||
return dscj.Chapter;
|
|
||||||
})
|
|
||||||
.DistinctBy(j =>
|
|
||||||
{
|
|
||||||
DownloadSingleChapterJob dscj = (DownloadSingleChapterJob)j;
|
|
||||||
return dscj.Chapter.ParentManga.MangaConnector;
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
List<Job> startJobs = jobsWithoutDownloading.Concat(firstChapterPerConnector).ToList();
|
//Match running and waiting jobs per Connector
|
||||||
|
Dictionary<string, Dictionary<JobType, List<Job>>> runningJobsPerConnector =
|
||||||
|
runningJobs.GetJobsPerJobTypeAndConnector();
|
||||||
|
Dictionary<string, Dictionary<JobType, List<Job>>> waitingJobsPerConnector =
|
||||||
|
jobsWithoutDependencies.GetJobsPerJobTypeAndConnector();
|
||||||
|
List<Job> jobsNotHeldBackByConnector =
|
||||||
|
MatchJobsRunningAndWaiting(runningJobsPerConnector, waitingJobsPerConnector);
|
||||||
|
|
||||||
|
|
||||||
|
List<Job> startJobs = jobsWithoutDownloading.Concat(jobsNotHeldBackByConnector).ToList();
|
||||||
Log.Debug($"Jobs Filtered! (took {DateTime.UtcNow.Subtract(filterStart).TotalMilliseconds}ms)");
|
Log.Debug($"Jobs Filtered! (took {DateTime.UtcNow.Subtract(filterStart).TotalMilliseconds}ms)");
|
||||||
|
|
||||||
|
|
||||||
@ -178,11 +169,19 @@ public static class Tranga
|
|||||||
while(!running)
|
while(!running)
|
||||||
Thread.Sleep(10);
|
Thread.Sleep(10);
|
||||||
}
|
}
|
||||||
Log.Debug($"Running: {runningJobs.Count()}\n" +
|
Log.Debug($"Running: {runningJobs.Count}\n" +
|
||||||
$"Waiting: {waitingJobs.Count()}\n" +
|
$"{string.Join("\n", runningJobs.Select(s => "\t- " + s))}\n" +
|
||||||
$"\tof which Due: {dueJobs.Count()}\n" +
|
$"Waiting: {waitingJobs.Count} Due: {dueJobs.Count}\n" +
|
||||||
$"\t\tof which can be started: {jobsWithoutMissingDependencies.Count()}\n" +
|
$"{string.Join("\n", dueJobs.Select(s => "\t- " + s))}\n" +
|
||||||
$"\t\t\tof which started: {startJobs.Count()}");
|
$"of which {jobsWithoutDependencies.Count} without missing dependencies, of which\n" +
|
||||||
|
$"\t{jobsWithoutDownloading.Count} without downloading\n" +
|
||||||
|
$"\t{jobsNotHeldBackByConnector.Count} not held back by Connector\n" +
|
||||||
|
$"{startJobs.Count} were started:\n" +
|
||||||
|
$"{string.Join("\n", startJobs.Select(s => "\t- " + s))}");
|
||||||
|
|
||||||
|
if (Log.IsDebugEnabled && dueJobs.Count < 1)
|
||||||
|
if(waitingJobs.MinBy(j => j.NextExecution) is { } nextJob)
|
||||||
|
Log.Debug($"Next job in {nextJob.NextExecution.Subtract(DateTime.UtcNow)} (at {nextJob.NextExecution}): {nextJob.JobId}");
|
||||||
|
|
||||||
(Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
|
(Thread, Job)[] removeFromThreadsList = RunningJobs.Where(t => !t.Key.IsAlive)
|
||||||
.Select(t => (t.Key, t.Value)).ToArray();
|
.Select(t => (t.Key, t.Value)).ToArray();
|
||||||
@ -200,51 +199,140 @@ public static class Tranga
|
|||||||
{
|
{
|
||||||
Log.Error("Failed saving Job changes.", e);
|
Log.Error("Failed saving Job changes.", e);
|
||||||
}
|
}
|
||||||
Log.Debug($"Job-Cycle over! (took {DateTime.UtcNow.Subtract(cycleStart).TotalMilliseconds}ms)");
|
Log.Debug($"Job-Cycle over! (took {DateTime.UtcNow.Subtract(cycleStart).TotalMilliseconds}ms");
|
||||||
Thread.Sleep(TrangaSettings.startNewJobTimeoutMs);
|
Thread.Sleep(TrangaSettings.startNewJobTimeoutMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<MangaConnector> GetBusyConnectors(List<Job> runningJobs)
|
private static List<Job> GetRunningJobs(this IQueryable<Job> jobs)
|
||||||
{
|
{
|
||||||
HashSet<MangaConnector> busyConnectors = new();
|
DateTime start = DateTime.UtcNow;
|
||||||
foreach (Job runningJob in runningJobs)
|
List<Job> ret = jobs.Where(j => j.state == JobState.Running).ToList();
|
||||||
{
|
DateTime end = DateTime.UtcNow;
|
||||||
if(GetJobConnector(runningJob) is { } mangaConnector)
|
Log.Debug($"Getting running Jobs took {end.Subtract(start).TotalMilliseconds}ms");
|
||||||
busyConnectors.Add(mangaConnector);
|
return ret;
|
||||||
}
|
|
||||||
return busyConnectors.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Job> FilterDueJobs(List<Job> jobs) =>
|
private static List<Job> GetWaitingJobs(this IQueryable<Job> jobs)
|
||||||
jobs.ToList()
|
|
||||||
.Where(j => j.NextExecution < DateTime.UtcNow)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
private static List<Job> FilterJobDependencies(List<Job> jobs) =>
|
|
||||||
jobs
|
|
||||||
.Where(job => job.DependsOnJobs.All(j => j.IsCompleted))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
private static List<Job> FilterJobWithBusyConnectors(List<Job> jobs, List<MangaConnector> busyConnectors) =>
|
|
||||||
jobs.Where(j =>
|
|
||||||
{
|
{
|
||||||
//Filter jobs with busy connectors
|
DateTime start = DateTime.UtcNow;
|
||||||
if (GetJobConnector(j) is { } mangaConnector)
|
List<Job> ret = jobs.Where(j => j.state == JobState.CompletedWaiting || j.state == JobState.FirstExecution).ToList();
|
||||||
return busyConnectors.Contains(mangaConnector) == false;
|
DateTime end = DateTime.UtcNow;
|
||||||
return true;
|
Log.Debug($"Getting waiting Jobs took {end.Subtract(start).TotalMilliseconds}ms");
|
||||||
}).ToList();
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
private static MangaConnector? GetJobConnector(Job job)
|
|
||||||
|
private static List<Job> FilterDueJobs(this List<Job> jobs)
|
||||||
|
{
|
||||||
|
DateTime start = DateTime.UtcNow;
|
||||||
|
List<Job> ret = jobs.Where(j => j.NextExecution < DateTime.UtcNow).ToList();
|
||||||
|
DateTime end = DateTime.UtcNow;
|
||||||
|
Log.Debug($"Filtering Due Jobs took {end.Subtract(start).TotalMilliseconds}ms");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static List<Job> FilterJobDependencies(this List<Job> jobs)
|
||||||
|
{
|
||||||
|
DateTime start = DateTime.UtcNow;
|
||||||
|
List<Job> ret = jobs.Where(job => job.DependsOnJobs.All(j => j.IsCompleted)).ToList();
|
||||||
|
DateTime end = DateTime.UtcNow;
|
||||||
|
Log.Debug($"Filtering Dependencies took {end.Subtract(start).TotalMilliseconds}ms");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static List<Job> FilterJobsWithoutDownloading(this List<Job> jobs)
|
||||||
|
{
|
||||||
|
JobType[] types = [JobType.MoveFileOrFolderJob, JobType.MoveMangaLibraryJob, JobType.UpdateChaptersDownloadedJob];
|
||||||
|
DateTime start = DateTime.UtcNow;
|
||||||
|
List<Job> ret = jobs.Where(j => types.Contains(j.JobType)).ToList();
|
||||||
|
DateTime end = DateTime.UtcNow;
|
||||||
|
Log.Debug($"Filtering Jobs without Download took {end.Subtract(start).TotalMilliseconds}ms");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Dictionary<string, Dictionary<JobType, List<Job>>> GetJobsPerJobTypeAndConnector(this List<Job> jobs)
|
||||||
|
{
|
||||||
|
DateTime start = DateTime.UtcNow;
|
||||||
|
Dictionary<string, Dictionary<JobType, List<Job>>> ret = new();
|
||||||
|
foreach (Job job in jobs)
|
||||||
|
{
|
||||||
|
if(GetJobConnectorName(job) is not { } connector)
|
||||||
|
continue;
|
||||||
|
if (!ret.ContainsKey(connector))
|
||||||
|
ret.Add(connector, new());
|
||||||
|
if (!ret[connector].ContainsKey(job.JobType))
|
||||||
|
ret[connector].Add(job.JobType, new());
|
||||||
|
ret[connector][job.JobType].Add(job);
|
||||||
|
}
|
||||||
|
DateTime end = DateTime.UtcNow;
|
||||||
|
Log.Debug($"Fetching connector per Job for jobs took {end.Subtract(start).TotalMilliseconds}ms");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Job> MatchJobsRunningAndWaiting(Dictionary<string, Dictionary<JobType, List<Job>>> running,
|
||||||
|
Dictionary<string, Dictionary<JobType, List<Job>>> waiting)
|
||||||
|
{
|
||||||
|
Log.Debug($"Matching {running.Count} running Jobs to {waiting.Count} waiting Jobs. Busy Connectors: {string.Join(", ", running.Select(r => r.Key))}");
|
||||||
|
DateTime start = DateTime.UtcNow;
|
||||||
|
List<Job> ret = new();
|
||||||
|
//Foreach MangaConnector
|
||||||
|
foreach ((string connector, Dictionary<JobType, List<Job>> jobTypeJobsWaiting) in waiting)
|
||||||
|
{
|
||||||
|
//Check if MangaConnector has a Job running
|
||||||
|
if (running.TryGetValue(connector, out Dictionary<JobType, List<Job>>? jobTypeJobsRunning))
|
||||||
|
{
|
||||||
|
//MangaConnector has running Jobs
|
||||||
|
//Match per JobType (MangaConnector can have 1 Job per Type running at the same time)
|
||||||
|
foreach ((JobType jobType, List<Job> jobsWaiting) in jobTypeJobsWaiting)
|
||||||
|
{
|
||||||
|
if(jobTypeJobsRunning.ContainsKey(jobType))
|
||||||
|
//Already a job of Type running on MangaConnector
|
||||||
|
continue;
|
||||||
|
if (jobType is not JobType.DownloadSingleChapterJob)
|
||||||
|
//If it is not a DownloadSingleChapterJob, just add the first
|
||||||
|
ret.Add(jobsWaiting.First());
|
||||||
|
else
|
||||||
|
//Add the Job with the lowest Chapternumber
|
||||||
|
ret.Add(jobsWaiting.OrderBy(j => ((DownloadSingleChapterJob)j).Chapter).First());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//MangaConnector has no running Jobs
|
||||||
|
foreach ((JobType jobType, List<Job> jobsWaiting) in jobTypeJobsWaiting)
|
||||||
|
{
|
||||||
|
if(ret.Any(j => j.JobType == jobType))
|
||||||
|
//Already a job of type to be started
|
||||||
|
continue;
|
||||||
|
if (jobType is not JobType.DownloadSingleChapterJob)
|
||||||
|
//If it is not a DownloadSingleChapterJob, just add the first
|
||||||
|
ret.Add(jobsWaiting.First());
|
||||||
|
else
|
||||||
|
//Add the Job with the lowest Chapternumber
|
||||||
|
ret.Add(jobsWaiting.OrderBy(j => ((DownloadSingleChapterJob)j).Chapter).First());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DateTime end = DateTime.UtcNow;
|
||||||
|
Log.Debug($"Getting eligible jobs (not held back by Connector) took {end.Subtract(start).TotalMilliseconds}ms");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static string? GetJobConnectorName(Job job)
|
||||||
{
|
{
|
||||||
if (job is DownloadAvailableChaptersJob dacj)
|
if (job is DownloadAvailableChaptersJob dacj)
|
||||||
return dacj.Manga.MangaConnector;
|
return dacj.Manga.MangaConnectorName;
|
||||||
if (job is DownloadMangaCoverJob dmcj)
|
if (job is DownloadMangaCoverJob dmcj)
|
||||||
return dmcj.Manga.MangaConnector;
|
return dmcj.Manga.MangaConnectorName;
|
||||||
if (job is DownloadSingleChapterJob dscj)
|
if (job is DownloadSingleChapterJob dscj)
|
||||||
return dscj.Chapter.ParentManga.MangaConnector;
|
return dscj.Chapter.ParentManga.MangaConnectorName;
|
||||||
if (job is RetrieveChaptersJob rcj)
|
if (job is RetrieveChaptersJob rcj)
|
||||||
return rcj.Manga.MangaConnector;
|
return rcj.Manga.MangaConnectorName;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,10 +11,11 @@ public static class TrangaSettings
|
|||||||
public static string downloadLocation { get; private set; } = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Downloads"));
|
public static string downloadLocation { get; private set; } = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Downloads"));
|
||||||
public static string workingDirectory { get; private set; } = Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
|
public static string workingDirectory { get; private set; } = Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
internal static readonly string DefaultUserAgent = $"Tranga ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")}) / 1.0";
|
internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})";
|
||||||
public static string userAgent { get; private set; } = DefaultUserAgent;
|
public static string userAgent { get; private set; } = DefaultUserAgent;
|
||||||
public static int compression{ get; private set; } = 40;
|
public static int compression{ get; private set; } = 40;
|
||||||
public static bool bwImages { get; private set; } = false;
|
public static bool bwImages { get; private set; } = false;
|
||||||
|
public static string flareSolverrUrl { get; private set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Placeholders:
|
/// Placeholders:
|
||||||
/// %M Manga Name
|
/// %M Manga Name
|
||||||
@ -35,14 +36,14 @@ public static class TrangaSettings
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
|
||||||
public static bool aprilFoolsMode { get; private set; } = true;
|
public static bool aprilFoolsMode { get; private set; } = true;
|
||||||
public static int startNewJobTimeoutMs { get; private set; } = 1000;
|
public static int startNewJobTimeoutMs { get; private set; } = 20000;
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
|
internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
|
||||||
{
|
{
|
||||||
{RequestType.MangaInfo, 60},
|
{RequestType.MangaInfo, 60},
|
||||||
{RequestType.MangaDexFeed, 60},
|
{RequestType.MangaDexFeed, 60},
|
||||||
{RequestType.MangaDexImage, 40},
|
{RequestType.MangaDexImage, 60},
|
||||||
{RequestType.MangaImage, 60},
|
{RequestType.MangaImage, 240},
|
||||||
{RequestType.MangaCover, 60},
|
{RequestType.MangaCover, 60},
|
||||||
{RequestType.Default, 60}
|
{RequestType.Default, 60}
|
||||||
};
|
};
|
||||||
@ -102,6 +103,12 @@ public static class TrangaSettings
|
|||||||
ExportSettings();
|
ExportSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void UpdateFlareSolverrUrl(string url)
|
||||||
|
{
|
||||||
|
flareSolverrUrl = url;
|
||||||
|
ExportSettings();
|
||||||
|
}
|
||||||
|
|
||||||
public static void ResetRequestLimits()
|
public static void ResetRequestLimits()
|
||||||
{
|
{
|
||||||
requestLimits = DefaultRequestLimits;
|
requestLimits = DefaultRequestLimits;
|
||||||
@ -148,6 +155,7 @@ public static class TrangaSettings
|
|||||||
jobj.Add("bwImages", JToken.FromObject(bwImages));
|
jobj.Add("bwImages", JToken.FromObject(bwImages));
|
||||||
jobj.Add("startNewJobTimeoutMs", JToken.FromObject(startNewJobTimeoutMs));
|
jobj.Add("startNewJobTimeoutMs", JToken.FromObject(startNewJobTimeoutMs));
|
||||||
jobj.Add("chapterNamingScheme", JToken.FromObject(chapterNamingScheme));
|
jobj.Add("chapterNamingScheme", JToken.FromObject(chapterNamingScheme));
|
||||||
|
jobj.Add("flareSolverrUrl", JToken.FromObject(flareSolverrUrl));
|
||||||
return jobj;
|
return jobj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,5 +182,7 @@ public static class TrangaSettings
|
|||||||
startNewJobTimeoutMs = snjt.Value<int>()!;
|
startNewJobTimeoutMs = snjt.Value<int>()!;
|
||||||
if (jobj.TryGetValue("chapterNamingScheme", out JToken? cns))
|
if (jobj.TryGetValue("chapterNamingScheme", out JToken? cns))
|
||||||
chapterNamingScheme = cns.Value<string>()!;
|
chapterNamingScheme = cns.Value<string>()!;
|
||||||
|
if (jobj.TryGetValue("flareSolverrUrl", out JToken? fsu))
|
||||||
|
flareSolverrUrl = fsu.Value<string>()!;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -39,4 +39,4 @@ WORKDIR /publish
|
|||||||
COPY --chown=1000:1000 --from=build-env /publish .
|
COPY --chown=1000:1000 --from=build-env /publish .
|
||||||
USER 0
|
USER 0
|
||||||
ENTRYPOINT ["dotnet", "/publish/API.dll"]
|
ENTRYPOINT ["dotnet", "/publish/API.dll"]
|
||||||
CMD ["-f", "-c", "-l", "/usr/share/tranga-api/logs"]
|
CMD [""]
|
20
README.md
20
README.md
@ -84,17 +84,16 @@ Endpoints are documented in Swagger. Just spin up an instance, and go to `http:/
|
|||||||
|
|
||||||
## Built With
|
## Built With
|
||||||
|
|
||||||
- .NET
|
- ASP.NET
|
||||||
- ASP.NET
|
- Entity Framework Core
|
||||||
- Entity Framework
|
|
||||||
- [PostgreSQL](https://www.postgresql.org/about/licence/)
|
- [PostgreSQL](https://www.postgresql.org/about/licence/)
|
||||||
- [Swagger](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/LICENSE)
|
|
||||||
- [Ngpsql](https://github.com/npgsql/npgsql/blob/main/LICENSE)
|
- [Ngpsql](https://github.com/npgsql/npgsql/blob/main/LICENSE)
|
||||||
|
- [Swagger](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/LICENSE)
|
||||||
- [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md)
|
- [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md)
|
||||||
|
- [Sixlabors.ImageSharp](https://docs-v2.sixlabors.com/articles/imagesharp/index.html#license)
|
||||||
- [PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp/blob/master/LICENSE)
|
- [PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp/blob/master/LICENSE)
|
||||||
- [Html Agility Pack (HAP)](https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE)
|
- [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)
|
- [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)
|
|
||||||
- 💙 Blåhaj 🦈
|
- 💙 Blåhaj 🦈
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
@ -126,13 +125,13 @@ access the folder. Permission conflicts with Komga and Kavita should thus be lim
|
|||||||
|
|
||||||
### Bare-Metal
|
### Bare-Metal
|
||||||
|
|
||||||
While not supported/currently built, Tranga will also run Bare-Metal without issue.
|
While not supported/currently built, Tranga should also run Bare-Metal without issue.
|
||||||
|
|
||||||
Configuration-Files will be stored per OS:
|
Configuration-Files will be stored per OS:
|
||||||
- Linux `/usr/share/tranga-api`
|
- Linux `/usr/share/tranga-api`
|
||||||
- Windows `%appdata%/tranga-api`
|
- Windows `%appdata%/tranga-api`
|
||||||
|
|
||||||
Downloads (default) are stored in - but this can be configured in `settings.json`:
|
Downloads (default) are stored in - but this can be configured in `settings.json` (which will be generated on first after first launch):
|
||||||
- Linux `/Manga`
|
- Linux `/Manga`
|
||||||
- Windows `%currentDirectory%/Downloads`
|
- Windows `%currentDirectory%/Downloads`
|
||||||
|
|
||||||
@ -148,9 +147,10 @@ If you want to contribute, please feel free to fork and create a Pull-Request!
|
|||||||
General rules:
|
General rules:
|
||||||
- Strongly-type your variables. This improves readability.
|
- Strongly-type your variables. This improves readability.
|
||||||
```csharp
|
```csharp
|
||||||
var xyz = Object.GetSomething(); //Do not do this. What type is xyz?
|
var xyz = Object.GetSomething(); //Do not do this. What type is xyz (without looking at Method returns etc.)?
|
||||||
Manga[] zyx = Object.GetAnotherThing(); //I can now easily see that zyx is an Array.
|
Manga[] zyx = Object.GetAnotherThing(); //I can now easily see that zyx is an Array.
|
||||||
```
|
```
|
||||||
|
Tranga is using a code-first Entity-Framework Core approach. If you modify the db-table structure you need to create a migration.
|
||||||
|
|
||||||
**A broad overview of where is what:**<br />
|
**A broad overview of where is what:**<br />
|
||||||
|
|
||||||
@ -171,6 +171,10 @@ If you want to add a new Website-Connector: <br />
|
|||||||
in the constructor).
|
in the constructor).
|
||||||
4. In `Program.cs` add a new Object to the Array.
|
4. In `Program.cs` add a new Object to the Array.
|
||||||
|
|
||||||
|
### How to test locally
|
||||||
|
|
||||||
|
In the Project root a `docker-compose.local.yaml` file will compile the code and create the container(s).
|
||||||
|
|
||||||
<!-- LICENSE -->
|
<!-- LICENSE -->
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -11,10 +11,16 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "6531:6531"
|
- "6531:6531"
|
||||||
depends_on:
|
depends_on:
|
||||||
- tranga-pg
|
tranga-pg:
|
||||||
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_HOST=tranga-pg
|
- POSTGRES_HOST=tranga-pg
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
tranga-pg:
|
tranga-pg:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
container_name: tranga-pg
|
container_name: tranga-pg
|
||||||
@ -22,4 +28,15 @@ services:
|
|||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=postgres
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 60s
|
||||||
|
retries: 5
|
||||||
|
start_period: 80s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
tranga-api:
|
tranga-api:
|
||||||
image: glax/tranga-api:latest
|
image: glax/tranga-api:Server-V2
|
||||||
container_name: tranga-api
|
container_name: tranga-api
|
||||||
volumes:
|
volumes:
|
||||||
- ./Manga:/Manga
|
- ./Manga:/Manga
|
||||||
@ -9,18 +9,29 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "6531:6531"
|
- "6531:6531"
|
||||||
depends_on:
|
depends_on:
|
||||||
- tranga-pg
|
tranga-pg:
|
||||||
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_HOST=tranga-pg
|
- POSTGRES_HOST=tranga-pg
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
tranga-website:
|
tranga-website:
|
||||||
image: glax/tranga-website:latest
|
image: glax/tranga-website:Server-V2
|
||||||
container_name: tranga-website
|
container_name: tranga-website
|
||||||
ports:
|
ports:
|
||||||
- "9555:80"
|
- "9555:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- tranga-api
|
- tranga-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
tranga-pg:
|
tranga-pg:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
container_name: tranga-pg
|
container_name: tranga-pg
|
||||||
@ -28,4 +39,15 @@ services:
|
|||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=postgres
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 60s
|
||||||
|
retries: 5
|
||||||
|
start_period: 80s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
|
Reference in New Issue
Block a user