Compare commits

...

12 Commits

21 changed files with 1082 additions and 203 deletions

View File

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

View File

@ -15,7 +15,7 @@ public class JobController(PgsqlContext context) : Controller
/// <summary>
/// Returns all Jobs
/// </summary>
/// <returns>Array of Jobs</returns>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<Job[]>(Status200OK)]
public IActionResult GetAllJobs()
@ -28,7 +28,7 @@ public class JobController(PgsqlContext context) : Controller
/// Returns Jobs with requested Job-IDs
/// </summary>
/// <param name="ids">Array of Job-IDs</param>
/// <returns>Array of Jobs</returns>
/// <response code="200"></response>
[HttpPost("WithIDs")]
[ProducesResponseType<Job[]>(Status200OK)]
public IActionResult GetJobs([FromBody]string[] ids)
@ -41,7 +41,7 @@ public class JobController(PgsqlContext context) : Controller
/// Get all Jobs in requested State
/// </summary>
/// <param name="state">Requested Job-State</param>
/// <returns>Array of Jobs</returns>
/// <response code="200"></response>
[HttpGet("State/{state}")]
[ProducesResponseType<Job[]>(Status200OK)]
public IActionResult GetJobsInState(JobState state)
@ -54,7 +54,7 @@ public class JobController(PgsqlContext context) : Controller
/// Returns all Jobs of requested Type
/// </summary>
/// <param name="type">Requested Job-Type</param>
/// <returns>Array of Jobs</returns>
/// <response code="200"></response>
[HttpGet("Type/{type}")]
[ProducesResponseType<Job[]>(Status200OK)]
public IActionResult GetJobsOfType(JobType type)
@ -67,7 +67,8 @@ public class JobController(PgsqlContext context) : Controller
/// Return Job with ID
/// </summary>
/// <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}")]
[ProducesResponseType<Job>(Status200OK)]
[ProducesResponseType(Status404NotFound)]
@ -84,8 +85,10 @@ public class JobController(PgsqlContext context) : Controller
/// <summary>
/// Create a new CreateNewDownloadChapterJob
/// </summary>
/// <param name="request">ID of the Manga, and how often we check again</param>
/// <returns>Nothing</returns>
/// <param name="mangaId">ID of Manga</param>
/// <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}")]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError)]
@ -99,7 +102,8 @@ public class JobController(PgsqlContext context) : Controller
/// Create a new DownloadSingleChapterJob
/// </summary>
/// <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}")]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError)]
@ -113,7 +117,8 @@ public class JobController(PgsqlContext context) : Controller
/// Create a new UpdateMetadataJob
/// </summary>
/// <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}")]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError)]
@ -126,7 +131,8 @@ public class JobController(PgsqlContext context) : Controller
/// <summary>
/// Create a new UpdateMetadataJob for all Manga
/// </summary>
/// <returns>Nothing</returns>
/// <response code="201">Created new Job</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateMetadataJob")]
[ProducesResponseType(Status201Created)]
[ProducesResponseType<string>(Status500InternalServerError)]
@ -161,12 +167,14 @@ public class JobController(PgsqlContext context) : Controller
}
/// <summary>
/// Delete Job with ID
/// Delete Job with ID and all children
/// </summary>
/// <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}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string[]>(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status500InternalServerError)]
public IActionResult DeleteJob(string id)
@ -174,14 +182,57 @@ public class JobController(PgsqlContext context) : Controller
try
{
Job? ret = context.Jobs.Find(id);
switch (ret is not null)
{
case true:
context.Remove(ret);
context.SaveChanges();
return Ok();
case false: return NotFound();
}
if(ret is null)
return NotFound();
IQueryable<Job> children = GetChildJobs(id);
context.RemoveRange(children);
context.Remove(ret);
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)
{
@ -196,12 +247,12 @@ public class JobController(PgsqlContext context) : Controller
/// <response code="202">Job started</response>
/// <response code="404">Job with ID not found</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")]
[ProducesResponseType<AcceptedResult>(Status202Accepted)]
[ProducesResponseType<NotFoundResult>(Status404NotFound)]
[ProducesResponseType<ConflictResult>(Status409Conflict)]
[ProducesResponseType<ObjectResult>(Status500InternalServerError)]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult StartJob(string id)
{
Job? ret = context.Jobs.Find(id);
@ -222,21 +273,13 @@ public class JobController(PgsqlContext context) : Controller
}
/// <summary>
/// NOT IMPLEMENTED. Stops the Job with the requested ID
/// Stops the Job with the requested ID
/// </summary>
/// <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>
[ProducesResponseType<AcceptedResult>(Status202Accepted)]
[ProducesResponseType<NotFoundResult>(Status404NotFound)]
[ProducesResponseType<ConflictResult>(Status409Conflict)]
[ProducesResponseType<ObjectResult>(Status500InternalServerError)]
[HttpPost("{id}/Stop")]
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>
/// Gets all configured Library-Connectors
/// </summary>
/// <returns>Array of configured Library-Connectors</returns>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<LibraryConnector[]>(Status200OK)]
public IActionResult GetAllConnectors()
@ -28,7 +28,8 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
/// Returns Library-Connector with requested ID
/// </summary>
/// <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}")]
[ProducesResponseType<LibraryConnector>(Status200OK)]
[ProducesResponseType(Status404NotFound)]
@ -46,7 +47,8 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
/// Creates a new Library-Connector
/// </summary>
/// <param name="libraryConnector">Library-Connector</param>
/// <returns>Nothing</returns>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError)]
@ -68,7 +70,9 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
/// Deletes the Library-Connector with the requested ID
/// </summary>
/// <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}")]
[ProducesResponseType(Status200OK)]
[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>
/// Returns all cached Manga
/// </summary>
/// <returns>Array of Manga</returns>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<Manga[]>(Status200OK)]
public IActionResult GetAllManga()
@ -27,7 +27,7 @@ public class MangaController(PgsqlContext context) : Controller
/// Returns all cached Manga with IDs
/// </summary>
/// <param name="ids">Array of Manga-IDs</param>
/// <returns>Array of Manga</returns>
/// <response code="200"></response>
[HttpPost("WithIDs")]
[ProducesResponseType<Manga[]>(Status200OK)]
public IActionResult GetManga([FromBody]string[] ids)
@ -40,42 +40,41 @@ public class MangaController(PgsqlContext context) : Controller
/// Return Manga with ID
/// </summary>
/// <param name="id">Manga-ID</param>
/// <returns>Manga</returns>
/// <response code="200"></response>
/// <response code="404">Manga with ID not found</response>
[HttpGet("{id}")]
[ProducesResponseType<Manga>(Status200OK)]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetManga(string id)
{
Manga? ret = context.Manga.Find(id);
return (ret is not null) switch
{
true => Ok(ret),
false => NotFound()
};
if (ret is null)
return NotFound();
return Ok(ret);
}
/// <summary>
/// Delete Manga with ID
/// </summary>
/// <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}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status500InternalServerError)]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult DeleteManga(string id)
{
try
{
Manga? ret = context.Manga.Find(id);
switch (ret is not null)
{
case true:
context.Remove(ret);
context.SaveChanges();
return Ok();
case false: return NotFound();
}
if (ret is null)
return NotFound();
context.Remove(ret);
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
@ -87,28 +86,29 @@ public class MangaController(PgsqlContext context) : Controller
/// Returns URL of Cover of Manga
/// </summary>
/// <param name="id">Manga-ID</param>
/// <returns>URL of Cover</returns>
/// <remarks>NOT IMPLEMENTED</remarks>
[HttpGet("{id}/Cover")]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult GetCover(string id)
{
return StatusCode(500, "Not implemented"); //TODO
throw new NotImplementedException();
}
/// <summary>
/// Returns all Chapters of Manga
/// </summary>
/// <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")]
[ProducesResponseType<Chapter[]>(Status200OK)]
[ProducesResponseType<string>(Status404NotFound)]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetChapters(string id)
{
Manga? m = context.Manga.Find(id);
if (m is null)
return NotFound("Manga could not be found");
Chapter[] ret = context.Chapters.Where(c => c.ParentManga.MangaId == m.MangaId).ToArray();
return NotFound();
Chapter[] ret = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToArray();
return Ok(ret);
}
@ -116,35 +116,46 @@ public class MangaController(PgsqlContext context) : Controller
/// Returns the latest Chapter of requested Manga
/// </summary>
/// <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")]
[ProducesResponseType<Chapter>(Status200OK)]
[ProducesResponseType<string>(Status404NotFound)]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult GetLatestChapter(string id)
{
Manga? m = context.Manga.Find(id);
if (m is null)
return NotFound("Manga could not be found");
List<Chapter> chapters = context.Chapters.Where(c => c.ParentManga.MangaId == m.MangaId).ToList();
return NotFound();
List<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == m.MangaId).ToList();
if (chapters.Count == 0)
return NoContent();
Chapter? max = chapters.Max();
if (max is null)
return NotFound("Chapter could not be found");
return StatusCode(500, "Max chapter could not be found");
return Ok(max);
}
/// <summary>
/// Configure the cut-off for Manga
/// </summary>
/// <remarks>This is important for the DownloadNewChapters-Job</remarks>
/// <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")]
[ProducesResponseType<float>(Status200OK)]
[ProducesResponseType(Status404NotFound)]
public IActionResult IgnoreChaptersBefore(string id)
{
Manga? m = context.Manga.Find(id);
if (m is null)
return NotFound("Manga could not be found");
return NotFound();
return Ok(m.IgnoreChapterBefore);
}
@ -153,11 +164,10 @@ public class MangaController(PgsqlContext context) : Controller
/// </summary>
/// <param name="id">Manga-ID</param>
/// <param name="folder">New Directory-Path</param>
/// <returns>Nothing</returns>
/// <remarks>NOT IMPLEMENTED</remarks>
[HttpPost("{id}/MoveFolder")]
[ProducesResponseType<string>(Status500InternalServerError)]
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>
/// Gets all configured Notification-Connectors
/// </summary>
/// <returns>Array of configured Notification-Connectors</returns>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<NotificationConnector[]>(Status200OK)]
public IActionResult GetAllConnectors()
@ -28,7 +28,8 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// Returns Notification-Connector with requested ID
/// </summary>
/// <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}")]
[ProducesResponseType<NotificationConnector>(Status200OK)]
[ProducesResponseType(Status404NotFound)]
@ -46,7 +47,8 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// Creates a new Notification-Connector
/// </summary>
/// <param name="notificationConnector">Notification-Connector</param>
/// <returns>Nothing</returns>
/// <response code="201"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut]
[ProducesResponseType<NotificationConnector[]>(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError)]
@ -68,7 +70,9 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// Deletes the Notification-Connector with the requested ID
/// </summary>
/// <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}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
@ -78,14 +82,12 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
try
{
NotificationConnector? ret = context.NotificationConnectors.Find(id);
switch (ret is not null)
{
case true:
context.Remove(ret);
context.SaveChanges();
return Ok();
case false: return NotFound();
}
if(ret is null)
return NotFound();
context.Remove(ret);
context.SaveChanges();
return Ok();
}
catch (Exception e)
{

View File

@ -18,14 +18,17 @@ public class SearchController(PgsqlContext context) : Controller
/// Initiate a search for a Manga on all Connectors
/// </summary>
/// <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}")]
[ProducesResponseType<Manga[]>(Status500InternalServerError)]
[ProducesResponseType<Manga[]>(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult SearchMangaGlobal(string name)
{
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> allManga = new();
foreach (MangaConnector contextMangaConnector in context.MangaConnectors)
allManga.AddRange(contextMangaConnector.GetManga(name));
List<Manga> retMangas = new();
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)
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());
@ -48,16 +51,23 @@ public class SearchController(PgsqlContext context) : Controller
/// </summary>
/// <param name="id">Manga-Connector-ID</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}")]
[ProducesResponseType<Manga[]>(Status200OK)]
[ProducesResponseType<ProblemResponse>(Status404NotFound)]
[ProducesResponseType<ProblemResponse>(Status500InternalServerError)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status406NotAcceptable)]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult SearchManga(string id, string name)
{
MangaConnector? connector = context.MangaConnectors.Find(id);
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);
List<Manga> retMangas = new();
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)
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());
}
private Manga? AddMangaToContext(Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links,
List<MangaAltTitle>? altTitles)
{
@ -144,7 +154,7 @@ public class SearchController(PgsqlContext context) : Controller
context.Manga.Update(existing);
else
context.Manga.Add(manga);
context.SaveChanges();
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 Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
@ -14,136 +16,144 @@ public class SettingsController(PgsqlContext context) : Controller
/// <summary>
/// Get all Settings
/// </summary>
/// <returns></returns>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<string>(Status500InternalServerError)]
[ProducesResponseType<JsonObject>(StatusCodes.Status200OK)]
public IActionResult GetSettings()
{
return StatusCode(500, "Not implemented"); //TODO
return Ok(TrangaSettings.Serialize());
}
/// <summary>
/// Get the current UserAgent used by Tranga
/// </summary>
/// <returns>UserAgent as string</returns>
/// <response code="200"></response>
[HttpGet("UserAgent")]
[ProducesResponseType<string>(Status500InternalServerError)]
[ProducesResponseType<string>(Status200OK)]
public IActionResult GetUserAgent()
{
return StatusCode(500, "Not implemented"); //TODO
return Ok(TrangaSettings.userAgent);
}
/// <summary>
/// Set a new UserAgent
/// </summary>
/// <returns>Nothing</returns>
/// <response code="200"></response>
[HttpPatch("UserAgent")]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult SetUserAgent()
[ProducesResponseType(Status200OK)]
public IActionResult SetUserAgent([FromBody]string userAgent)
{
return StatusCode(500, "Not implemented"); //TODO
TrangaSettings.UpdateUserAgent(userAgent);
return Ok();
}
/// <summary>
/// Reset the UserAgent to default
/// </summary>
/// <returns>Nothing</returns>
/// <response code="200"></response>
[HttpDelete("UserAgent")]
[ProducesResponseType<string>(Status500InternalServerError)]
[ProducesResponseType(Status200OK)]
public IActionResult ResetUserAgent()
{
return StatusCode(500, "Not implemented"); //TODO
TrangaSettings.UpdateUserAgent(TrangaSettings.DefaultUserAgent);
return Ok();
}
/// <summary>
/// Get all Request-Limits
/// </summary>
/// <returns></returns>
/// <response code="200"></response>
[HttpGet("RequestLimits")]
[ProducesResponseType<string>(Status500InternalServerError)]
[ProducesResponseType<Dictionary<RequestType,int>>(Status200OK)]
public IActionResult GetRequestLimits()
{
return StatusCode(500, "Not implemented"); //TODO
return Ok(TrangaSettings.requestLimits);
}
/// <summary>
/// Update all Request-Limits to new values
/// </summary>
/// <returns>Nothing</returns>
/// <remarks>NOT IMPLEMENTED</remarks>
[HttpPatch("RequestLimits")]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult SetRequestLimits()
{
return StatusCode(500, "Not implemented"); //TODO
throw new NotImplementedException();
}
/// <summary>
/// Reset all Request-Limits
/// </summary>
/// <returns>Nothing</returns>
/// <response code="200"></response>
[HttpDelete("RequestLimits")]
[ProducesResponseType<string>(Status500InternalServerError)]
[ProducesResponseType<string>(Status200OK)]
public IActionResult ResetRequestLimits()
{
return StatusCode(500, "Not implemented"); //TODO
TrangaSettings.ResetRequestLimits();
return Ok();
}
/// <summary>
/// Returns Level of Image-Compression for Images
/// </summary>
/// <returns></returns>
/// <response code="200">JPEG compression-level as Integer</response>
[HttpGet("ImageCompression")]
[ProducesResponseType<string>(Status500InternalServerError)]
[ProducesResponseType<int>(Status200OK)]
public IActionResult GetImageCompression()
{
return StatusCode(500, "Not implemented"); //TODO
return Ok(TrangaSettings.compression);
}
/// <summary>
/// Set the Image-Compression-Level for Images
/// </summary>
/// <param name="percentage">100 to disable, 0-99 for JPEG compression-Level</param>
/// <returns>Nothing</returns>
/// <param name="level">100 to disable, 0-99 for JPEG compression-Level</param>
/// <response code="200"></response>
/// <response code="400">Level outside permitted range</response>
[HttpPatch("ImageCompression")]
[ProducesResponseType<string>(Status500InternalServerError)]
public IActionResult SetImageCompression(int percentage)
[ProducesResponseType(Status200OK)]
[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>
/// Get state of Black/White-Image setting
/// </summary>
/// <returns>True if enabled</returns>
/// <response code="200">True if enabled</response>
[HttpGet("BWImages")]
[ProducesResponseType<string>(Status500InternalServerError)]
[ProducesResponseType<bool>(Status200OK)]
public IActionResult GetBwImagesToggle()
{
return StatusCode(500, "Not implemented"); //TODO
return Ok(TrangaSettings.bwImages);
}
/// <summary>
/// Enable/Disable conversion of Images to Black and White
/// </summary>
/// <param name="enabled">true to enable</param>
/// <returns>Nothing</returns>
/// <response code="200"></response>
[HttpPatch("BWImages")]
[ProducesResponseType<string>(Status500InternalServerError)]
[ProducesResponseType(Status200OK)]
public IActionResult SetBwImagesToggle(bool enabled)
{
return StatusCode(500, "Not implemented"); //TODO
TrangaSettings.UpdateBwImages(enabled);
return Ok();
}
/// <summary>
/// Get state of April Fools Mode
/// </summary>
/// <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")]
[ProducesResponseType<string>(Status500InternalServerError)]
[ProducesResponseType<bool>(Status200OK)]
public IActionResult GetAprilFoolsMode()
{
return StatusCode(500, "Not implemented"); //TODO
return Ok(TrangaSettings.aprilFoolsMode);
}
/// <summary>
@ -151,11 +161,12 @@ public class SettingsController(PgsqlContext context) : Controller
/// </summary>
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
/// <param name="enabled">true to enable</param>
/// <returns>Nothing</returns>
/// <response code="200"></response>
[HttpPatch("AprilFoolsMode")]
[ProducesResponseType<string>(Status500InternalServerError)]
[ProducesResponseType(Status200OK)]
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
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("ProductVersion", "9.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@ -86,6 +86,9 @@ namespace API.Migrations
.HasMaxLength(64)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("JobId1")
.HasColumnType("character varying(64)");
@ -260,6 +263,9 @@ namespace API.Migrations
.IsRequired()
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired()
.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>
/// Creates full file path of chapter-archive
/// Creates full file path of chapter-archive
/// </summary>
/// <returns>Filepath</returns>
internal string GetArchiveFilePath()
@ -92,6 +92,10 @@ public class Chapter : IComparable<Chapter>
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()
{
string path = GetArchiveFilePath();

View File

@ -20,15 +20,9 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
Chapter c = Chapter ?? context.Chapters.Find(ChapterId)!;
Manga m = c.ParentManga ?? context.Manga.Find(c.ParentMangaId)!;
MangaConnector connector = m.MangaConnector ?? context.MangaConnectors.Find(m.MangaConnectorId)!;
DownloadChapterImages(c, connector, m);
return [];
}
private bool DownloadChapterImages(Chapter chapter, MangaConnector connector, Manga manga)
{
Chapter chapter = Chapter ?? context.Chapters.Find(ChapterId)!;
Manga manga = chapter.ParentManga ?? context.Manga.Find(chapter.ParentMangaId)!;
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
string[] imageUrls = connector.GetChapterImageUrls(chapter);
string saveArchiveFilePath = chapter.GetArchiveFilePath();
@ -52,7 +46,7 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
if (imageUrls.Length == 0)
{
Directory.Delete(tempFolder, true);
return false;
return [];
}
foreach (string imageUrl in imageUrls)
@ -61,10 +55,10 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
bool status = DownloadImage(imageUrl, imagePath);
if (status is false)
return false;
return [];
}
CopyCoverFromCacheToDownloadLocation(manga);
CopyCoverFromCacheToDownloadLocation(context, manga);
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);
Directory.Delete(tempFolder, true); //Cleanup
return true;
chapter.Downloaded = true;
context.SaveChanges();
return [];
}
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
string publicationFolder = manga.CreatePublicationFolder();
@ -107,8 +104,9 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
{
if (retries > 0)
{
manga.SaveCoverImageToCache();
CopyCoverFromCacheToDownloadLocation(manga, --retries);
manga.CoverFileNameInCache = manga.SaveCoverImageToCache();
context.SaveChanges();
CopyCoverFromCacheToDownloadLocation(context, manga, --retries);
}
return;

View File

@ -26,6 +26,7 @@ public abstract class Job
[NotMapped]
public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs);
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)
: 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)]
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)
{
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[] 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>?)? GetMangaFromUrl(string url);

View File

@ -69,18 +69,21 @@ public static class Tranga
Log.Info(TRANGA);
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)
if (job.RecurrenceMs <= 0)
context.Jobs.Remove(job);
else
{
if (job.state >= JobState.Failed)
job.Enabled = false;
else
job.state = JobState.Waiting;
job.LastExecution = DateTime.UtcNow;
job.state = JobState.Waiting;
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();
foreach (Job job in runJobs)
{

View File

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