33 Commits

Author SHA1 Message Date
6687ab4b3b Port Manganato 2025-03-08 19:22:23 +01:00
290324f9d9 Merge branch 'cuttingedge-merge-ServerV2' into postgres-Server-V2
# Conflicts:
#	Tranga/Chapter.cs
#	Tranga/MangaConnectors/Manganato.cs
#	Tranga/MangaConnectors/WeebCentral.cs
2025-03-08 19:04:36 +01:00
b00b0ee030 Merge branch 'master' into cuttingedge-merge-ServerV2
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-08 19:00:42 +01:00
e47c52ad48 Merge pull request #367 from C9Glax/cuttingedge
Cuttingedge merge
2025-03-08 18:58:40 +01:00
ef87e02d0b Add Endpoints to change single requestlimit 2025-03-08 18:46:35 +01:00
0af83f2fd0 Update Swagger Documenatation for 501 NotImplemented 2025-03-08 18:36:57 +01:00
f3854ab594 Implement MangaController/MoveFolder 2025-03-08 18:36:30 +01:00
207604a437 Return JobIds and NotificationConnector-IDs on creation 2025-03-08 18:36:10 +01:00
832ddf1442 Implement MoveFileOrFolderJob.cs 2025-03-08 18:35:41 +01:00
313225a1a1 Migrations 2025-03-08 18:09:54 +01:00
ffc0e7555a Add NewtonsoftJson to Swagger
Add RetrieveChaptersJob.cs
Add UpdateFilesDownloadedJob.cs
Remove DownloadNewChaptersJob.cs and instead use DownloadAvailableChaptersJob.cs
2025-03-08 18:09:41 +01:00
ecfc8f349b Back to working 2025-03-08 16:53:27 +01:00
293f0af8e3 Merge pull request #366 from merlinmarijn/manganato-domain-switch
Some checks are pending
Docker Image CI / build (push) Waiting to run
Manganato connector search fix
2025-03-08 07:40:11 +01:00
merlinmarijn
ebfa34e386 Update Manganato.cs 2025-03-07 22:33:24 +01:00
merlinmarijn
14524407f9 Update Manganato.cs 2025-03-07 22:29:40 +01:00
d56f0b383a Merge pull request #365 from merlinmarijn/manganato-domain-switch
Some checks are pending
Docker Image CI / build (push) Waiting to run
Manganato fix chapter naming format in CBZ files (i am sorry)
2025-03-07 21:54:06 +01:00
merlinmarijn
70391c83c1 Update Manganato.cs
i found out, i am stupid
2025-03-07 21:39:17 +01:00
dc7696ee26 Merge pull request #364 from merlinmarijn/manganato-domain-switch
Some checks are pending
Docker Image CI / build (push) Waiting to run
Enforce correct referrer check for access to Manganato
2025-03-07 20:19:52 +01:00
merlinmarijn
49dab9a670 Referrer policy changed
- Updated: image hosting platform seem to have changed a policy requiring now to send the referrer from the actual site instead of just allowing any connecting regardless of the referrer address
2025-03-07 19:57:27 +01:00
c9bc79fbd5 Update new_connector.yml
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-07 10:19:08 +01:00
83ce315f87 Merge pull request #357 from merlinmarijn/manganato-domain-switch
Some checks are pending
Docker Image CI / build (push) Waiting to run
Update Connector for Manganato connector: Migrate from .com to .gg & Adjust HTML Parsing
#358 @merlinmarijn
2025-03-07 10:06:44 +01:00
merlinmarijn
59511056d0 added try around getting urls 2025-03-03 23:43:35 +01:00
merlinmarijn
ed3ca5dba8 removed leftover comment 2025-03-03 23:04:43 +01:00
merlinmarijn
8df05d7e8a fixed image referrer 2025-03-03 22:59:25 +01:00
merlinmarijn
95d1e37b47 Update Manganato.cs 2025-03-03 22:27:37 +01:00
b6494ab7f9 Merge pull request #354 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.6.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/setup-qemu-action from 3.5.0 to 3.6.0
2025-03-03 13:54:59 +01:00
dependabot[bot]
1d1d01b6e5 Bump docker/setup-qemu-action from 3.5.0 to 3.6.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3.5.0...v3.6.0)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 05:09:43 +00:00
5bb4977876 Merge pull request #353 from ale-ben/cuttingedge
Some checks failed
Docker Image CI / build (push) Has been cancelled
Weebcentral: File name also depends on original chapter name
2025-03-02 16:20:07 +01:00
Alessandro Benetton
c6bb1c9180 [cuttingedge] fix(Chapter): Minor logic change to account for all chapterName cases 2025-03-02 16:06:21 +01:00
Alessandro Benetton
9a066e7ac7 [cuttingedge] fix(Weebcentral): Updated CheckChapterIsDownloaded logic to also consider chapter name if present 2025-03-02 15:57:09 +01:00
Alessandro Benetton
4bafffded4 [cuttingedge] feat(Weebcentra): When ordering chapters, order name desc to put special chapters first 2025-03-02 15:56:12 +01:00
Alessandro Benetton
5a4bc1c6de [cuttingedge] fix(Weebcentral): Handle case of chapter name with multiple number parts 2025-03-01 12:06:51 +01:00
Alessandro Benetton
71f663ca2f [cuttingedge] fix(Weebcentral): File name also depends on original chapter name 2025-03-01 11:39:12 +01:00
32 changed files with 3002 additions and 165 deletions

View File

@@ -12,7 +12,7 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is the Website free to access? label: Is the Website free to access?
description: We can't support pay-to-use sites. description: We can't support pay-to-use sites, or captcha-proxied sites as Cloudflare.
options: options:
- label: The Website is freely accessible. - label: The Website is freely accessible.
required: true required: true

View File

@@ -17,7 +17,7 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage # https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.5.0 uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx # https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx - name: Set up Docker Buildx

View File

@@ -17,7 +17,7 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage # https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.5.0 uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx # https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx - name: Set up Docker Buildx

View File

@@ -17,7 +17,7 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage # https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.5.0 uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx # https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx - name: Set up Docker Buildx

View File

@@ -17,7 +17,7 @@ jobs:
# https://github.com/docker/setup-qemu-action#usage # https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.5.0 uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx # https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx - name: Set up Docker Buildx

View File

@@ -27,6 +27,7 @@
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.920" /> <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="Swashbuckle.AspNetCore.Newtonsoft" Version="7.3.1" />
<PackageReference Include="System.Drawing.Common" Version="9.0.2" /> <PackageReference Include="System.Drawing.Common" Version="9.0.2" />
</ItemGroup> </ItemGroup>

View File

@@ -83,58 +83,114 @@ public class JobController(PgsqlContext context) : Controller
} }
/// <summary> /// <summary>
/// Create a new CreateNewDownloadChapterJob /// Create a new DownloadAvailableChaptersJob
/// </summary> /// </summary>
/// <param name="MangaId">ID of Manga</param> /// <param name="MangaId">ID of Manga</param>
/// <param name="recurrenceTime">How often should we check for new chapters</param> /// <param name="recurrenceTime">How often should we check for new chapters</param>
/// <response code="201">Created new Job</response> /// <response code="201">Job-IDs</response>
/// <response code="404">Could not find Manga with ID</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("NewDownloadChapterJob/{MangaId}")] [HttpPut("DownloadAvailableChaptersJob/{MangaId}")]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewDownloadChapterJob(string MangaId, [FromBody]ulong recurrenceTime) public IActionResult CreateNewDownloadChapterJob(string MangaId, [FromBody]ulong recurrenceTime)
{ {
Job job = new DownloadNewChaptersJob(recurrenceTime, MangaId); if (context.Manga.Find(MangaId) is null)
return AddJob(job); return NotFound();
Job dep = new RetrieveChaptersJob(recurrenceTime, MangaId);
Job job = new DownloadAvailableChaptersJob(recurrenceTime, MangaId, null, [dep.JobId]);
return AddJobs([dep, job]);
} }
/// <summary> /// <summary>
/// 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>
/// <response code="201">Created new Job</response> /// <response code="201">Job-IDs</response>
/// <response code="404">Could not find Chapter with ID</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("DownloadSingleChapterJob/{ChapterId}")] [HttpPut("DownloadSingleChapterJob/{ChapterId}")]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewDownloadChapterJob(string ChapterId) public IActionResult CreateNewDownloadChapterJob(string ChapterId)
{ {
if(context.Chapters.Find(ChapterId) is null)
return NotFound();
Job job = new DownloadSingleChapterJob(ChapterId); Job job = new DownloadSingleChapterJob(ChapterId);
return AddJob(job); return AddJobs([job]);
}
/// <summary>
/// Create a new UpdateFilesDownloadedJob
/// </summary>
/// <param name="MangaId">ID of the Manga</param>
/// <response code="201">Job-IDs</response>
/// <response code="201">Could not find Manga with ID</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateFilesJob/{MangaId}")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateFilesDownloadedJob(string MangaId)
{
if(context.Manga.Find(MangaId) is null)
return NotFound();
Job job = new UpdateFilesDownloadedJob(0, MangaId);
return AddJobs([job]);
}
/// <summary>
/// Create a new UpdateMetadataJob for all Manga
/// </summary>
/// <response code="201">Job-IDs</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateAllFilesJob")]
[ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllFilesDownloadedJob()
{
List<string> ids = context.Manga.Select(m => m.MangaId).ToList();
List<UpdateFilesDownloadedJob> jobs = ids.Select(id => new UpdateFilesDownloadedJob(0, id)).ToList();
try
{
context.Jobs.AddRange(jobs);
context.SaveChanges();
return Created();
}
catch (Exception e)
{
return StatusCode(500, e.Message);
}
} }
/// <summary> /// <summary>
/// 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>
/// <response code="201">Created new Job</response> /// <response code="201">Job-IDs</response>
/// <response code="404">Could not find Manga with ID</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateMetadataJob/{MangaId}")] [HttpPut("UpdateMetadataJob/{MangaId}")]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateMetadataJob(string MangaId) public IActionResult CreateUpdateMetadataJob(string MangaId)
{ {
if(context.Manga.Find(MangaId) is null)
return NotFound();
Job job = new UpdateMetadataJob(0, MangaId); Job job = new UpdateMetadataJob(0, MangaId);
return AddJob(job); return AddJobs([job]);
} }
/// <summary> /// <summary>
/// Create a new UpdateMetadataJob for all Manga /// Create a new UpdateMetadataJob for all Manga
/// </summary> /// </summary>
/// <response code="201">Created new Job</response> /// <response code="201">Job-IDs</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("UpdateMetadataJob")] [HttpPut("UpdateAllMetadataJob")]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string[]>(Status201Created, "application/json")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllMetadataJob() public IActionResult CreateUpdateAllMetadataJob()
{ {
@@ -152,13 +208,13 @@ public class JobController(PgsqlContext context) : Controller
} }
} }
private IActionResult AddJob(Job job) private IActionResult AddJobs(Job[] jobs)
{ {
try try
{ {
context.Jobs.Add(job); context.Jobs.AddRange(jobs);
context.SaveChanges(); context.SaveChanges();
return new CreatedResult(job.JobId, job); return new CreatedResult((string?)null, jobs.Select(j => j.JobId).ToArray());
} }
catch (Exception e) catch (Exception e)
{ {
@@ -276,10 +332,11 @@ public class JobController(PgsqlContext context) : Controller
/// Stops the Job with the requested ID /// Stops the Job with the requested ID
/// </summary> /// </summary>
/// <param name="JobId">Job-ID</param> /// <param name="JobId">Job-ID</param>
/// <remarks>NOT IMPLEMENTED</remarks> /// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
[HttpPost("{JobId}/Stop")] [HttpPost("{JobId}/Stop")]
[ProducesResponseType(Status501NotImplemented)]
public IActionResult StopJob(string JobId) public IActionResult StopJob(string JobId)
{ {
throw new NotImplementedException(); return StatusCode(501);
} }
} }

View File

@@ -1,5 +1,6 @@
using API.APIEndpointRecords; using API.APIEndpointRecords;
using API.Schema; using API.Schema;
using API.Schema.Jobs;
using Asp.Versioning; using Asp.Versioning;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -228,10 +229,28 @@ public class MangaController(PgsqlContext context) : Controller
/// </summary> /// </summary>
/// <param name="MangaId">Manga-ID</param> /// <param name="MangaId">Manga-ID</param>
/// <param name="folder">New Directory-Path</param> /// <param name="folder">New Directory-Path</param>
/// <remarks>NOT IMPLEMENTED</remarks> /// <response code="202">Folder is going to be moved</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MangaId}/MoveFolder")] [HttpPost("{MangaId}/MoveFolder")]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult MoveFolder(string MangaId, [FromBody]string folder) public IActionResult MoveFolder(string MangaId, [FromBody]string folder)
{ {
throw new NotImplementedException(); Manga? manga = context.Manga.Find(MangaId);
if (manga is null)
return NotFound();
MoveFileOrFolderJob dep = manga.UpdateFolderName(TrangaSettings.downloadLocation, folder);
UpdateFilesDownloadedJob up = new UpdateFilesDownloadedJob(0, manga.MangaId, null, [dep.JobId]);
try
{
context.Jobs.AddRange([dep, up]);
context.SaveChanges();
return Accepted();
}
catch (Exception e)
{
return StatusCode(500, e.Message);
}
} }
} }

View File

@@ -50,11 +50,11 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// </summary> /// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks> /// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks>
/// <param name="notificationConnector">Notification-Connector</param> /// <param name="notificationConnector">Notification-Connector</param>
/// <response code="201"></response> /// <response code="201">ID of new connector</response>
/// <response code="409">A NotificationConnector with name already exists</response> /// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut] [HttpPut]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string>(Status201Created, "application/json")]
[ProducesResponseType(Status409Conflict)] [ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector) public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector)
@@ -65,7 +65,7 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
{ {
context.NotificationConnectors.Add(notificationConnector); context.NotificationConnectors.Add(notificationConnector);
context.SaveChanges(); context.SaveChanges();
return Created(); return Created(notificationConnector.Name, notificationConnector);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -77,12 +77,12 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// Creates a new Gotify-Notification-Connector /// Creates a new Gotify-Notification-Connector
/// </summary> /// </summary>
/// <remarks>Priority needs to be between 0 and 10</remarks> /// <remarks>Priority needs to be between 0 and 10</remarks>
/// <response code="201"></response> /// <response code="201">ID of new connector</response>
/// <response code="400"></response> /// <response code="400"></response>
/// <response code="409">A NotificationConnector with name already exists</response> /// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Gotify")] [HttpPut("Gotify")]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string>(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status409Conflict)] [ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
@@ -103,12 +103,12 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// Creates a new Ntfy-Notification-Connector /// Creates a new Ntfy-Notification-Connector
/// </summary> /// </summary>
/// <remarks>Priority needs to be between 1 and 5</remarks> /// <remarks>Priority needs to be between 1 and 5</remarks>
/// <response code="201"></response> /// <response code="201">ID of new connector</response>
/// <response code="400"></response> /// <response code="400"></response>
/// <response code="409">A NotificationConnector with name already exists</response> /// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Ntfy")] [HttpPut("Ntfy")]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string>(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status409Conflict)] [ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
@@ -136,12 +136,12 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
/// Creates a new Lunasea-Notification-Connector /// Creates a new Lunasea-Notification-Connector
/// </summary> /// </summary>
/// <remarks>https://docs.lunasea.app/lunasea/notifications/custom-notifications for id. Either device/:device_id or user/:user_id</remarks> /// <remarks>https://docs.lunasea.app/lunasea/notifications/custom-notifications for id. Either device/:device_id or user/:user_id</remarks>
/// <response code="201"></response> /// <response code="201">ID of new connector</response>
/// <response code="400"></response> /// <response code="400"></response>
/// <response code="409">A NotificationConnector with name already exists</response> /// <response code="409">A NotificationConnector with name already exists</response>
/// <response code="500">Error during Database Operation</response> /// <response code="500">Error during Database Operation</response>
[HttpPut("Lunasea")] [HttpPut("Lunasea")]
[ProducesResponseType(Status201Created)] [ProducesResponseType<string>(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status409Conflict)] [ProducesResponseType(Status409Conflict)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]

View File

@@ -49,8 +49,8 @@ public class SearchController(PgsqlContext context) : Controller
/// <summary> /// <summary>
/// Initiate a search for a Manga on a specific Connector /// Initiate a search for a Manga on a specific Connector
/// </summary> /// </summary>
/// <param name="id">Manga-Connector-ID</param> /// <param name="MangaConnectorName">Manga-Connector-ID</param>
/// <param name="name">Name/Title of the Manga</param> /// <param name="SearchName">Name/Title of the Manga</param>
/// <response code="200"></response> /// <response code="200"></response>
/// <response code="404">MangaConnector with ID not found</response> /// <response code="404">MangaConnector with ID not found</response>
/// <response code="406">MangaConnector with ID is disabled</response> /// <response code="406">MangaConnector with ID is disabled</response>
@@ -60,15 +60,15 @@ public class SearchController(PgsqlContext context) : Controller
[ProducesResponseType(Status404NotFound)] [ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status406NotAcceptable)] [ProducesResponseType(Status406NotAcceptable)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")] [ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public IActionResult SearchManga(string id, string name) public IActionResult SearchManga(string MangaConnectorName, string SearchName)
{ {
MangaConnector? connector = context.MangaConnectors.Find(id); MangaConnector? connector = context.MangaConnectors.Find(MangaConnectorName);
if (connector is null) if (connector is null)
return NotFound(); return NotFound();
else if (connector.Enabled is false) else if (connector.Enabled is false)
return StatusCode(406); 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(SearchName);
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)
{ {
@@ -142,8 +142,8 @@ public class SearchController(PgsqlContext context) : Controller
MangaTag? inDb = context.Tags.FirstOrDefault(t => t.Equals(mt)); MangaTag? inDb = context.Tags.FirstOrDefault(t => t.Equals(mt));
return inDb ?? mt; return inDb ?? mt;
}); });
manga.Tags = mergedTags.ToList(); manga.MangaTags = mergedTags.ToList();
IEnumerable<MangaTag> newTags = manga.Tags.Where(mt => !context.Tags.Any(t => t.Tag.Equals(mt.Tag))); IEnumerable<MangaTag> newTags = manga.MangaTags.Where(mt => !context.Tags.Any(t => t.Tag.Equals(mt.Tag)));
context.Tags.AddRange(newTags); context.Tags.AddRange(newTags);
} }
@@ -195,6 +195,7 @@ public class SearchController(PgsqlContext context) : Controller
context.Manga.Add(manga); context.Manga.Add(manga);
context.Jobs.Add(new DownloadMangaCoverJob(manga.MangaId)); context.Jobs.Add(new DownloadMangaCoverJob(manga.MangaId));
context.Jobs.Add(new RetrieveChaptersJob(0, manga.MangaId));
context.SaveChanges(); context.SaveChanges();
return existing ?? manga; return existing ?? manga;

View File

@@ -72,15 +72,46 @@ public class SettingsController(PgsqlContext context) : Controller
/// <summary> /// <summary>
/// Update all Request-Limits to new values /// Update all Request-Limits to new values
/// </summary> /// </summary>
/// <remarks>NOT IMPLEMENTED</remarks> /// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
[HttpPatch("RequestLimits")] [HttpPatch("RequestLimits")]
[ProducesResponseType(Status501NotImplemented)]
public IActionResult SetRequestLimits() public IActionResult SetRequestLimits()
{ {
throw new NotImplementedException(); return StatusCode(501);
} }
/// <summary> /// <summary>
/// Reset all Request-Limits /// Updates a Request-Limit value
/// </summary>
/// <param name="RequestType">Type of Request</param>
/// <param name="requestLimit">New limit in Requests/Minute</param>
/// <response code="200"></response>
/// <response code="400">Limit needs to be greater than 0</response>
[HttpPatch("RequestLimits/{RequestType}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status400BadRequest)]
public IActionResult SetRequestLimit(RequestType RequestType, [FromBody]int requestLimit)
{
if (requestLimit <= 0)
return BadRequest();
TrangaSettings.UpdateRequestLimit(RequestType, requestLimit);
return Ok();
}
/// <summary>
/// Reset Request-Limit
/// </summary>
/// <response code="200"></response>
[HttpDelete("RequestLimits/{RequestType}")]
[ProducesResponseType<string>(Status200OK)]
public IActionResult ResetRequestLimits(RequestType RequestType)
{
TrangaSettings.UpdateRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
return Ok();
}
/// <summary>
/// Reset Request-Limit
/// </summary> /// </summary>
/// <response code="200"></response> /// <response code="200"></response>
[HttpDelete("RequestLimits")] [HttpDelete("RequestLimits")]

View File

@@ -0,0 +1,673 @@
// <auto-generated />
using System;
using System.Collections.Generic;
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("20250308154843_dev-080325-2")]
partial class dev0803252
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
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.Property<string>("IconUrl")
.IsRequired()
.HasColumnType("text");
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>("Name")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
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.DownloadMangaCoverJob", 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("DownloadMangaCoverJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)4);
});
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.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.DownloadMangaCoverJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
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,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class dev0803252 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AltTitleIds",
table: "Manga");
migrationBuilder.DropColumn(
name: "AuthorIds",
table: "Manga");
migrationBuilder.DropColumn(
name: "LinkIds",
table: "Manga");
migrationBuilder.DropColumn(
name: "TagIds",
table: "Manga");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string[]>(
name: "AltTitleIds",
table: "Manga",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
migrationBuilder.AddColumn<string[]>(
name: "AuthorIds",
table: "Manga",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
migrationBuilder.AddColumn<string[]>(
name: "LinkIds",
table: "Manga",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
migrationBuilder.AddColumn<string[]>(
name: "TagIds",
table: "Manga",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
}
}
}

View File

@@ -0,0 +1,735 @@
// <auto-generated />
using System;
using System.Collections.Generic;
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("20250308162728_dev-080325-3")]
partial class dev0803253
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
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.Property<string>("IconUrl")
.IsRequired()
.HasColumnType("text");
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>("Name")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
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.DownloadAvailableChaptersJob", 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("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", 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)4);
});
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.RetrieveChaptersJob", 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("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", 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("UpdateFilesDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
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.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.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", 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.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
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,126 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class dev0803253 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Manga_DownloadMangaCoverJob_MangaId",
table: "Jobs");
migrationBuilder.RenameColumn(
name: "DownloadMangaCoverJob_MangaId",
table: "Jobs",
newName: "UpdateFilesDownloadedJob_MangaId");
migrationBuilder.RenameIndex(
name: "IX_Jobs_DownloadMangaCoverJob_MangaId",
table: "Jobs",
newName: "IX_Jobs_UpdateFilesDownloadedJob_MangaId");
migrationBuilder.AddColumn<string>(
name: "DownloadAvailableChaptersJob_MangaId",
table: "Jobs",
type: "character varying(64)",
maxLength: 64,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "RetrieveChaptersJob_MangaId",
table: "Jobs",
type: "character varying(64)",
maxLength: 64,
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Jobs_DownloadAvailableChaptersJob_MangaId",
table: "Jobs",
column: "DownloadAvailableChaptersJob_MangaId");
migrationBuilder.CreateIndex(
name: "IX_Jobs_RetrieveChaptersJob_MangaId",
table: "Jobs",
column: "RetrieveChaptersJob_MangaId");
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Manga_DownloadAvailableChaptersJob_MangaId",
table: "Jobs",
column: "DownloadAvailableChaptersJob_MangaId",
principalTable: "Manga",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Manga_RetrieveChaptersJob_MangaId",
table: "Jobs",
column: "RetrieveChaptersJob_MangaId",
principalTable: "Manga",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Manga_UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
column: "UpdateFilesDownloadedJob_MangaId",
principalTable: "Manga",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Manga_DownloadAvailableChaptersJob_MangaId",
table: "Jobs");
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Manga_RetrieveChaptersJob_MangaId",
table: "Jobs");
migrationBuilder.DropForeignKey(
name: "FK_Jobs_Manga_UpdateFilesDownloadedJob_MangaId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_DownloadAvailableChaptersJob_MangaId",
table: "Jobs");
migrationBuilder.DropIndex(
name: "IX_Jobs_RetrieveChaptersJob_MangaId",
table: "Jobs");
migrationBuilder.DropColumn(
name: "DownloadAvailableChaptersJob_MangaId",
table: "Jobs");
migrationBuilder.DropColumn(
name: "RetrieveChaptersJob_MangaId",
table: "Jobs");
migrationBuilder.RenameColumn(
name: "UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
newName: "DownloadMangaCoverJob_MangaId");
migrationBuilder.RenameIndex(
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
table: "Jobs",
newName: "IX_Jobs_DownloadMangaCoverJob_MangaId");
migrationBuilder.AddForeignKey(
name: "FK_Jobs_Manga_DownloadMangaCoverJob_MangaId",
table: "Jobs",
column: "DownloadMangaCoverJob_MangaId",
principalTable: "Manga",
principalColumn: "MangaId",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@@ -0,0 +1,735 @@
// <auto-generated />
using System;
using System.Collections.Generic;
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("20250308170713_dev-080325-4")]
partial class dev0803254
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
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.Property<string>("IconUrl")
.IsRequired()
.HasColumnType("text");
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>("Name")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
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>("MangaTagsTag")
.HasColumnType("text");
b.HasKey("MangaId", "MangaTagsTag");
b.HasIndex("MangaTagsTag");
b.ToTable("MangaMangaTag");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", 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("DownloadAvailableChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", 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)4);
});
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.RetrieveChaptersJob", 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("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", 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("UpdateFilesDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
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.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("MangaTagsTag")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", 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.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
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,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class dev0803254 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MangaMangaTag_Tags_TagsTag",
table: "MangaMangaTag");
migrationBuilder.RenameColumn(
name: "TagsTag",
table: "MangaMangaTag",
newName: "MangaTagsTag");
migrationBuilder.RenameIndex(
name: "IX_MangaMangaTag_TagsTag",
table: "MangaMangaTag",
newName: "IX_MangaMangaTag_MangaTagsTag");
migrationBuilder.AddForeignKey(
name: "FK_MangaMangaTag_Tags_MangaTagsTag",
table: "MangaMangaTag",
column: "MangaTagsTag",
principalTable: "Tags",
principalColumn: "Tag",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MangaMangaTag_Tags_MangaTagsTag",
table: "MangaMangaTag");
migrationBuilder.RenameColumn(
name: "MangaTagsTag",
table: "MangaMangaTag",
newName: "TagsTag");
migrationBuilder.RenameIndex(
name: "IX_MangaMangaTag_MangaTagsTag",
table: "MangaMangaTag",
newName: "IX_MangaMangaTag_TagsTag");
migrationBuilder.AddForeignKey(
name: "FK_MangaMangaTag_Tags_TagsTag",
table: "MangaMangaTag",
column: "TagsTag",
principalTable: "Tags",
principalColumn: "Tag",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@@ -179,14 +179,6 @@ namespace API.Migrations
.HasMaxLength(64) .HasMaxLength(64)
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.PrimitiveCollection<string[]>("AltTitleIds")
.IsRequired()
.HasColumnType("text[]");
b.PrimitiveCollection<string[]>("AuthorIds")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("ConnectorId") b.Property<string>("ConnectorId")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
@@ -210,10 +202,6 @@ namespace API.Migrations
b.Property<float>("IgnoreChapterBefore") b.Property<float>("IgnoreChapterBefore")
.HasColumnType("real"); .HasColumnType("real");
b.PrimitiveCollection<string[]>("LinkIds")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("MangaConnectorId") b.Property<string>("MangaConnectorId")
.IsRequired() .IsRequired()
.HasColumnType("character varying(32)"); .HasColumnType("character varying(32)");
@@ -228,10 +216,6 @@ namespace API.Migrations
b.Property<byte>("ReleaseStatus") b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint"); .HasColumnType("smallint");
b.PrimitiveCollection<string[]>("TagIds")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("WebsiteUrl") b.Property<string>("WebsiteUrl")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
@@ -383,17 +367,17 @@ namespace API.Migrations
b.Property<string>("MangaId") b.Property<string>("MangaId")
.HasColumnType("character varying(64)"); .HasColumnType("character varying(64)");
b.Property<string>("TagsTag") b.Property<string>("MangaTagsTag")
.HasColumnType("text"); .HasColumnType("text");
b.HasKey("MangaId", "TagsTag"); b.HasKey("MangaId", "MangaTagsTag");
b.HasIndex("TagsTag"); b.HasIndex("MangaTagsTag");
b.ToTable("MangaMangaTag"); b.ToTable("MangaMangaTag");
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@@ -407,13 +391,13 @@ namespace API.Migrations
b.ToTable("Jobs", t => b.ToTable("Jobs", t =>
{ {
t.Property("MangaId") t.Property("MangaId")
.HasColumnName("DownloadMangaCoverJob_MangaId"); .HasColumnName("DownloadAvailableChaptersJob_MangaId");
}); });
b.HasDiscriminator().HasValue((byte)4); b.HasDiscriminator().HasValue((byte)1);
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@@ -424,7 +408,7 @@ namespace API.Migrations
b.HasIndex("MangaId"); b.HasIndex("MangaId");
b.HasDiscriminator().HasValue((byte)1); b.HasDiscriminator().HasValue((byte)4);
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
@@ -456,6 +440,46 @@ namespace API.Migrations
b.HasDiscriminator().HasValue((byte)3); b.HasDiscriminator().HasValue((byte)3);
}); });
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", 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("RetrieveChaptersJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)5);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", 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("UpdateFilesDownloadedJob_MangaId");
});
b.HasDiscriminator().HasValue((byte)6);
});
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{ {
b.HasBaseType("API.Schema.Jobs.Job"); b.HasBaseType("API.Schema.Jobs.Job");
@@ -620,12 +644,12 @@ namespace API.Migrations
b.HasOne("API.Schema.MangaTag", null) b.HasOne("API.Schema.MangaTag", null)
.WithMany() .WithMany()
.HasForeignKey("TagsTag") .HasForeignKey("MangaTagsTag")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
.WithMany() .WithMany()
@@ -636,7 +660,7 @@ namespace API.Migrations
b.Navigation("Manga"); b.Navigation("Manga");
}); });
modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")
.WithMany() .WithMany()
@@ -658,6 +682,28 @@ namespace API.Migrations
b.Navigation("Chapter"); b.Navigation("Chapter");
}); });
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateFilesDownloadedJob", b =>
{
b.HasOne("API.Schema.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
});
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
{ {
b.HasOne("API.Schema.Manga", "Manga") b.HasOne("API.Schema.Manga", "Manga")

View File

@@ -8,6 +8,7 @@ using Asp.Versioning;
using Asp.Versioning.Builder; using Asp.Versioning.Builder;
using Asp.Versioning.Conventions; using Asp.Versioning.Conventions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -25,7 +26,7 @@ builder.Services.AddCors(options =>
}); });
builder.Services.AddApiVersioning(option => builder.Services.AddApiVersioning(option =>
{ {
option.AssumeDefaultVersionWhenUnspecified = true; option.AssumeDefaultVersionWhenUnspecified = true;
option.DefaultApiVersion = new ApiVersion(2); option.DefaultApiVersion = new ApiVersion(2);
option.ReportApiVersions = true; option.ReportApiVersions = true;
@@ -34,16 +35,18 @@ builder.Services.AddApiVersioning(option =>
new QueryStringApiVersionReader("api-version"), new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("X-Version"), new HeaderApiVersionReader("X-Version"),
new MediaTypeApiVersionReader("x-version")); new MediaTypeApiVersionReader("x-version"));
}) })
.AddMvc(options => .AddMvc(options =>
{ {
options.Conventions.Add(new VersionByNamespaceConvention()); options.Conventions.Add(new VersionByNamespaceConvention());
}) })
.AddApiExplorer(options => { .AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'V"; options.GroupNameFormat = "'v'V";
options.SubstituteApiVersionInUrl = true; options.SubstituteApiVersionInUrl = true;
}); });
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGenNewtonsoftSupport();
builder.Services.AddSwaggerGen(opt => builder.Services.AddSwaggerGen(opt =>
{ {
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
@@ -58,12 +61,13 @@ builder.Services.AddDbContext<PgsqlContext>(options =>
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")??"postgres"}")); $"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")??"postgres"}"));
builder.Services.AddControllers(options => builder.Services.AddControllers(options =>
{ {
options.AllowEmptyInputInBodyModelBinding = true; options.AllowEmptyInputInBodyModelBinding = true;
}) });
.AddNewtonsoftJson(opts => builder.Services.AddControllers().AddNewtonsoftJson(opts =>
{ {
opts.SerializerSettings.Converters.Add(new StringEnumConverter()); opts.SerializerSettings.Converters.Add(new StringEnumConverter());
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
}); });
builder.WebHost.UseUrls("http://*:6531"); builder.WebHost.UseUrls("http://*:6531");
@@ -110,14 +114,13 @@ using (var scope = app.Services.CreateScope())
new MangaKatana(), new MangaKatana(),
new Mangaworld(), new Mangaworld(),
new ManhuaPlus(), new ManhuaPlus(),
new Weebcentral() new Weebcentral(),
new Manganato()
]; ];
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray(); MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
context.MangaConnectors.AddRange(newConnectors); context.MangaConnectors.AddRange(newConnectors);
IQueryable<string> updateMetadataJobMangaIds = context.Jobs.Where(j => j.JobType == JobType.UpdateMetaDataJob).Select(j => ((UpdateMetadataJob)j).MangaId); context.Jobs.AddRange(context.Manga.AsEnumerable().Select(m => new UpdateFilesDownloadedJob(0, m.MangaId)));
Job[] newUpdateMetadataJobs = context.Manga.Where(m => !updateMetadataJobMangaIds.Contains(m.MangaId)).ToList().Select(m => new UpdateMetadataJob(0, m.MangaId)).ToArray();
context.Jobs.AddRange(newUpdateMetadataJobs);
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1)); context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));

View File

@@ -39,8 +39,7 @@ public class Chapter : IComparable<Chapter>
public bool Downloaded { get; internal set; } = false; public bool Downloaded { get; internal set; } = false;
public string ParentMangaId { get; internal set; } public string ParentMangaId { get; internal set; }
[JsonIgnore] [JsonIgnore] public Manga? ParentManga { get; init; }
public Manga? ParentManga { get; init; }
public int CompareTo(Chapter? other) public int CompareTo(Chapter? other)
{ {
@@ -130,7 +129,7 @@ public class Chapter : IComparable<Chapter>
internal string GetComicInfoXmlString() internal string GetComicInfoXmlString()
{ {
XElement comicInfo = new("ComicInfo", XElement comicInfo = new("ComicInfo",
new XElement("Tags", string.Join(',', ParentManga.Tags.Select(tag => tag.Tag))), new XElement("Tags", string.Join(',', ParentManga.MangaTags.Select(tag => tag.Tag))),
new XElement("LanguageISO", ParentManga.OriginalLanguage), new XElement("LanguageISO", ParentManga.OriginalLanguage),
new XElement("Title", Title), new XElement("Title", Title),
new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))), new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))),

View File

@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class DownloadAvailableChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{
[MaxLength(64)]
public string MangaId { get; init; } = mangaId;
[JsonIgnore]
public Manga? Manga { get; init; }
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
return context.Chapters.Where(c => c.ParentMangaId == MangaId).AsEnumerable()
.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId));
}
}

View File

@@ -4,8 +4,10 @@
public enum JobType : byte public enum JobType : byte
{ {
DownloadSingleChapterJob = 0, DownloadSingleChapterJob = 0,
DownloadNewChaptersJob = 1, DownloadAvailableChaptersJob = 1,
UpdateMetaDataJob = 2, UpdateMetaDataJob = 2,
MoveFileOrFolderJob = 3, MoveFileOrFolderJob = 3,
DownloadMangaCoverJob = 4 DownloadMangaCoverJob = 4,
RetrieveChaptersJob = 5,
UpdateFilesDownloadedJob = 6
} }

View File

@@ -8,6 +8,33 @@ public class MoveFileOrFolderJob(string fromLocation, string toLocation, string?
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
throw new NotImplementedException(); try
{
FileInfo fi = new FileInfo(FromLocation);
if (!fi.Exists)
return [];
if (File.Exists(ToLocation))//Do not override existing
return [];
if(fi.Attributes.HasFlag(FileAttributes.Directory))
MoveDirectory(fi, ToLocation);
else
MoveFile(fi, ToLocation);
}
catch (Exception e)
{
}
return [];
}
private void MoveDirectory(FileInfo from, string toLocation)
{
Directory.Move(from.FullName, toLocation);
}
private void MoveFile(FileInfo from, string toLocation)
{
File.Move(from.FullName, toLocation);
} }
} }

View File

@@ -1,11 +1,11 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Schema.MangaConnectors; using API.Schema.MangaConnectors;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace API.Schema.Jobs; namespace API.Schema.Jobs;
public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null) public class RetrieveChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(DownloadNewChaptersJob)), JobType.DownloadNewChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds) : Job(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{ {
[MaxLength(64)] [MaxLength(64)]
public string MangaId { get; init; } = mangaId; public string MangaId { get; init; } = mangaId;
@@ -31,6 +31,6 @@ public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string?
context.Chapters.AddRangeAsync(newChapters).Wait(); context.Chapters.AddRangeAsync(newChapters).Wait();
context.SaveChangesAsync().Wait(); context.SaveChangesAsync().Wait();
return allNewChapters.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId)); return [];
} }
} }

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace API.Schema.Jobs;
public class UpdateFilesDownloadedJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
: Job(TokenGen.CreateToken(typeof(UpdateFilesDownloadedJob)), JobType.UpdateFilesDownloadedJob, recurrenceMs, parentJobId, dependsOnJobsIds)
{
[MaxLength(64)]
public string MangaId { get; init; } = mangaId;
[JsonIgnore]
public virtual Manga? Manga { get; init; }
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{
IQueryable<Chapter> chapters = context.Chapters.Where(c => c.ParentMangaId == MangaId);
foreach (Chapter chapter in chapters)
chapter.Downloaded = chapter.IsDownloaded();
context.SaveChanges();
return [];
}
}

View File

@@ -21,14 +21,6 @@ public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? paren
/// <param name="context"></param> /// <param name="context"></param>
protected override IEnumerable<Job> RunInternal(PgsqlContext context) protected override IEnumerable<Job> RunInternal(PgsqlContext context)
{ {
//Manga manga = Manga ?? context.Manga.Find(MangaId)!; return [];//TODO
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

@@ -1,5 +1,5 @@
using System.Collections;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Net; using System.Net;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -32,39 +32,31 @@ public class Manga
public float IgnoreChapterBefore { get; internal set; } public float IgnoreChapterBefore { get; internal set; }
public string MangaConnectorId { get; private set; } public string MangaConnectorId { get; private set; }
[JsonIgnore] [JsonIgnore] public MangaConnector? MangaConnector { get; private set; }
public MangaConnector? MangaConnector { get; private set; }
public ICollection<string> AuthorIds { get; internal set; } [JsonIgnore] public ICollection<Author>? Authors { get; internal set; }
[JsonIgnore] [NotMapped] public IEnumerable<string> AuthorIds => Authors?.Select(a => a.AuthorId) ?? [];
public ICollection<Author>? Authors { get; internal set; }
public ICollection<string> TagIds { get; internal set; } [JsonIgnore] public ICollection<MangaTag>? MangaTags { get; internal set; }
[JsonIgnore] [NotMapped] public IEnumerable<string> Tags => MangaTags.Select(t => t.Tag);
public ICollection<MangaTag>? Tags { get; internal set; }
public ICollection<string> LinkIds { get; internal set; }
[JsonIgnore]
public ICollection<Link>? Links { get; internal set; }
public ICollection<string> AltTitleIds { get; internal set; } [JsonIgnore] public ICollection<Link>? Links { get; internal set; }
[JsonIgnore] [NotMapped] public IEnumerable<string> LinkIds => Links?.Select(l => l.LinkId) ?? [];
public ICollection<MangaAltTitle>? AltTitles { get; internal set; }
[JsonIgnore] public ICollection<MangaAltTitle>? AltTitles { get; internal set; }
[NotMapped] public IEnumerable<string> AltTitleIds => AltTitles?.Select(a => a.AltTitleId) ?? [];
public Manga(string connectorId, string name, string description, string websiteUrl, string coverUrl, public Manga(string connectorId, string name, string description, string websiteUrl, string coverUrl,
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus, string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
float ignoreChapterBefore, MangaConnector mangaConnector, ICollection<Author> authors, float ignoreChapterBefore, MangaConnector mangaConnector, ICollection<Author> authors,
ICollection<MangaTag> tags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles) ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles)
: this(connectorId, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage, : this(connectorId, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage,
releaseStatus, ignoreChapterBefore, mangaConnector.Name) releaseStatus, ignoreChapterBefore, mangaConnector.Name)
{ {
this.AuthorIds = authors.Select(a => a.AuthorId) as ICollection<string>;
this.Authors = authors; this.Authors = authors;
this.TagIds = tags.Select(t => t.Tag) as ICollection<string>; this.MangaTags = mangaTags;
this.Tags = tags;
this.LinkIds = links.Select(l => l.LinkId) as ICollection<string>;
this.Links = links; this.Links = links;
this.AltTitleIds = altTitles.Select(t => t.AltTitleId) as ICollection<string>;
this.AltTitles = altTitles; this.AltTitles = altTitles;
} }
@@ -103,7 +95,7 @@ public class Manga
this.OriginalLanguage = other.OriginalLanguage; this.OriginalLanguage = other.OriginalLanguage;
this.Authors = other.Authors; this.Authors = other.Authors;
this.Links = other.Links; this.Links = other.Links;
this.Tags = other.Tags; this.MangaTags = other.MangaTags;
this.AltTitles = other.AltTitles; this.AltTitles = other.AltTitles;
this.ReleaseStatus = other.ReleaseStatus; this.ReleaseStatus = other.ReleaseStatus;
} }
@@ -127,7 +119,7 @@ public class Manga
if (File.Exists(saveImagePath)) if (File.Exists(saveImagePath))
return saveImagePath; return saveImagePath;
RequestResult coverResult = new HttpDownloadClient().MakeRequest(CoverUrl, RequestType.MangaCover); RequestResult coverResult = new HttpDownloadClient().MakeRequest(CoverUrl, RequestType.MangaCover, this.WebsiteUrl);
if (coverResult.statusCode is < HttpStatusCode.Accepted or >= HttpStatusCode.Ambiguous) if (coverResult.statusCode is < HttpStatusCode.Accepted or >= HttpStatusCode.Ambiguous)
return SaveCoverImageToCache(--retries); return SaveCoverImageToCache(--retries);

View File

@@ -0,0 +1,219 @@
using System.Globalization;
using System.Net;
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using HtmlAgilityPack;
namespace API.Schema.MangaConnectors;
public class Manganato : MangaConnector
{
public Manganato() : base("Manganato", ["en"],
["natomanga.com", "manganato.gg", "mangakakalot.gg", "nelomanga.com"],
"https://www.manganato.gg/images/favicon-manganato.webp")
{
this.downloadClient = new HttpDownloadClient();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(
string publicationTitle = "")
{
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0))
.ToLower();
string requestUrl = $"https://manganato.gg/search/story/{sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return [];
if (requestResult.htmlDocument is null)
return [];
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications =
ParsePublicationsFromHtml(requestResult.htmlDocument);
return publications;
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(
HtmlDocument document)
{
List<HtmlNode> searchResults =
document.DocumentNode.Descendants("div").Where(n => n.HasClass("story_item")).ToList();
List<string> urls = new();
foreach (HtmlNode mangaResult in searchResults)
{
try
{
urls.Add(mangaResult.Descendants("h3").First(n => n.HasClass("story_name"))
.Descendants("a").First().GetAttributeValue("href", ""));
}
catch
{
//failed to get a url, send it to the void
}
}
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
foreach (string url in urls)
{
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl(url);
if (manga is { } m)
ret.Add(m);
}
return ret.ToArray();
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(
string publicationId)
{
return GetMangaFromUrl($"https://chapmanganato.com/{publicationId}");
}
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)?
GetMangaFromUrl(string url)
{
RequestResult requestResult =
downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
if (requestResult.htmlDocument is null)
return null;
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
}
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(
HtmlDocument document, string publicationId, string websiteUrl)
{
Dictionary<string, string> altTitles = new();
List<MangaTag> tags = new();
List<Author> authors = new();
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
HtmlNode infoNode = document.DocumentNode.Descendants("ul").First(d => d.HasClass("manga-info-text"));
string sortName = infoNode.Descendants("h1").First().InnerText;
foreach (HtmlNode li in infoNode.Descendants("li"))
{
string text = li.InnerText.Trim().ToLower();
if (text.StartsWith("author(s) :"))
{
authors = li.Descendants("a").Select(a => a.InnerText.Trim()).Select(a => new Author(a)).ToList();
}
else if (text.StartsWith("status :"))
{
string status = text.Replace("status :", "").Trim().ToLower();
if (string.IsNullOrWhiteSpace(status))
releaseStatus = MangaReleaseStatus.Continuing;
else if (status == "ongoing")
releaseStatus = MangaReleaseStatus.Continuing;
else
releaseStatus = Enum.Parse<MangaReleaseStatus>(status, true);
}
else if (li.HasClass("genres"))
{
tags = li.Descendants("a").Select(a => new MangaTag(a.InnerText.Trim())).ToList();
}
}
string posterUrl = document.DocumentNode.Descendants("div").First(s => s.HasClass("manga-info-pic"))
.Descendants("img").First()
.GetAttributes().First(a => a.Name == "src").Value;
string description = document.DocumentNode.SelectSingleNode("//div[@id='contentBox']")
.InnerText.Replace("Description :", "");
while (description.StartsWith('\n'))
description = description.Substring(1);
string pattern = "MMM-dd-yyyy HH:mm";
HtmlNode? oldestChapter = document.DocumentNode
.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),' row ')]/span[@title]").MaxBy(
node => DateTime.ParseExact(node.GetAttributeValue("title", "Dec-31-2400 23:59"), pattern,
CultureInfo.InvariantCulture).Millisecond);
uint year = Convert.ToUInt32(DateTime.ParseExact(
oldestChapter?.GetAttributeValue("title", "Dec 31 2400, 23:59") ?? "Dec 31 2400, 23:59", pattern,
CultureInfo.InvariantCulture).Year);
Manga manga = new(publicationId, sortName, description, websiteUrl, posterUrl, null, year, null, releaseStatus,
-1, this, authors, tags, [], []);
return (manga, authors, tags, [], []);
}
public override Chapter[] GetChapters(Manga manga, string language = "en")
{
string requestUrl = manga.WebsiteUrl;
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>();
//Return Chapters ordered by Chapter-Number
if (requestResult.htmlDocument is null)
return Array.Empty<Chapter>();
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
return chapters.Order().ToArray();
}
internal override string[] GetChapterImageUrls(Chapter chapter)
{
string requestUrl = chapter.Url;
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
requestResult.htmlDocument is null)
return [];
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
return imageUrls;
}
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
{
List<Chapter> ret = new();
HtmlNode chapterList = document.DocumentNode.Descendants("div").First(l => l.HasClass("chapter-list"));
Regex volRex = new(@"Vol\.([0-9]+).*");
Regex chapterRex = new(@"https:\/\/chapmanganato.[A-z]+\/manga-[A-z0-9]+\/chapter-([0-9\.]+)");
Regex nameRex = new(@"Chapter ([0-9]+(\.[0-9]+)*){1}:? (.*)");
foreach (HtmlNode chapterInfo in chapterList.Descendants("div").Where(x => x.HasClass("row")))
{
string url = chapterInfo.Descendants("a").First().GetAttributeValue("href", "");
var name = chapterInfo.Descendants("a").First().InnerText.Trim();
string chapterName = nameRex.Match(name).Groups[3].Value;
string chapterNumber = Regex.Match(name, @"Chapter ([0-9]+(\.[0-9]+)*)").Groups[1].Value;
string? volumeNumber = Regex.Match(chapterName, @"Vol\.([0-9]+)").Groups[1].Value;
if (string.IsNullOrWhiteSpace(volumeNumber))
volumeNumber = "0";
try
{
ret.Add(new Chapter(manga, url, chapterNumber, int.Parse(volumeNumber), chapterName));
}
catch (Exception e)
{
}
}
ret.Reverse();
return ret;
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
{
List<string> ret = new();
HtmlNode imageContainer =
document.DocumentNode.Descendants("div").First(i => i.HasClass("container-chapter-reader"));
foreach (HtmlNode imageNode in imageContainer.Descendants("img"))
ret.Add(imageNode.GetAttributeValue("src", ""));
return ret.ToArray();
}
}

View File

@@ -40,17 +40,19 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
modelBuilder.Entity<Job>() modelBuilder.Entity<Job>()
.HasDiscriminator<JobType>(j => j.JobType) .HasDiscriminator<JobType>(j => j.JobType)
.HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob) .HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob)
.HasValue<DownloadNewChaptersJob>(JobType.DownloadNewChaptersJob) .HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob)
.HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob) .HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
.HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob) .HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
.HasValue<UpdateMetadataJob>(JobType.UpdateMetaDataJob); .HasValue<UpdateMetadataJob>(JobType.UpdateMetaDataJob)
.HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
.HasValue<UpdateFilesDownloadedJob>(JobType.UpdateFilesDownloadedJob);
modelBuilder.Entity<Job>() modelBuilder.Entity<Job>()
.HasOne<Job>(j => j.ParentJob) .HasOne<Job>(j => j.ParentJob)
.WithMany() .WithMany()
.HasForeignKey(j => j.ParentJobId); .HasForeignKey(j => j.ParentJobId);
modelBuilder.Entity<Job>() modelBuilder.Entity<Job>()
.HasMany<Job>(j => j.DependsOnJobs); .HasMany<Job>(j => j.DependsOnJobs);
modelBuilder.Entity<DownloadNewChaptersJob>() modelBuilder.Entity<DownloadAvailableChaptersJob>()
.Navigation(dncj => dncj.Manga) .Navigation(dncj => dncj.Manga)
.AutoInclude(); .AutoInclude();
modelBuilder.Entity<DownloadSingleChapterJob>() modelBuilder.Entity<DownloadSingleChapterJob>()
@@ -74,10 +76,10 @@ public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(op
.Navigation(m => m.Authors) .Navigation(m => m.Authors)
.AutoInclude(); .AutoInclude();
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.HasMany<MangaTag>(m => m.Tags) .HasMany<MangaTag>(m => m.MangaTags)
.WithMany(); .WithMany();
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.Navigation(m => m.Tags) .Navigation(m => m.MangaTags)
.AutoInclude(); .AutoInclude();
modelBuilder.Entity<Manga>() modelBuilder.Entity<Manga>()
.HasMany<Link>(m => m.Links) .HasMany<Link>(m => m.Links)

View File

@@ -91,10 +91,10 @@ public static class Tranga
// If the job is already running, skip it // If the job is already running, skip it
if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue; if (RunningJobs.Values.Any(j => j.JobId == job.JobId)) continue;
if (job is DownloadNewChaptersJob dncj) if (job is DownloadAvailableChaptersJob dncj)
{ {
if (RunningJobs.Values.Any(j => if (RunningJobs.Values.Any(j =>
j is DownloadNewChaptersJob rdncj && j is DownloadAvailableChaptersJob rdncj &&
rdncj.Manga?.MangaConnector == dncj.Manga?.MangaConnector)) rdncj.Manga?.MangaConnector == dncj.Manga?.MangaConnector))
{ {
continue; continue;
@@ -145,11 +145,13 @@ public static class Tranga
ret = ret.Concat(jobsByType[JobType.MoveFileOrFolderJob]); ret = ret.Concat(jobsByType[JobType.MoveFileOrFolderJob]);
if(jobsByType.ContainsKey(JobType.DownloadMangaCoverJob)) if(jobsByType.ContainsKey(JobType.DownloadMangaCoverJob))
ret = ret.Concat(jobsByType[JobType.DownloadMangaCoverJob]); ret = ret.Concat(jobsByType[JobType.DownloadMangaCoverJob]);
if(jobsByType.ContainsKey(JobType.UpdateFilesDownloadedJob))
ret = ret.Concat(jobsByType[JobType.UpdateFilesDownloadedJob]);
Dictionary<MangaConnector, List<Job>> metadataJobsByConnector = new(); Dictionary<MangaConnector, List<Job>> metadataJobsByConnector = new();
if (jobsByType.ContainsKey(JobType.DownloadNewChaptersJob)) if (jobsByType.ContainsKey(JobType.DownloadAvailableChaptersJob))
{ {
foreach (DownloadNewChaptersJob job in jobsByType[JobType.DownloadNewChaptersJob]) foreach (DownloadAvailableChaptersJob job in jobsByType[JobType.DownloadAvailableChaptersJob])
{ {
Manga manga = job.Manga ?? context.Manga.Find(job.MangaId)!; Manga manga = job.Manga ?? context.Manga.Find(job.MangaId)!;
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!; MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
@@ -167,6 +169,16 @@ public static class Tranga
metadataJobsByConnector[connector].Add(job); metadataJobsByConnector[connector].Add(job);
} }
} }
if (jobsByType.ContainsKey(JobType.RetrieveChaptersJob))
{
foreach (RetrieveChaptersJob job in jobsByType[JobType.RetrieveChaptersJob])
{
Manga manga = job.Manga ?? context.Manga.Find(job.MangaId)!;
MangaConnector connector = manga.MangaConnector ?? context.MangaConnectors.Find(manga.MangaConnectorId)!;
if(!metadataJobsByConnector.TryAdd(connector, [job]))
metadataJobsByConnector[connector].Add(job);
}
}
foreach (List<Job> metadataJobs in metadataJobsByConnector.Values) foreach (List<Job> metadataJobs in metadataJobsByConnector.Values)
ret = ret.Append(metadataJobs.MinBy(j => j.NextExecution))!; ret = ret.Append(metadataJobs.MinBy(j => j.NextExecution))!;

View File

@@ -111,7 +111,7 @@ public static class TrangaSettings
ExportSettings(); ExportSettings();
} }
public static void UpdateRateLimit(RequestType requestType, int newLimit) public static void UpdateRequestLimit(RequestType requestType, int newLimit)
{ {
requestLimits[requestType] = newLimit; requestLimits[requestType] = newLimit;
ExportSettings(); ExportSettings();

View File

@@ -44,24 +44,24 @@
Tranga can download Chapters and Metadata from "Scanlation" sites such as Tranga can download Chapters and Metadata from "Scanlation" sites such as
- [MangaDex.org](https://mangadex.org/) (Multilingual) - [MangaDex.org](https://mangadex.org/) (Multilingual)
- [Manganato.com](https://manganato.com/) (en) - [Manganato.gg](https://manganato.com/) (en)
- [MangaKatana.com](https://mangakatana.com) (en) - [MangaKatana.com](https://mangakatana.com) (en)
- [Mangaworld.bz](https://www.mangaworld.bz/) (it) - [Mangaworld.bz](https://www.mangaworld.bz/) (it)
- [Bato.to](https://bato.to/v3x) (en) - [Bato.to](https://bato.to/v3x) (en)
- [ManhuaPlus](https://manhuaplus.org/) (en) - [ManhuaPlus](https://manhuaplus.org/) (en)
- [MangaHere](https://www.mangahere.cc/) (en) (Their covers aren't scrapeable.) - [MangaHere](https://www.mangahere.cc/) (en) (Their covers aren't scrapeable.)
- [Weebcentral](https://weebcentral.com) (en) - [Weebcentral](https://weebcentral.com) (en)
- [Webtoons](https://www.webtoons.com/en/) - [Webtoons](https://www.webtoons.com/en/) (en)
- ❓ Open an [issue](https://github.com/C9Glax/tranga/issues/new?assignees=&labels=New+Connector&projects=&template=new_connector.yml&title=%5BNew+Connector%5D%3A+) - ❓ Open an [issue](https://github.com/C9Glax/tranga/issues/new?assignees=&labels=New+Connector&projects=&template=new_connector.yml&title=%5BNew+Connector%5D%3A+)
and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/). and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/).
Notifications can be sent to your devices using [Gotify](https://gotify.net/), [LunaSea](https://www.lunasea.app/) or [Ntfy](https://ntfy.sh/ Notifications can be sent to your devices using [Gotify](https://gotify.net/), [LunaSea](https://www.lunasea.app/) or [Ntfy](https://ntfy.sh/
). ), or any other service that can use REST Webhooks.
### What this does and doesn't do ### What this does and doesn't do
Tranga (this git-repo) will open a port (standard 6531) and listen for requests to add Jobs to Monitor and/or download specific Manga. Tranga (this git-repo) will open a port (standard 6531) and listen for requests to add Jobs to Monitor and/or download specific Manga.
The configuration is all done through HTTP-Requests. [Documentation](docs/API_Calls_v2.md) The configuration is all done through HTTP-Requests.
_**For a web-frontend use [tranga-website](https://github.com/C9Glax/tranga-website).**_ _**For a web-frontend use [tranga-website](https://github.com/C9Glax/tranga-website).**_
@@ -91,7 +91,6 @@ That is why I wanted to create my own project, in a language I understand, and t
- [Html Agility Pack (HAP)](https://html-agility-pack.net/) - [Html Agility Pack (HAP)](https://html-agility-pack.net/)
- [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch) - [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch)
- [Sixlabors.ImageSharp](https://docs-v2.sixlabors.com/articles/imagesharp/index.html#license) - [Sixlabors.ImageSharp](https://docs-v2.sixlabors.com/articles/imagesharp/index.html#license)
- [zstd-wrapper](https://github.com/oleg-st/ZstdSharp) [zstd](https://github.com/facebook/zstd)
- 💙 Blåhaj 🦈 - 💙 Blåhaj 🦈
<p align="right">(<a href="#readme-top">back to top</a>)</p> <p align="right">(<a href="#readme-top">back to top</a>)</p>
@@ -120,10 +119,7 @@ access the folder.
### Prerequisites ### Prerequisites
#### To Build .NET-9.0
[.NET-Core 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
#### To Run
[.NET-Core 8.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) scroll down a bit, should be on the right the second item.
See the [open issues](https://github.com/C9Glax/tranga/issues) for a full list of proposed features (and known issues). See the [open issues](https://github.com/C9Glax/tranga/issues) for a full list of proposed features (and known issues).