Compare commits

..

12 Commits

21 changed files with 1082 additions and 203 deletions

View File

@ -12,10 +12,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" /> <PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.74" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.74" />
<PackageReference Include="log4net" Version="3.0.3" /> <PackageReference Include="log4net" Version="3.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -24,10 +24,10 @@
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
<PackageReference Include="PuppeteerSharp" Version="20.1.3" /> <PackageReference Include="PuppeteerSharp" Version="20.1.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.919" /> <PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.920" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
<PackageReference Include="System.Drawing.Common" Version="9.0.0" /> <PackageReference Include="System.Drawing.Common" Version="9.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -15,7 +15,7 @@ public class JobController(PgsqlContext context) : Controller
/// <summary> /// <summary>
/// Returns all Jobs /// Returns all Jobs
/// </summary> /// </summary>
/// <returns>Array of Jobs</returns> /// <response code="200"></response>
[HttpGet] [HttpGet]
[ProducesResponseType<Job[]>(Status200OK)] [ProducesResponseType<Job[]>(Status200OK)]
public IActionResult GetAllJobs() public IActionResult GetAllJobs()
@ -28,7 +28,7 @@ public class JobController(PgsqlContext context) : Controller
/// Returns Jobs with requested Job-IDs /// Returns Jobs with requested Job-IDs
/// </summary> /// </summary>
/// <param name="ids">Array of Job-IDs</param> /// <param name="ids">Array of Job-IDs</param>
/// <returns>Array of Jobs</returns> /// <response code="200"></response>
[HttpPost("WithIDs")] [HttpPost("WithIDs")]
[ProducesResponseType<Job[]>(Status200OK)] [ProducesResponseType<Job[]>(Status200OK)]
public IActionResult GetJobs([FromBody]string[] ids) public IActionResult GetJobs([FromBody]string[] ids)
@ -41,7 +41,7 @@ public class JobController(PgsqlContext context) : Controller
/// Get all Jobs in requested State /// Get all Jobs in requested State
/// </summary> /// </summary>
/// <param name="state">Requested Job-State</param> /// <param name="state">Requested Job-State</param>
/// <returns>Array of Jobs</returns> /// <response code="200"></response>
[HttpGet("State/{state}")] [HttpGet("State/{state}")]
[ProducesResponseType<Job[]>(Status200OK)] [ProducesResponseType<Job[]>(Status200OK)]
public IActionResult GetJobsInState(JobState state) public IActionResult GetJobsInState(JobState state)
@ -54,7 +54,7 @@ public class JobController(PgsqlContext context) : Controller
/// Returns all Jobs of requested Type /// Returns all Jobs of requested Type
/// </summary> /// </summary>
/// <param name="type">Requested Job-Type</param> /// <param name="type">Requested Job-Type</param>
/// <returns>Array of Jobs</returns> /// <response code="200"></response>
[HttpGet("Type/{type}")] [HttpGet("Type/{type}")]
[ProducesResponseType<Job[]>(Status200OK)] [ProducesResponseType<Job[]>(Status200OK)]
public IActionResult GetJobsOfType(JobType type) public IActionResult GetJobsOfType(JobType type)
@ -67,7 +67,8 @@ public class JobController(PgsqlContext context) : Controller
/// Return Job with ID /// Return Job with ID
/// </summary> /// </summary>
/// <param name="id">Job-ID</param> /// <param name="id">Job-ID</param>
/// <returns>Job</returns> /// <response code="200"></response>
/// <response code="404">Job with ID could not be found</response>
[HttpGet("{id}")] [HttpGet("{id}")]
[ProducesResponseType<Job>(Status200OK)] [ProducesResponseType<Job>(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
@ -84,8 +85,10 @@ public class JobController(PgsqlContext context) : Controller
/// <summary> /// <summary>
/// Create a new CreateNewDownloadChapterJob /// Create a new CreateNewDownloadChapterJob
/// </summary> /// </summary>
/// <param name="request">ID of the Manga, and how often we check again</param> /// <param name="mangaId">ID of Manga</param>
/// <returns>Nothing</returns> /// <param name="recurrenceTime">How often should we check for new chapters</param>
/// <response code="201">Created new Job</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("NewDownloadChapterJob/{mangaId}")] [HttpPut("NewDownloadChapterJob/{mangaId}")]
[ProducesResponseType(Status201Created)] [ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<string>(Status500InternalServerError)]
@ -99,7 +102,8 @@ public class JobController(PgsqlContext context) : Controller
/// Create a new DownloadSingleChapterJob /// Create a new DownloadSingleChapterJob
/// </summary> /// </summary>
/// <param name="chapterId">ID of the Chapter</param> /// <param name="chapterId">ID of the Chapter</param>
/// <returns>Nothing</returns> /// <response code="201">Created new Job</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("DownloadSingleChapterJob/{chapterId}")] [HttpPut("DownloadSingleChapterJob/{chapterId}")]
[ProducesResponseType(Status201Created)] [ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<string>(Status500InternalServerError)]
@ -113,7 +117,8 @@ public class JobController(PgsqlContext context) : Controller
/// Create a new UpdateMetadataJob /// Create a new UpdateMetadataJob
/// </summary> /// </summary>
/// <param name="mangaId">ID of the Manga</param> /// <param name="mangaId">ID of the Manga</param>
/// <returns>Nothing</returns> /// <response code="201">Created new Job</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateMetadataJob/{mangaId}")] [HttpPut("UpdateMetadataJob/{mangaId}")]
[ProducesResponseType(Status201Created)] [ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<string>(Status500InternalServerError)]
@ -126,7 +131,8 @@ public class JobController(PgsqlContext context) : Controller
/// <summary> /// <summary>
/// Create a new UpdateMetadataJob for all Manga /// Create a new UpdateMetadataJob for all Manga
/// </summary> /// </summary>
/// <returns>Nothing</returns> /// <response code="201">Created new Job</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateMetadataJob")] [HttpPut("UpdateMetadataJob")]
[ProducesResponseType(Status201Created)] [ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<string>(Status500InternalServerError)]
@ -161,12 +167,14 @@ public class JobController(PgsqlContext context) : Controller
} }
/// <summary> /// <summary>
/// Delete Job with ID /// Delete Job with ID and all children
/// </summary> /// </summary>
/// <param name="id">Job-ID</param> /// <param name="id">Job-ID</param>
/// <returns>Nothing</returns> /// <response code="200">Job(s) deleted</response>
/// <response code="404">Job could not be found</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{id}")] [HttpDelete("{id}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType<string[]>(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status500InternalServerError)] [ProducesResponseType(Status500InternalServerError)]
public IActionResult DeleteJob(string id) public IActionResult DeleteJob(string id)
@ -174,14 +182,57 @@ public class JobController(PgsqlContext context) : Controller
try try
{ {
Job? ret = context.Jobs.Find(id); Job? ret = context.Jobs.Find(id);
switch (ret is not null) if(ret is null)
{ return NotFound();
case true: IQueryable<Job> children = GetChildJobs(id);
context.Remove(ret);
context.SaveChanges(); context.RemoveRange(children);
return Ok(); context.Remove(ret);
case false: return NotFound(); context.SaveChanges();
} return new OkObjectResult(children.Select(x => x.JobId).Append(ret.JobId).ToArray());
}
catch (Exception e)
{
return StatusCode(500, e.Message);
}
}
private IQueryable<Job> GetChildJobs(string parentJobId)
{
IQueryable<Job> children = context.Jobs.Where(j => j.ParentJobId == parentJobId);
foreach (Job child in children)
foreach (Job grandChild in GetChildJobs(child.JobId))
children.Append(grandChild);
return children;
}
/// <summary>
/// Modify Job with ID
/// </summary>
/// <param name="id">Job-ID</param>
/// <param name="modifyJobRecord">Fields to modify, set to null to keep previous value</param>
/// <response code="202">Job modified</response>
/// <response code="400">Malformed request</response>
/// <response code="404">Job with ID not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{id}/")]
[ProducesResponseType<Job>(Status202Accepted)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult ModifyJob(string id, [FromBody]ModifyJobRecord modifyJobRecord)
{
try
{
Job? ret = context.Jobs.Find(id);
if(ret is null)
return NotFound();
ret.RecurrenceMs = modifyJobRecord.RecurrenceMs ?? ret.RecurrenceMs;
ret.Enabled = modifyJobRecord.Enabled ?? ret.Enabled;
context.SaveChanges();
return new AcceptedResult(ret.JobId, ret);
} }
catch (Exception e) catch (Exception e)
{ {
@ -196,12 +247,12 @@ public class JobController(PgsqlContext context) : Controller
/// <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>
/// <response code="500">Internal Error</response> /// <response code="500">Error during Database Operation</response>
[HttpPost("{id}/Start")] [HttpPost("{id}/Start")]
[ProducesResponseType<AcceptedResult>(Status202Accepted)] [ProducesResponseType(Status202Accepted)]
[ProducesResponseType<NotFoundResult>(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<ConflictResult>(Status409Conflict)] [ProducesResponseType(Status409Conflict)]
[ProducesResponseType<ObjectResult>(Status500InternalServerError)] [ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult StartJob(string id) public IActionResult StartJob(string id)
{ {
Job? ret = context.Jobs.Find(id); Job? ret = context.Jobs.Find(id);
@ -222,21 +273,13 @@ public class JobController(PgsqlContext context) : Controller
} }
/// <summary> /// <summary>
/// NOT IMPLEMENTED. Stops the Job with the requested ID /// Stops the Job with the requested ID
/// </summary> /// </summary>
/// <param name="id">Job-ID</param> /// <param name="id">Job-ID</param>
/// <response code="202">Job started</response>
/// <response code="404">Job with ID not found</response>
/// <response code="409">Job was not running</response>
/// <response code="500">Internal Error</response>
/// <remarks>NOT IMPLEMENTED</remarks> /// <remarks>NOT IMPLEMENTED</remarks>
[ProducesResponseType<AcceptedResult>(Status202Accepted)]
[ProducesResponseType<NotFoundResult>(Status404NotFound)]
[ProducesResponseType<ConflictResult>(Status409Conflict)]
[ProducesResponseType<ObjectResult>(Status500InternalServerError)]
[HttpPost("{id}/Stop")] [HttpPost("{id}/Stop")]
public IActionResult StopJob(string id) public IActionResult StopJob(string id)
{ {
return NotFound(new ProblemResponse("Not implemented")); //TODO throw new NotImplementedException();
} }
} }

View File

@ -15,7 +15,7 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
/// <summary> /// <summary>
/// Gets all configured Library-Connectors /// Gets all configured Library-Connectors
/// </summary> /// </summary>
/// <returns>Array of configured Library-Connectors</returns> /// <response code="200"></response>
[HttpGet] [HttpGet]
[ProducesResponseType<LibraryConnector[]>(Status200OK)] [ProducesResponseType<LibraryConnector[]>(Status200OK)]
public IActionResult GetAllConnectors() public IActionResult GetAllConnectors()
@ -28,7 +28,8 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
/// Returns Library-Connector with requested ID /// Returns Library-Connector with requested ID
/// </summary> /// </summary>
/// <param name="id">Library-Connector-ID</param> /// <param name="id">Library-Connector-ID</param>
/// <returns>Library-Connector</returns> /// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
[HttpGet("{id}")] [HttpGet("{id}")]
[ProducesResponseType<LibraryConnector>(Status200OK)] [ProducesResponseType<LibraryConnector>(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
@ -46,7 +47,8 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
/// Creates a new Library-Connector /// Creates a new Library-Connector
/// </summary> /// </summary>
/// <param name="libraryConnector">Library-Connector</param> /// <param name="libraryConnector">Library-Connector</param>
/// <returns>Nothing</returns> /// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut] [HttpPut]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<string>(Status500InternalServerError)]
@ -68,7 +70,9 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
/// Deletes the Library-Connector with the requested ID /// Deletes the Library-Connector with the requested ID
/// </summary> /// </summary>
/// <param name="id">Library-Connector-ID</param> /// <param name="id">Library-Connector-ID</param>
/// <returns>Nothing</returns> /// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{id}")] [HttpDelete("{id}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]

View File

@ -0,0 +1,79 @@
using API.Schema;
using API.Schema.MangaConnectors;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Produces("application/json")]
[Route("v{v:apiVersion}")]
public class MangaConnectorController(PgsqlContext context) : Controller
{
/// <summary>
/// Get all available Connectors (Scanlation-Sites)
/// </summary>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<MangaConnector[]>(Status200OK)]
public IActionResult GetConnectors()
{
MangaConnector[] connectors = context.MangaConnectors.ToArray();
return Ok(connectors);
}
/// <summary>
/// Get all enabled Connectors (Scanlation-Sites)
/// </summary>
/// <response code="200"></response>
[HttpGet("enabled")]
[ProducesResponseType<MangaConnector[]>(Status200OK)]
public IActionResult GetEnabledConnectors()
{
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == true).ToArray();
return Ok(connectors);
}
/// <summary>
/// Get all disabled Connectors (Scanlation-Sites)
/// </summary>
/// <response code="200"></response>
[HttpGet("disabled")]
[ProducesResponseType<MangaConnector[]>(Status200OK)]
public IActionResult GetDisabledConnectors()
{
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == false).ToArray();
return Ok(connectors);
}
/// <summary>
/// Enabled or disables a Connector
/// </summary>
/// <param name="id">ID of the connector</param>
/// <param name="enabled">Set true to enable</param>
/// <response code="200"></response>
/// <response code="404">Connector with ID not found.</response>
[HttpPatch("{id}/SetEnabled/{enabled}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
public IActionResult SetEnabled(string id, bool enabled)
{
try
{
MangaConnector? connector = context.MangaConnectors.Find(id);
if (connector is null)
return NotFound();
connector.Enabled = enabled;
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
return StatusCode(500, e.Message);
}
}
}

View File

@ -14,7 +14,7 @@ public class MangaController(PgsqlContext context) : Controller
/// <summary> /// <summary>
/// Returns all cached Manga /// Returns all cached Manga
/// </summary> /// </summary>
/// <returns>Array of Manga</returns> /// <response code="200"></response>
[HttpGet] [HttpGet]
[ProducesResponseType<Manga[]>(Status200OK)] [ProducesResponseType<Manga[]>(Status200OK)]
public IActionResult GetAllManga() public IActionResult GetAllManga()
@ -27,7 +27,7 @@ public class MangaController(PgsqlContext context) : Controller
/// Returns all cached Manga with IDs /// Returns all cached Manga with IDs
/// </summary> /// </summary>
/// <param name="ids">Array of Manga-IDs</param> /// <param name="ids">Array of Manga-IDs</param>
/// <returns>Array of Manga</returns> /// <response code="200"></response>
[HttpPost("WithIDs")] [HttpPost("WithIDs")]
[ProducesResponseType<Manga[]>(Status200OK)] [ProducesResponseType<Manga[]>(Status200OK)]
public IActionResult GetManga([FromBody]string[] ids) public IActionResult GetManga([FromBody]string[] ids)
@ -40,42 +40,41 @@ public class MangaController(PgsqlContext context) : Controller
/// Return Manga with ID /// Return Manga with ID
/// </summary> /// </summary>
/// <param name="id">Manga-ID</param> /// <param name="id">Manga-ID</param>
/// <returns>Manga</returns> /// <response code="200"></response>
/// <response code="404">Manga with ID not found</response>
[HttpGet("{id}")] [HttpGet("{id}")]
[ProducesResponseType<Manga>(Status200OK)] [ProducesResponseType<Manga>(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetManga(string id) public IActionResult GetManga(string id)
{ {
Manga? ret = context.Manga.Find(id); Manga? ret = context.Manga.Find(id);
return (ret is not null) switch if (ret is null)
{ return NotFound();
true => Ok(ret), return Ok(ret);
false => NotFound()
};
} }
/// <summary> /// <summary>
/// Delete Manga with ID /// Delete Manga with ID
/// </summary> /// </summary>
/// <param name="id">Manga-ID</param> /// <param name="id">Manga-ID</param>
/// <returns>Nothing</returns> /// <response code="200"></response>
/// <response code="404">Manga with ID not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{id}")] [HttpDelete("{id}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status500InternalServerError)] [ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult DeleteManga(string id) public IActionResult DeleteManga(string id)
{ {
try try
{ {
Manga? ret = context.Manga.Find(id); Manga? ret = context.Manga.Find(id);
switch (ret is not null) if (ret is null)
{ return NotFound();
case true:
context.Remove(ret); context.Remove(ret);
context.SaveChanges(); context.SaveChanges();
return Ok(); return Ok();
case false: return NotFound();
}
} }
catch (Exception e) catch (Exception e)
{ {
@ -87,28 +86,29 @@ public class MangaController(PgsqlContext context) : Controller
/// Returns URL of Cover of Manga /// Returns URL of Cover of Manga
/// </summary> /// </summary>
/// <param name="id">Manga-ID</param> /// <param name="id">Manga-ID</param>
/// <returns>URL of Cover</returns> /// <remarks>NOT IMPLEMENTED</remarks>
[HttpGet("{id}/Cover")] [HttpGet("{id}/Cover")]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult GetCover(string id) public IActionResult GetCover(string id)
{ {
return StatusCode(500, "Not implemented"); //TODO throw new NotImplementedException();
} }
/// <summary> /// <summary>
/// Returns all Chapters of Manga /// Returns all Chapters of Manga
/// </summary> /// </summary>
/// <param name="id">Manga-ID</param> /// <param name="id">Manga-ID</param>
/// <returns>Array of Chapters</returns> /// <response code="200"></response>
/// <response code="404">Manga with ID not found</response>
[HttpGet("{id}/Chapters")] [HttpGet("{id}/Chapters")]
[ProducesResponseType<Chapter[]>(Status200OK)] [ProducesResponseType<Chapter[]>(Status200OK)]
[ProducesResponseType<string>(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
public IActionResult GetChapters(string id) public IActionResult GetChapters(string id)
{ {
Manga? m = context.Manga.Find(id); Manga? m = context.Manga.Find(id);
if (m is null) if (m is null)
return NotFound("Manga could not be found"); return NotFound();
Chapter[] ret = context.Chapters.Where(c => c.ParentManga.MangaId == m.MangaId).ToArray();
Chapter[] ret = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToArray();
return Ok(ret); return Ok(ret);
} }
@ -116,35 +116,46 @@ public class MangaController(PgsqlContext context) : Controller
/// Returns the latest Chapter of requested Manga /// Returns the latest Chapter of requested Manga
/// </summary> /// </summary>
/// <param name="id">Manga-ID</param> /// <param name="id">Manga-ID</param>
/// <returns>Latest Chapter</returns> /// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404">Manga with ID not found.</response>
/// <response code="500">Could not retrieve the maximum chapter-number</response>
[HttpGet("{id}/Chapter/Latest")] [HttpGet("{id}/Chapter/Latest")]
[ProducesResponseType<Chapter>(Status200OK)] [ProducesResponseType<Chapter>(Status200OK)]
[ProducesResponseType<string>(Status404NotFound)] [ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult GetLatestChapter(string id) public IActionResult GetLatestChapter(string id)
{ {
Manga? m = context.Manga.Find(id); Manga? m = context.Manga.Find(id);
if (m is null) if (m is null)
return NotFound("Manga could not be found"); return NotFound();
List<Chapter> chapters = context.Chapters.Where(c => c.ParentManga.MangaId == m.MangaId).ToList();
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToList();
if (chapters.Count == 0)
return NoContent();
Chapter? max = chapters.Max(); Chapter? max = chapters.Max();
if (max is null) if (max is null)
return NotFound("Chapter could not be found"); return StatusCode(500, "Max chapter could not be found");
return Ok(max); return Ok(max);
} }
/// <summary> /// <summary>
/// Configure the cut-off for Manga /// Configure the cut-off for Manga
/// </summary> /// </summary>
/// <remarks>This is important for the DownloadNewChapters-Job</remarks>
/// <param name="id">Manga-ID</param> /// <param name="id">Manga-ID</param>
/// <returns>Nothing</returns> /// <response code="200"></response>
/// <response code="404">Manga with ID not found.</response>
[HttpPatch("{id}/IgnoreChaptersBefore")] [HttpPatch("{id}/IgnoreChaptersBefore")]
[ProducesResponseType<float>(Status200OK)] [ProducesResponseType<float>(Status200OK)]
[ProducesResponseType(Status404NotFound)]
public IActionResult IgnoreChaptersBefore(string id) public IActionResult IgnoreChaptersBefore(string id)
{ {
Manga? m = context.Manga.Find(id); Manga? m = context.Manga.Find(id);
if (m is null) if (m is null)
return NotFound("Manga could not be found"); return NotFound();
return Ok(m.IgnoreChapterBefore); return Ok(m.IgnoreChapterBefore);
} }
@ -153,11 +164,10 @@ public class MangaController(PgsqlContext context) : Controller
/// </summary> /// </summary>
/// <param name="id">Manga-ID</param> /// <param name="id">Manga-ID</param>
/// <param name="folder">New Directory-Path</param> /// <param name="folder">New Directory-Path</param>
/// <returns>Nothing</returns> /// <remarks>NOT IMPLEMENTED</remarks>
[HttpPost("{id}/MoveFolder")] [HttpPost("{id}/MoveFolder")]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult MoveFolder(string id, [FromBody]string folder) public IActionResult MoveFolder(string id, [FromBody]string folder)
{ {
return StatusCode(500, "Not implemented"); //TODO throw new NotImplementedException();
} }
} }

View File

@ -1,26 +0,0 @@
using API.Schema;
using API.Schema.MangaConnectors;
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Produces("application/json")]
[Route("v{v:apiVersion}")]
public class MiscController(PgsqlContext context) : Controller
{
/// <summary>
/// Get all available Connectors (Scanlation-Sites)
/// </summary>
/// <returns>Array of MangaConnector</returns>
[HttpGet("GetConnectors")]
[ProducesResponseType<MangaConnector[]>(Status200OK)]
public IActionResult GetConnectors()
{
MangaConnector[] connectors = context.MangaConnectors.ToArray();
return Ok(connectors);
}
}

View File

@ -15,7 +15,7 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// <summary> /// <summary>
/// Gets all configured Notification-Connectors /// Gets all configured Notification-Connectors
/// </summary> /// </summary>
/// <returns>Array of configured Notification-Connectors</returns> /// <response code="200"></response>
[HttpGet] [HttpGet]
[ProducesResponseType<NotificationConnector[]>(Status200OK)] [ProducesResponseType<NotificationConnector[]>(Status200OK)]
public IActionResult GetAllConnectors() public IActionResult GetAllConnectors()
@ -28,7 +28,8 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// Returns Notification-Connector with requested ID /// Returns Notification-Connector with requested ID
/// </summary> /// </summary>
/// <param name="id">Notification-Connector-ID</param> /// <param name="id">Notification-Connector-ID</param>
/// <returns>Notification-Connector</returns> /// <response code="200"></response>
/// <response code="404">NotificationConnector with ID not found</response>
[HttpGet("{id}")] [HttpGet("{id}")]
[ProducesResponseType<NotificationConnector>(Status200OK)] [ProducesResponseType<NotificationConnector>(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
@ -46,7 +47,8 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// Creates a new Notification-Connector /// Creates a new Notification-Connector
/// </summary> /// </summary>
/// <param name="notificationConnector">Notification-Connector</param> /// <param name="notificationConnector">Notification-Connector</param>
/// <returns>Nothing</returns> /// <response code="201"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut] [HttpPut]
[ProducesResponseType<NotificationConnector[]>(Status200OK)] [ProducesResponseType<NotificationConnector[]>(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<string>(Status500InternalServerError)]
@ -68,7 +70,9 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// Deletes the Notification-Connector with the requested ID /// Deletes the Notification-Connector with the requested ID
/// </summary> /// </summary>
/// <param name="id">Notification-Connector-ID</param> /// <param name="id">Notification-Connector-ID</param>
/// <returns>Nothing</returns> /// <response code="200"></response>
/// <response code="404">NotificationConnector with ID not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{id}")] [HttpDelete("{id}")]
[ProducesResponseType(Status200OK)] [ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
@ -78,14 +82,12 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
try try
{ {
NotificationConnector? ret = context.NotificationConnectors.Find(id); NotificationConnector? ret = context.NotificationConnectors.Find(id);
switch (ret is not null) if(ret is null)
{ return NotFound();
case true:
context.Remove(ret); context.Remove(ret);
context.SaveChanges(); context.SaveChanges();
return Ok(); return Ok();
case false: return NotFound();
}
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -18,14 +18,17 @@ public class SearchController(PgsqlContext context) : Controller
/// Initiate a search for a Manga on all Connectors /// Initiate a search for a Manga on all Connectors
/// </summary> /// </summary>
/// <param name="name">Name/Title of the Manga</param> /// <param name="name">Name/Title of the Manga</param>
/// <returns>Array of Manga</returns> /// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{name}")] [HttpPost("{name}")]
[ProducesResponseType<Manga[]>(Status500InternalServerError)] [ProducesResponseType<Manga[]>(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult SearchMangaGlobal(string name) public IActionResult SearchMangaGlobal(string name)
{ {
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> allManga = new(); List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> allManga = new();
foreach (MangaConnector contextMangaConnector in context.MangaConnectors) foreach (MangaConnector contextMangaConnector in context.MangaConnectors)
allManga.AddRange(contextMangaConnector.GetManga(name)); allManga.AddRange(contextMangaConnector.GetManga(name));
List<Manga> retMangas = new(); List<Manga> retMangas = new();
foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in allManga) foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in allManga)
{ {
@ -35,9 +38,9 @@ public class SearchController(PgsqlContext context) : Controller
if(add is not null) if(add is not null)
retMangas.Add(add); retMangas.Add(add);
} }
catch (DbUpdateException) catch (Exception e)
{ {
return StatusCode(500, new ProblemResponse("An error occurred while processing your request.")); return StatusCode(500, e);
} }
} }
return Ok(retMangas.ToArray()); return Ok(retMangas.ToArray());
@ -48,16 +51,23 @@ public class SearchController(PgsqlContext context) : Controller
/// </summary> /// </summary>
/// <param name="id">Manga-Connector-ID</param> /// <param name="id">Manga-Connector-ID</param>
/// <param name="name">Name/Title of the Manga</param> /// <param name="name">Name/Title of the Manga</param>
/// <returns>Manga</returns> /// <response code="200"></response>
/// <response code="404">MangaConnector with ID not found</response>
/// <response code="406">MangaConnector with ID is disabled</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{id}/{name}")] [HttpPost("{id}/{name}")]
[ProducesResponseType<Manga[]>(Status200OK)] [ProducesResponseType<Manga[]>(Status200OK)]
[ProducesResponseType<ProblemResponse>(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType<ProblemResponse>(Status500InternalServerError)] [ProducesResponseType(Status406NotAcceptable)]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult SearchManga(string id, string name) public IActionResult SearchManga(string id, string name)
{ {
MangaConnector? connector = context.MangaConnectors.Find(id); MangaConnector? connector = context.MangaConnectors.Find(id);
if (connector is null) if (connector is null)
return NotFound(new ProblemResponse("Connector not found.")); return NotFound();
else if (connector.Enabled is false)
return StatusCode(406);
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] mangas = connector.GetManga(name); (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] mangas = connector.GetManga(name);
List<Manga> retMangas = new(); List<Manga> retMangas = new();
foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in mangas) foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in mangas)
@ -68,15 +78,15 @@ public class SearchController(PgsqlContext context) : Controller
if(add is not null) if(add is not null)
retMangas.Add(add); retMangas.Add(add);
} }
catch (DbUpdateException e) catch (Exception e)
{ {
return StatusCode(500, new ProblemResponse("An error occurred while processing your request.", e.Message)); return StatusCode(500, e.Message);
} }
} }
return Ok(retMangas.ToArray()); return Ok(retMangas.ToArray());
} }
private Manga? AddMangaToContext(Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, private Manga? AddMangaToContext(Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links,
List<MangaAltTitle>? altTitles) List<MangaAltTitle>? altTitles)
{ {
@ -144,7 +154,7 @@ public class SearchController(PgsqlContext context) : Controller
context.Manga.Update(existing); context.Manga.Update(existing);
else else
context.Manga.Add(manga); context.Manga.Add(manga);
context.SaveChanges(); context.SaveChanges();
return existing ?? manga; return existing ?? manga;
} }

View File

@ -1,4 +1,6 @@
using API.Schema; using System.Text.Json.Nodes;
using API.MangaDownloadClients;
using API.Schema;
using Asp.Versioning; using Asp.Versioning;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Http.StatusCodes;
@ -14,136 +16,144 @@ public class SettingsController(PgsqlContext context) : Controller
/// <summary> /// <summary>
/// Get all Settings /// Get all Settings
/// </summary> /// </summary>
/// <returns></returns> /// <response code="200"></response>
[HttpGet] [HttpGet]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<JsonObject>(StatusCodes.Status200OK)]
public IActionResult GetSettings() public IActionResult GetSettings()
{ {
return StatusCode(500, "Not implemented"); //TODO return Ok(TrangaSettings.Serialize());
} }
/// <summary> /// <summary>
/// Get the current UserAgent used by Tranga /// Get the current UserAgent used by Tranga
/// </summary> /// </summary>
/// <returns>UserAgent as string</returns> /// <response code="200"></response>
[HttpGet("UserAgent")] [HttpGet("UserAgent")]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<string>(Status200OK)]
public IActionResult GetUserAgent() public IActionResult GetUserAgent()
{ {
return StatusCode(500, "Not implemented"); //TODO return Ok(TrangaSettings.userAgent);
} }
/// <summary> /// <summary>
/// Set a new UserAgent /// Set a new UserAgent
/// </summary> /// </summary>
/// <returns>Nothing</returns> /// <response code="200"></response>
[HttpPatch("UserAgent")] [HttpPatch("UserAgent")]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType(Status200OK)]
public IActionResult SetUserAgent() public IActionResult SetUserAgent([FromBody]string userAgent)
{ {
return StatusCode(500, "Not implemented"); //TODO TrangaSettings.UpdateUserAgent(userAgent);
return Ok();
} }
/// <summary> /// <summary>
/// Reset the UserAgent to default /// Reset the UserAgent to default
/// </summary> /// </summary>
/// <returns>Nothing</returns> /// <response code="200"></response>
[HttpDelete("UserAgent")] [HttpDelete("UserAgent")]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType(Status200OK)]
public IActionResult ResetUserAgent() public IActionResult ResetUserAgent()
{ {
return StatusCode(500, "Not implemented"); //TODO TrangaSettings.UpdateUserAgent(TrangaSettings.DefaultUserAgent);
return Ok();
} }
/// <summary> /// <summary>
/// Get all Request-Limits /// Get all Request-Limits
/// </summary> /// </summary>
/// <returns></returns> /// <response code="200"></response>
[HttpGet("RequestLimits")] [HttpGet("RequestLimits")]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<Dictionary<RequestType,int>>(Status200OK)]
public IActionResult GetRequestLimits() public IActionResult GetRequestLimits()
{ {
return StatusCode(500, "Not implemented"); //TODO return Ok(TrangaSettings.requestLimits);
} }
/// <summary> /// <summary>
/// Update all Request-Limits to new values /// Update all Request-Limits to new values
/// </summary> /// </summary>
/// <returns>Nothing</returns> /// <remarks>NOT IMPLEMENTED</remarks>
[HttpPatch("RequestLimits")] [HttpPatch("RequestLimits")]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult SetRequestLimits() public IActionResult SetRequestLimits()
{ {
return StatusCode(500, "Not implemented"); //TODO throw new NotImplementedException();
} }
/// <summary> /// <summary>
/// Reset all Request-Limits /// Reset all Request-Limits
/// </summary> /// </summary>
/// <returns>Nothing</returns> /// <response code="200"></response>
[HttpDelete("RequestLimits")] [HttpDelete("RequestLimits")]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<string>(Status200OK)]
public IActionResult ResetRequestLimits() public IActionResult ResetRequestLimits()
{ {
return StatusCode(500, "Not implemented"); //TODO TrangaSettings.ResetRequestLimits();
return Ok();
} }
/// <summary> /// <summary>
/// Returns Level of Image-Compression for Images /// Returns Level of Image-Compression for Images
/// </summary> /// </summary>
/// <returns></returns> /// <response code="200">JPEG compression-level as Integer</response>
[HttpGet("ImageCompression")] [HttpGet("ImageCompression")]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<int>(Status200OK)]
public IActionResult GetImageCompression() public IActionResult GetImageCompression()
{ {
return StatusCode(500, "Not implemented"); //TODO return Ok(TrangaSettings.compression);
} }
/// <summary> /// <summary>
/// Set the Image-Compression-Level for Images /// Set the Image-Compression-Level for Images
/// </summary> /// </summary>
/// <param name="percentage">100 to disable, 0-99 for JPEG compression-Level</param> /// <param name="level">100 to disable, 0-99 for JPEG compression-Level</param>
/// <returns>Nothing</returns> /// <response code="200"></response>
/// <response code="400">Level outside permitted range</response>
[HttpPatch("ImageCompression")] [HttpPatch("ImageCompression")]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType(Status200OK)]
public IActionResult SetImageCompression(int percentage) [ProducesResponseType(Status400BadRequest)]
public IActionResult SetImageCompression(int level)
{ {
return StatusCode(500, "Not implemented"); //TODO if (level < 0 || level > 100)
return BadRequest();
TrangaSettings.UpdateCompressImages(level);
return Ok();
} }
/// <summary> /// <summary>
/// Get state of Black/White-Image setting /// Get state of Black/White-Image setting
/// </summary> /// </summary>
/// <returns>True if enabled</returns> /// <response code="200">True if enabled</response>
[HttpGet("BWImages")] [HttpGet("BWImages")]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<bool>(Status200OK)]
public IActionResult GetBwImagesToggle() public IActionResult GetBwImagesToggle()
{ {
return StatusCode(500, "Not implemented"); //TODO return Ok(TrangaSettings.bwImages);
} }
/// <summary> /// <summary>
/// Enable/Disable conversion of Images to Black and White /// Enable/Disable conversion of Images to Black and White
/// </summary> /// </summary>
/// <param name="enabled">true to enable</param> /// <param name="enabled">true to enable</param>
/// <returns>Nothing</returns> /// <response code="200"></response>
[HttpPatch("BWImages")] [HttpPatch("BWImages")]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType(Status200OK)]
public IActionResult SetBwImagesToggle(bool enabled) public IActionResult SetBwImagesToggle(bool enabled)
{ {
return StatusCode(500, "Not implemented"); //TODO TrangaSettings.UpdateBwImages(enabled);
return Ok();
} }
/// <summary> /// <summary>
/// Get state of April Fools Mode /// Get state of April Fools Mode
/// </summary> /// </summary>
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks> /// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
/// <returns>True if enabled</returns> /// <response code="200">True if enabled</response>
[HttpGet("AprilFoolsMode")] [HttpGet("AprilFoolsMode")]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType<bool>(Status200OK)]
public IActionResult GetAprilFoolsMode() public IActionResult GetAprilFoolsMode()
{ {
return StatusCode(500, "Not implemented"); //TODO return Ok(TrangaSettings.aprilFoolsMode);
} }
/// <summary> /// <summary>
@ -151,11 +161,12 @@ public class SettingsController(PgsqlContext context) : Controller
/// </summary> /// </summary>
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks> /// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
/// <param name="enabled">true to enable</param> /// <param name="enabled">true to enable</param>
/// <returns>Nothing</returns> /// <response code="200"></response>
[HttpPatch("AprilFoolsMode")] [HttpPatch("AprilFoolsMode")]
[ProducesResponseType<string>(Status500InternalServerError)] [ProducesResponseType(Status200OK)]
public IActionResult SetAprilFoolsMode(bool enabled) public IActionResult SetAprilFoolsMode(bool enabled)
{ {
return StatusCode(500, "Not implemented"); //TODO TrangaSettings.UpdateAprilFoolsMode(enabled);
return Ok();
} }
} }

View File

@ -0,0 +1,678 @@
// <auto-generated />
using System;
using API.Schema;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations
{
[DbContext(typeof(PgsqlContext))]
[Migration("20250307104111_dev-070325-1")]
partial class dev0703251
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.Author", b =>
{
b.Property<string>("AuthorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasColumnType("text");
b.HasKey("AuthorId");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.Property<string>("ChapterId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ArchiveFileName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ChapterNumber")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded")
.HasColumnType("boolean");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("ChapterId");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Property<string>("JobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("JobId1")
.HasColumnType("character varying(64)");
b.Property<byte>("JobType")
.HasColumnType("smallint");
b.Property<DateTime>("LastExecution")
.HasColumnType("timestamp with time zone");
b.Property<string>("ParentJobId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<decimal>("RecurrenceMs")
.HasColumnType("numeric(20,0)");
b.Property<byte>("state")
.HasColumnType("smallint");
b.HasKey("JobId");
b.HasIndex("JobId1");
b.HasIndex("ParentJobId");
b.ToTable("Jobs");
b.HasDiscriminator<byte>("JobType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
{
b.Property<string>("LibraryConnectorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("text");
b.Property<string>("BaseUrl")
.IsRequired()
.HasColumnType("text");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("LibraryConnectorId");
b.ToTable("LibraryConnectors");
b.HasDiscriminator<byte>("LibraryType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.Link", b =>
{
b.Property<string>("LinkId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("LinkProvider")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkUrl")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("LinkId");
b.HasIndex("MangaId");
b.ToTable("Link");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Property<string>("MangaId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ConnectorId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasColumnType("text");
b.Property<string>("CoverUrl")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FolderName")
.IsRequired()
.HasColumnType("text");
b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real");
b.Property<string>("MangaConnectorId")
.IsRequired()
.HasColumnType("character varying(32)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("OriginalLanguage")
.HasColumnType("text");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<string>("WebsiteUrl")
.IsRequired()
.HasColumnType("text");
b.Property<long>("Year")
.HasColumnType("bigint");
b.HasKey("MangaId");
b.HasIndex("MangaConnectorId");
b.ToTable("Manga");
});
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
{
b.Property<string>("AltTitleId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("AltTitleId");
b.HasIndex("MangaId");
b.ToTable("AltTitles");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.PrimitiveCollection<string[]>("BaseUris")
.IsRequired()
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired()
.HasColumnType("text[]");
b.HasKey("Name");
b.ToTable("MangaConnectors");
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaTag", b =>
{
b.Property<string>("Tag")
.HasColumnType("text");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.Notification", b =>
{
b.Property<string>("NotificationId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("NotificationId");
b.ToTable("Notifications");
});
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
{
b.Property<string>("NotificationConnectorId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<byte>("NotificationConnectorType")
.HasColumnType("smallint");
b.HasKey("NotificationConnectorId");
b.ToTable("NotificationConnectors");
b.HasDiscriminator<byte>("NotificationConnectorType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("AuthorManga", b =>
{
b.Property<string>("AuthorsAuthorId")
.HasColumnType("character varying(64)");
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.HasKey("AuthorsAuthorId", "MangaId");
b.HasIndex("MangaId");
b.ToTable("AuthorManga");
});
modelBuilder.Entity("MangaMangaTag", b =>
{
b.Property<string>("MangaId")
.HasColumnType("character varying(64)");
b.Property<string>("TagsTag")
.HasColumnType("text");
b.HasKey("MangaId", "TagsTag");
b.HasIndex("TagsTag");
b.ToTable("MangaMangaTag");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("ChapterId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("ChapterId");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("FromLocation")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ToLocation")
.IsRequired()
.HasColumnType("text");
b.HasDiscriminator().HasValue((byte)3);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{
b.HasBaseType("API.Schema.Jobs.Job");
b.Property<string>("MangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasIndex("MangaId");
b.ToTable("Jobs", t =>
{
t.Property("MangaId")
.HasColumnName("UpdateMetadataJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)2);
});
modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b =>
{
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
{
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("AsuraToon");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Bato");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaHere");
});
modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaKatana");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Mangaworld");
});
modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ManhuaPlus");
});
modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
{
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Weebcentral");
});
modelBuilder.Entity("API.Schema.NotificationConnectors.Gotify", b =>
{
b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector");
b.Property<string>("AppToken")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("text");
b.HasDiscriminator().HasValue((byte)0);
});
modelBuilder.Entity("API.Schema.NotificationConnectors.Lunasea", b =>
{
b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector");
b.Property<string>("Id")
.IsRequired()
.HasColumnType("text");
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.NotificationConnectors.Ntfy", b =>
{
b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Topic")
.IsRequired()
.HasColumnType("text");
b.ToTable("NotificationConnectors", t =>
{
t.Property("Endpoint")
.HasColumnName("Ntfy_Endpoint");
});
b.HasDiscriminator().HasValue((byte)2);
});
modelBuilder.Entity("API.Schema.Chapter", b =>
{
b.HasOne("API.Schema.Manga", "ParentManga")
.WithMany()
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.HasOne("API.Schema.Jobs.Job", null)
.WithMany("DependsOnJobs")
.HasForeignKey("JobId1");
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
.WithMany()
.HasForeignKey("ParentJobId");
b.Navigation("ParentJob");
});
modelBuilder.Entity("API.Schema.Link", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("Links")
.HasForeignKey("MangaId");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
.WithMany()
.HasForeignKey("MangaConnectorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("MangaConnector");
});
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany("AltTitles")
.HasForeignKey("MangaId");
});
modelBuilder.Entity("AuthorManga", b =>
{
b.HasOne("API.Schema.Author", null)
.WithMany()
.HasForeignKey("AuthorsAuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaMangaTag", b =>
{
b.HasOne("API.Schema.Manga", null)
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaTag", null)
.WithMany()
.HasForeignKey("TagsTag")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
{
b.HasOne("API.Schema.Chapter", "Chapter")
.WithMany()
.HasForeignKey("ChapterId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Chapter");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
{
b.Navigation("DependsOnJobs");
});
modelBuilder.Entity("API.Schema.Manga", b =>
{
b.Navigation("AltTitles");
b.Navigation("Links");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class dev0703251 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Enabled",
table: "MangaConnectors",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "Enabled",
table: "Jobs",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Enabled",
table: "MangaConnectors");
migrationBuilder.DropColumn(
name: "Enabled",
table: "Jobs");
}
}
}

View File

@ -17,7 +17,7 @@ namespace API.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("ProductVersion", "9.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@ -86,6 +86,9 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("text[]"); .HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("JobId1") b.Property<string>("JobId1")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
@ -260,6 +263,9 @@ namespace API.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text[]"); .HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.PrimitiveCollection<string[]>("SupportedLanguages") b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired() .IsRequired()
.HasColumnType("text[]"); .HasColumnType("text[]");

3
API/ModifyJobRecord.cs Normal file
View File

@ -0,0 +1,3 @@
namespace API;
public record ModifyJobRecord(ulong? RecurrenceMs, bool? Enabled);

View File

@ -1,3 +0,0 @@
namespace API;
public record ProblemResponse(string title, string? message = null);

View File

@ -84,7 +84,7 @@ public class Chapter : IComparable<Chapter>
} }
/// <summary> /// <summary>
/// Creates full file path of chapter-archive /// Creates full file path of chapter-archive
/// </summary> /// </summary>
/// <returns>Filepath</returns> /// <returns>Filepath</returns>
internal string GetArchiveFilePath() internal string GetArchiveFilePath()
@ -92,6 +92,10 @@ public class Chapter : IComparable<Chapter>
return Path.Join(TrangaSettings.downloadLocation, ParentManga.FolderName, ArchiveFileName); return Path.Join(TrangaSettings.downloadLocation, ParentManga.FolderName, ArchiveFileName);
} }
/// <summary>
/// Checks the filesystem if an archive at the ArchiveFilePath exists
/// </summary>
/// <returns>True if archive exists on disk</returns>
public bool IsDownloaded() public bool IsDownloaded()
{ {
string path = GetArchiveFilePath(); string path = GetArchiveFilePath();

View File

@ -20,15 +20,9 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
Chapter c = Chapter ?? context.Chapters.Find(ChapterId)!; Chapter chapter = Chapter ?? context.Chapters.Find(ChapterId)!;
Manga m = c.ParentManga ?? context.Manga.Find(c.ParentMangaId)!; Manga manga = chapter.ParentManga ?? context.Manga.Find(chapter.ParentMangaId)!;
MangaConnector connector = m.MangaConnector ?? context.MangaConnectors.Find(m.MangaConnectorId)!; MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
DownloadChapterImages(c, connector, m);
return [];
}
private bool DownloadChapterImages(Chapter chapter, MangaConnector connector, Manga manga)
{
string[] imageUrls = connector.GetChapterImageUrls(chapter); string[] imageUrls = connector.GetChapterImageUrls(chapter);
string saveArchiveFilePath = chapter.GetArchiveFilePath(); string saveArchiveFilePath = chapter.GetArchiveFilePath();
@ -52,7 +46,7 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
if (imageUrls.Length == 0) if (imageUrls.Length == 0)
{ {
Directory.Delete(tempFolder, true); Directory.Delete(tempFolder, true);
return false; return [];
} }
foreach (string imageUrl in imageUrls) foreach (string imageUrl in imageUrls)
@ -61,10 +55,10 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}"); string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
bool status = DownloadImage(imageUrl, imagePath); bool status = DownloadImage(imageUrl, imagePath);
if (status is false) if (status is false)
return false; return [];
} }
CopyCoverFromCacheToDownloadLocation(manga); CopyCoverFromCacheToDownloadLocation(context, manga);
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString()); File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
@ -74,7 +68,10 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute); File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
Directory.Delete(tempFolder, true); //Cleanup Directory.Delete(tempFolder, true); //Cleanup
return true; chapter.Downloaded = true;
context.SaveChanges();
return [];
} }
private void ProcessImage(string imagePath) private void ProcessImage(string imagePath)
@ -92,7 +89,7 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
}); });
} }
private void CopyCoverFromCacheToDownloadLocation(Manga manga, int? retries = 1) private void CopyCoverFromCacheToDownloadLocation(PgsqlContext context, Manga manga, int? retries = 1)
{ {
//Check if Publication already has a Folder and cover //Check if Publication already has a Folder and cover
string publicationFolder = manga.CreatePublicationFolder(); string publicationFolder = manga.CreatePublicationFolder();
@ -107,8 +104,9 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
{ {
if (retries > 0) if (retries > 0)
{ {
manga.SaveCoverImageToCache(); manga.CoverFileNameInCache = manga.SaveCoverImageToCache();
CopyCoverFromCacheToDownloadLocation(manga, --retries); context.SaveChanges();
CopyCoverFromCacheToDownloadLocation(context, manga, --retries);
} }
return; return;

View File

@ -26,6 +26,7 @@ public abstract class Job
[NotMapped] [NotMapped]
public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs); public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs);
public JobState state { get; internal set; } = JobState.Waiting; public JobState state { get; internal set; } = JobState.Waiting;
public bool Enabled { get; internal set; } = true;
public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null) public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
: this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList()) : this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList())

View File

@ -8,10 +8,24 @@ public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? paren
{ {
[MaxLength(64)] [MaxLength(64)]
public string MangaId { get; init; } = mangaId; public string MangaId { get; init; } = mangaId;
public virtual Manga Manga { get; init; } public virtual Manga? Manga { get; init; }
/// <summary>
/// Updates all data related to Manga.
/// Retrieves data from Mangaconnector
/// Updates Chapter-info
/// </summary>
/// <param name="context"></param>
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
throw new NotImplementedException(); //Manga manga = Manga ?? context.Manga.Find(MangaId)!;
IQueryable<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId);
foreach (Chapter chapter in chapters)
chapter.Downloaded = chapter.IsDownloaded();
context.SaveChanges();
return [];
//TODO implement Metadata-Update from MangaConnector
} }
} }

View File

@ -14,6 +14,8 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
public string[] SupportedLanguages { get; init; } = supportedLanguages; public string[] SupportedLanguages { get; init; } = supportedLanguages;
public string[] BaseUris { get; init; } = baseUris; public string[] BaseUris { get; init; } = baseUris;
public bool Enabled { get; internal set; } = true;
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = ""); public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "");
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url); public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url);

View File

@ -69,18 +69,21 @@ public static class Tranga
Log.Info(TRANGA); Log.Info(TRANGA);
while (true) while (true)
{ {
List<Job> completedJobs = context.Jobs.Where(j => j.state >= JobState.Completed && j.state < JobState.Failed).ToList(); List<Job> completedJobs = context.Jobs.Where(j => j.state >= JobState.Completed).ToList();
foreach (Job job in completedJobs) foreach (Job job in completedJobs)
if (job.RecurrenceMs <= 0) if (job.RecurrenceMs <= 0)
context.Jobs.Remove(job); context.Jobs.Remove(job);
else else
{ {
if (job.state >= JobState.Failed)
job.Enabled = false;
else
job.state = JobState.Waiting;
job.LastExecution = DateTime.UtcNow; job.LastExecution = DateTime.UtcNow;
job.state = JobState.Waiting;
context.Jobs.Update(job); context.Jobs.Update(job);
} }
List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running).ToList() List<Job> runJobs = context.Jobs.Where(j => j.state <= JobState.Running && j.Enabled == true).ToList()
.Where(j => j.NextExecution < DateTime.UtcNow).ToList(); .Where(j => j.NextExecution < DateTime.UtcNow).ToList();
foreach (Job job in runJobs) foreach (Job job in runJobs)
{ {

View File

@ -118,7 +118,7 @@ public static class TrangaSettings
ExportSettings(); ExportSettings();
} }
public static void ResetRateLimits() public static void ResetRequestLimits()
{ {
requestLimits = DefaultRequestLimits; requestLimits = DefaultRequestLimits;
ExportSettings(); ExportSettings();