mirror of
https://github.com/C9Glax/tranga.git
synced 2025-06-13 06:47:54 +02:00
Compare commits
292 Commits
be6b3da1be
...
postgres-S
Author | SHA1 | Date | |
---|---|---|---|
aa67c11050 | |||
7b38d0aa2b | |||
64e31fad54 | |||
49a70e2341 | |||
9659f2a68a | |||
d474868116 | |||
b1312c4164 | |||
33856f9927 | |||
02ab3d8cae | |||
7974c58fd5 | |||
503d9dfb5f | |||
62b035f6c5 | |||
40c70fbf19 | |||
49bd66ccab | |||
9b251169a5 | |||
aa29c45094 | |||
bd60fda05a | |||
8ecbdb91b2 | |||
cb1c68f295 | |||
421a25ec31 | |||
2d122a918f | |||
100cb06ba0 | |||
6125b036bf | |||
3fe3fc09b0 | |||
96d5b09391 | |||
84aecda916 | |||
0803a92a66 | |||
7f55aaf85d | |||
3853e2daa2 | |||
852fbf5ae8 | |||
4e7a725fee | |||
698d138642 | |||
8efb60652b | |||
fe60b98cb8 | |||
63442e9af6 | |||
703e32a30e | |||
4ddfe4a54c | |||
fb2b4d6920 | |||
496b49ccb3 | |||
b3efcf19d9 | |||
0903ec606b | |||
6cfa29e3dd | |||
0519ed26de | |||
aacdb72d6a | |||
3283dd7290 | |||
937c5cb7a7 | |||
225b7f02ad | |||
6258e07f20 | |||
622198a09e | |||
49b382fe1f | |||
5a6dc5a5b2 | |||
4bc70eca68 | |||
63fee081e6 | |||
e45b72dcf9 | |||
021ad5e804 | |||
8e0c964883 | |||
d6e945741a | |||
3a3306240f | |||
110dd36166 | |||
065cac62af | |||
563afa1e6f | |||
be2adff57d | |||
adc7ee606e | |||
a764f381c9 | |||
590ccdd09a | |||
0f0a49f74f | |||
a1a5028858 | |||
1792952039 | |||
9e62eb53cb | |||
f3c4b012b0 | |||
7e1c65b470 | |||
4247ae7740 | |||
a5954ed5c8 | |||
d08544b892 | |||
f6f86deb7f | |||
16f5817a31 | |||
d5d9f44a5f | |||
83bc3b418b | |||
205f0a1629 | |||
a1c2942208 | |||
4b4e24c6a0 | |||
475a29b10d | |||
694b88d200 | |||
0f6c060026 | |||
b49b11828c | |||
2d69b30e83 | |||
53d9be5656 | |||
7d4a6be569 | |||
7477f4d04d | |||
30a8162777 | |||
57baad3d2c | |||
3c5f51e495 | |||
397d3c93df | |||
1b49b171f4 | |||
ec5d048df5 | |||
a490e233d7 | |||
f085c5cf8e | |||
31beeeffae | |||
99a3f2614d | |||
15ced9aed8 | |||
64b17aea7a | |||
c696c38983 | |||
dbbac1ad59 | |||
b955d41530 | |||
91e033a2ec | |||
4dd31dfe18 | |||
66fcdca7e7 | |||
9350de0ae9 | |||
c94c55300c | |||
721f932fac | |||
f691529591 | |||
d75262a8f3 | |||
9521f66bac | |||
3981a41303 | |||
f2961711cf | |||
93ad691971 | |||
cef3b24efd | |||
f10c478cab | |||
01ba927491 | |||
90ce1395b8 | |||
892ef6c9d6 | |||
31c039d71e | |||
5b03befbf1 | |||
5012bbb2eb | |||
45a8f7a038 | |||
9b4baa1334 | |||
657ab571f9 | |||
1480aa0a03 | |||
232fe6406a | |||
a2bc14d54a | |||
d278a25f16 | |||
5f42a2d5ae | |||
60d84d1186 | |||
7ee4d32c07 | |||
0e68d64f75 | |||
434e30dc47 | |||
19ff3f578a | |||
cc03b6fa9c | |||
1f59ef66cd | |||
d7f21550cd | |||
d93c8fdb94 | |||
80320fd44d | |||
6449132dbf | |||
0df3381c8c | |||
2ca43e6f5d | |||
3ba1261f31 | |||
be72d4ba97 | |||
f237d82cac | |||
a43901564b | |||
34ec185125 | |||
cd08d9d78b | |||
339f40b61b | |||
8ee8f33f33 | |||
6721bd863f | |||
ee820cfa27 | |||
24361d5a43 | |||
6687ab4b3b | |||
290324f9d9 | |||
89ed500751 | |||
b00b0ee030 | |||
e47c52ad48 | |||
ef87e02d0b | |||
0af83f2fd0 | |||
f3854ab594 | |||
207604a437 | |||
832ddf1442 | |||
313225a1a1 | |||
ffc0e7555a | |||
ecfc8f349b | |||
94678e744f | |||
73eb02e7cb | |||
c3ebd6acac | |||
a694b9f5ab | |||
b24d2e12fc | |||
6909c367e5 | |||
293f0af8e3 | |||
ebfa34e386 | |||
14524407f9 | |||
d56f0b383a | |||
70391c83c1 | |||
dc7696ee26 | |||
49dab9a670 | |||
4cb48dd1b4 | |||
d43ae881b5 | |||
3583e45071 | |||
c4adba6357 | |||
ed74975312 | |||
043f0b9593 | |||
022ebe2bcc | |||
3a8b400851 | |||
6c5bc3685e | |||
c679d7c677 | |||
4075adfe6b | |||
72abc90af3 | |||
894f105786 | |||
d314559361 | |||
5ab0bd0b78 | |||
5c0ace291b | |||
0d931bc835 | |||
e1e5a45960 | |||
3ecbc1a805 | |||
3305519307 | |||
9fca2d81ab | |||
949c0cc16d | |||
5921e524a9 | |||
bf332717a5 | |||
bb31a94eea | |||
c9bc79fbd5 | |||
83ce315f87 | |||
59511056d0 | |||
ed3ca5dba8 | |||
8df05d7e8a | |||
95d1e37b47 | |||
1d2ca4d76a | |||
e2ff2c76ed | |||
8a0829ef69 | |||
d257095885 | |||
68cc23e158 | |||
b6494ab7f9 | |||
1d1d01b6e5 | |||
5bb4977876 | |||
c6bb1c9180 | |||
9a066e7ac7 | |||
4bafffded4 | |||
235183cd7f | |||
942b43da67 | |||
ce5538b352 | |||
0cfdf17bd4 | |||
0c48c1e020 | |||
0638e75ed6 | |||
5a4bc1c6de | |||
71f663ca2f | |||
1b61a16061 | |||
db81fdce39 | |||
fdb5451162 | |||
6b7632b071 | |||
06c080dfce | |||
8130e11a9c | |||
659a42d370 | |||
9cef068785 | |||
4ad3149523 | |||
dccc9fdbef | |||
e6d40a7b36 | |||
a95cb90561 | |||
603e1b41d9 | |||
bb8a514830 | |||
9928abb674 | |||
ebb034e0c7 | |||
edacaaba8a | |||
d97da26994 | |||
8b923d73c4 | |||
1dca7ec569 | |||
7229fad6c5 | |||
814efd3528 | |||
2cd5d8bc4f | |||
5a864ab9b7 | |||
663e2e2ca0 | |||
c700974693 | |||
553b5558d3 | |||
c9bbfee26b | |||
6e869eeb0d | |||
be7da69dbd | |||
7f13d9b1e6 | |||
0c9e3205c2 | |||
6315940cd6 | |||
ef7ebf022d | |||
725813c2f3 | |||
a69e12179b | |||
45ca2695eb | |||
bd9e79d026 | |||
6bbd09072b | |||
d6018b60ae | |||
8c3b70b32e | |||
4f7031ecfc | |||
a713a006dd | |||
ebe7e145aa | |||
f7a285aabd | |||
786482398c | |||
7921dcb1cb | |||
d0c9313279 | |||
58cf4cf4e0 | |||
280d715a7c | |||
b4edcccafe | |||
1701881f4b | |||
e5be5703f8 | |||
ce217aae4f | |||
3abf7224d0 | |||
b39dbd5671 | |||
375fad0c21 | |||
ee0d17c24f | |||
36ab3c3fdb | |||
c3d60c6586 |
4
.github/ISSUE_TEMPLATE/new_connector.yml
vendored
4
.github/ISSUE_TEMPLATE/new_connector.yml
vendored
@ -12,7 +12,7 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
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:
|
||||
- label: The Website is freely accessible.
|
||||
required: true
|
||||
@ -20,4 +20,4 @@ body:
|
||||
attributes:
|
||||
label: Anything else?
|
||||
validations:
|
||||
required: false
|
||||
required: false
|
||||
|
@ -17,12 +17,12 @@ jobs:
|
||||
|
||||
# https://github.com/docker/setup-qemu-action#usage
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
uses: docker/setup-qemu-action@v3.6.0
|
||||
|
||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@v3.10.0
|
||||
|
||||
# https://github.com/docker/login-action#docker-hub
|
||||
- name: Login to Docker Hub
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
# https://github.com/docker/build-push-action#multi-platform-image
|
||||
- name: Build and push API
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
|
45
.github/workflows/docker-image-dev.yml
vendored
45
.github/workflows/docker-image-dev.yml
vendored
@ -1,45 +0,0 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "dev" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/docker/setup-qemu-action#usage
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
|
||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
# https://github.com/docker/login-action#docker-hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# https://github.com/docker/build-push-action#multi-platform-image
|
||||
- name: Build and push API
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
|
||||
platforms: linux/amd64,linux/arm64
|
||||
pull: true
|
||||
push: true
|
||||
tags: |
|
||||
glax/tranga-api:dev
|
6
.github/workflows/docker-image-master.yml
vendored
6
.github/workflows/docker-image-master.yml
vendored
@ -17,12 +17,12 @@ jobs:
|
||||
|
||||
# https://github.com/docker/setup-qemu-action#usage
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
uses: docker/setup-qemu-action@v3.6.0
|
||||
|
||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@v3.10.0
|
||||
|
||||
# https://github.com/docker/login-action#docker-hub
|
||||
- name: Login to Docker Hub
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
# https://github.com/docker/build-push-action#multi-platform-image
|
||||
- name: Build and push API
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
|
8
.github/workflows/docker-image-serverv2.yml
vendored
8
.github/workflows/docker-image-serverv2.yml
vendored
@ -2,7 +2,7 @@ name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "Server-V2" ]
|
||||
branches: [ "postgres-Server-V2" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@ -17,12 +17,12 @@ jobs:
|
||||
|
||||
# https://github.com/docker/setup-qemu-action#usage
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
uses: docker/setup-qemu-action@v3.6.0
|
||||
|
||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@v3.10.0
|
||||
|
||||
# https://github.com/docker/login-action#docker-hub
|
||||
- name: Login to Docker Hub
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
# https://github.com/docker/build-push-action#multi-platform-image
|
||||
- name: Build and push API
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v6.15.0
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
|
@ -11,23 +11,25 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.71" />
|
||||
<PackageReference Include="log4net" Version="3.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
|
||||
<PackageReference Include="log4net" Version="3.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.5" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||
<PackageReference Include="PuppeteerSharp" Version="20.0.5" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.697" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.0" />
|
||||
<PackageReference Include="PuppeteerSharp" Version="20.1.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.929" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="8.1.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -0,0 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record DownloadAvailableChaptersJobRecord([Required]string language, [Required]ulong recurrenceTimeMs, [Required]string localLibraryId);
|
16
API/APIEndpointRecords/GotifyRecord.cs
Normal file
16
API/APIEndpointRecords/GotifyRecord.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record GotifyRecord(string endpoint, string appToken, int priority)
|
||||
{
|
||||
public bool Validate()
|
||||
{
|
||||
if (endpoint == string.Empty)
|
||||
return false;
|
||||
if (appToken == string.Empty)
|
||||
return false;
|
||||
if (priority < 0 || priority > 10)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
3
API/APIEndpointRecords/ModifyJobRecord.cs
Normal file
3
API/APIEndpointRecords/ModifyJobRecord.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record ModifyJobRecord(ulong? RecurrenceMs, bool? Enabled);
|
13
API/APIEndpointRecords/NewLibraryRecord.cs
Normal file
13
API/APIEndpointRecords/NewLibraryRecord.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record NewLibraryRecord(string path, string name)
|
||||
{
|
||||
public bool Validate()
|
||||
{
|
||||
if (path.Length < 1) //TODO Better Path validation
|
||||
return false;
|
||||
if (name.Length < 1)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
17
API/APIEndpointRecords/NtfyRecord.cs
Normal file
17
API/APIEndpointRecords/NtfyRecord.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record NtfyRecord(string endpoint, string username, string password, string topic, int priority)
|
||||
{
|
||||
public bool Validate()
|
||||
{
|
||||
if (endpoint == string.Empty)
|
||||
return false;
|
||||
if (username == string.Empty)
|
||||
return false;
|
||||
if (password == string.Empty)
|
||||
return false;
|
||||
if (priority < 1 || priority > 5)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
13
API/APIEndpointRecords/PushoverRecord.cs
Normal file
13
API/APIEndpointRecords/PushoverRecord.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace API.APIEndpointRecords;
|
||||
|
||||
public record PushoverRecord(string apptoken, string user)
|
||||
{
|
||||
public bool Validate()
|
||||
{
|
||||
if (apptoken == string.Empty)
|
||||
return false;
|
||||
if (user == string.Empty)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,23 +1,27 @@
|
||||
using API.Schema;
|
||||
using API.APIEndpointRecords;
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.Jobs;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("v{version:apiVersion}/[controller]")]
|
||||
public class JobController(PgsqlContext context) : Controller
|
||||
public class JobController(PgsqlContext context, ILog Log) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all Jobs
|
||||
/// </summary>
|
||||
/// <returns>Array of Jobs</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<Job[]>(Status200OK)]
|
||||
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetAllJobs()
|
||||
{
|
||||
Job[] ret = context.Jobs.ToArray();
|
||||
@ -28,9 +32,9 @@ public class JobController(PgsqlContext context) : Controller
|
||||
/// Returns Jobs with requested Job-IDs
|
||||
/// </summary>
|
||||
/// <param name="ids">Array of Job-IDs</param>
|
||||
/// <returns>Array of Jobs</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpPost("WithIDs")]
|
||||
[ProducesResponseType<Job[]>(Status200OK)]
|
||||
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetJobs([FromBody]string[] ids)
|
||||
{
|
||||
Job[] ret = context.Jobs.Where(job => ids.Contains(job.JobId)).ToArray();
|
||||
@ -40,40 +44,55 @@ public class JobController(PgsqlContext context) : Controller
|
||||
/// <summary>
|
||||
/// Get all Jobs in requested State
|
||||
/// </summary>
|
||||
/// <param name="state">Requested Job-State</param>
|
||||
/// <returns>Array of Jobs</returns>
|
||||
[HttpGet("State/{state}")]
|
||||
[ProducesResponseType<Job[]>(Status200OK)]
|
||||
public IActionResult GetJobsInState(JobState state)
|
||||
/// <param name="JobState">Requested Job-State</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("State/{JobState}")]
|
||||
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetJobsInState(JobState JobState)
|
||||
{
|
||||
Job[] jobsInState = context.Jobs.Where(job => job.state == state).ToArray();
|
||||
Job[] jobsInState = context.Jobs.Where(job => job.state == JobState).ToArray();
|
||||
return Ok(jobsInState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Jobs of requested Type
|
||||
/// </summary>
|
||||
/// <param name="type">Requested Job-Type</param>
|
||||
/// <returns>Array of Jobs</returns>
|
||||
[HttpGet("Type/{type}")]
|
||||
[ProducesResponseType<Job[]>(Status200OK)]
|
||||
public IActionResult GetJobsOfType(JobType type)
|
||||
/// <param name="JobType">Requested Job-Type</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("Type/{JobType}")]
|
||||
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetJobsOfType(JobType JobType)
|
||||
{
|
||||
Job[] jobsOfType = context.Jobs.Where(job => job.JobType == type).ToArray();
|
||||
Job[] jobsOfType = context.Jobs.Where(job => job.JobType == JobType).ToArray();
|
||||
return Ok(jobsOfType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Jobs of requested Type and State
|
||||
/// </summary>
|
||||
/// <param name="JobType">Requested Job-Type</param>
|
||||
/// <param name="JobState">Requested Job-State</param>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("TypeAndState/{JobType}/{JobState}")]
|
||||
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetJobsOfType(JobType JobType, JobState JobState)
|
||||
{
|
||||
Job[] jobsOfType = context.Jobs.Where(job => job.JobType == JobType && job.state == JobState).ToArray();
|
||||
return Ok(jobsOfType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return Job with ID
|
||||
/// </summary>
|
||||
/// <param name="id">Job-ID</param>
|
||||
/// <returns>Job</returns>
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType<Job>(Status200OK)]
|
||||
/// <param name="JobId">Job-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Job with ID could not be found</response>
|
||||
[HttpGet("{JobId}")]
|
||||
[ProducesResponseType<Job>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetJob(string id)
|
||||
public IActionResult GetJob(string JobId)
|
||||
{
|
||||
Job? ret = context.Jobs.Find(id);
|
||||
Job? ret = context.Jobs.Find(JobId);
|
||||
return (ret is not null) switch
|
||||
{
|
||||
true => Ok(ret),
|
||||
@ -82,58 +101,98 @@ public class JobController(PgsqlContext context) : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new CreateNewDownloadChapterJob
|
||||
/// Create a new DownloadAvailableChaptersJob
|
||||
/// </summary>
|
||||
/// <param name="request">ID of the Manga, and how often we check again</param>
|
||||
/// <returns>Nothing</returns>
|
||||
[HttpPut("NewDownloadChapterJob/{mangaId}")]
|
||||
[ProducesResponseType(Status201Created)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
public IActionResult CreateNewDownloadChapterJob(string mangaId, [FromBody]ulong recurrenceTime)
|
||||
/// <param name="MangaId">ID of Manga</param>
|
||||
/// <param name="record">Job-Configuration</param>
|
||||
/// <response code="201">Job-IDs</response>
|
||||
/// <response code="400">Could not find ToLibrary with ID</response>
|
||||
/// <response code="404">Could not find Manga with ID</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("DownloadAvailableChaptersJob/{MangaId}")]
|
||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateDownloadAvailableChaptersJob(string MangaId, [FromBody]DownloadAvailableChaptersJobRecord record)
|
||||
{
|
||||
Job job = new DownloadNewChaptersJob(recurrenceTime, mangaId);
|
||||
return AddJob(job);
|
||||
if (context.Mangas.Find(MangaId) is not { } m)
|
||||
return NotFound();
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
LocalLibrary? l = context.LocalLibraries.Find(record.localLibraryId);
|
||||
if (l is null)
|
||||
return BadRequest();
|
||||
m.Library = l;
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
Job retrieveChapters = new RetrieveChaptersJob(m, record.language, record.recurrenceTimeMs);
|
||||
Job updateFilesDownloaded =
|
||||
new UpdateChaptersDownloadedJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
|
||||
Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters, updateFilesDownloaded]);
|
||||
Job UpdateCover = new UpdateCoverJob(m, record.recurrenceTimeMs, downloadChapters);
|
||||
retrieveChapters.ParentJob = downloadChapters;
|
||||
updateFilesDownloaded.ParentJob = retrieveChapters;
|
||||
return AddJobs([retrieveChapters, downloadChapters, updateFilesDownloaded, UpdateCover]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DownloadSingleChapterJob
|
||||
/// </summary>
|
||||
/// <param name="chapterId">ID of the Chapter</param>
|
||||
/// <returns>Nothing</returns>
|
||||
[HttpPut("DownloadSingleChapterJob/{chapterId}")]
|
||||
[ProducesResponseType(Status201Created)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
public IActionResult CreateNewDownloadChapterJob(string chapterId)
|
||||
/// <param name="ChapterId">ID of the Chapter</param>
|
||||
/// <response code="201">Job-IDs</response>
|
||||
/// <response code="404">Could not find Chapter with ID</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("DownloadSingleChapterJob/{ChapterId}")]
|
||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateNewDownloadChapterJob(string ChapterId)
|
||||
{
|
||||
Job job = new DownloadSingleChapterJob(chapterId);
|
||||
return AddJob(job);
|
||||
if(context.Chapters.Find(ChapterId) is not { } c)
|
||||
return NotFound();
|
||||
Job job = new DownloadSingleChapterJob(c);
|
||||
return AddJobs([job]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new UpdateMetadataJob
|
||||
/// Create a new UpdateChaptersDownloadedJob
|
||||
/// </summary>
|
||||
/// <param name="mangaId">ID of the Manga</param>
|
||||
/// <returns>Nothing</returns>
|
||||
[HttpPut("UpdateMetadataJob/{mangaId}")]
|
||||
[ProducesResponseType(Status201Created)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
public IActionResult CreateUpdateMetadataJob(string mangaId)
|
||||
/// <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)
|
||||
{
|
||||
Job job = new UpdateMetadataJob(0, mangaId);
|
||||
return AddJob(job);
|
||||
if(context.Mangas.Find(MangaId) is not { } m)
|
||||
return NotFound();
|
||||
Job job = new UpdateChaptersDownloadedJob(m, 0);
|
||||
return AddJobs([job]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new UpdateMetadataJob for all Manga
|
||||
/// </summary>
|
||||
/// <returns>Nothing</returns>
|
||||
[HttpPut("UpdateMetadataJob")]
|
||||
[ProducesResponseType(Status201Created)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
public IActionResult CreateUpdateAllMetadataJob()
|
||||
/// <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<UpdateMetadataJob> jobs = ids.Select(id => new UpdateMetadataJob(0, id)).ToList();
|
||||
List<UpdateChaptersDownloadedJob> jobs = context.Mangas.Select(m => new UpdateChaptersDownloadedJob(m, 0, null, null)).ToList();
|
||||
try
|
||||
{
|
||||
context.Jobs.AddRange(jobs);
|
||||
@ -142,49 +201,124 @@ public class JobController(PgsqlContext context) : Controller
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult AddJob(Job job)
|
||||
{
|
||||
try
|
||||
{
|
||||
context.Jobs.Add(job);
|
||||
context.SaveChanges();
|
||||
return Created();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete Job with ID
|
||||
/// Not Implemented: Create a new UpdateMetadataJob
|
||||
/// </summary>
|
||||
/// <param name="id">Job-ID</param>
|
||||
/// <returns>Nothing</returns>
|
||||
[HttpDelete("{id}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
/// <param name="MangaId">ID of the Manga</param>
|
||||
/// <response code="201">Job-IDs</response>
|
||||
/// <response code="404">Could not find Manga with ID</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("UpdateMetadataJob/{MangaId}")]
|
||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public IActionResult DeleteJob(string id)
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateUpdateMetadataJob(string MangaId)
|
||||
{
|
||||
return StatusCode(Status501NotImplemented);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not Implemented: Create a new UpdateMetadataJob for all Manga
|
||||
/// </summary>
|
||||
/// <response code="201">Job-IDs</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("UpdateAllMetadataJob")]
|
||||
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateUpdateAllMetadataJob()
|
||||
{
|
||||
return StatusCode(Status501NotImplemented);
|
||||
}
|
||||
|
||||
private IActionResult AddJobs(Job[] jobs)
|
||||
{
|
||||
try
|
||||
{
|
||||
Job? ret = context.Jobs.Find(id);
|
||||
switch (ret is not null)
|
||||
{
|
||||
case true:
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
case false: return NotFound();
|
||||
}
|
||||
context.Jobs.AddRange(jobs);
|
||||
context.SaveChanges();
|
||||
return new CreatedResult((string?)null, jobs.Select(j => j.JobId).ToArray());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete Job with ID and all children
|
||||
/// </summary>
|
||||
/// <param name="JobId">Job-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Job could not be found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{JobId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult DeleteJob(string JobId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(context.Jobs.Find(JobId) is not { } ret)
|
||||
return NotFound();
|
||||
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private IQueryable<Job> GetChildJobs(string parentJobId)
|
||||
{
|
||||
IQueryable<Job> children = context.Jobs.Where(j => j.ParentJobId == parentJobId);
|
||||
foreach (Job child in children)
|
||||
foreach (Job grandChild in GetChildJobs(child.JobId))
|
||||
children.Append(grandChild);
|
||||
return children;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modify Job with ID
|
||||
/// </summary>
|
||||
/// <param name="JobId">Job-ID</param>
|
||||
/// <param name="modifyJobRecord">Fields to modify, set to null to keep previous value</param>
|
||||
/// <response code="202">Job modified</response>
|
||||
/// <response code="400">Malformed request</response>
|
||||
/// <response code="404">Job with ID not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{JobId}")]
|
||||
[ProducesResponseType<Job>(Status202Accepted, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult ModifyJob(string JobId, [FromBody]ModifyJobRecord modifyJobRecord)
|
||||
{
|
||||
try
|
||||
{
|
||||
Job? ret = context.Jobs.Find(JobId);
|
||||
if(ret is null)
|
||||
return NotFound();
|
||||
|
||||
ret.RecurrenceMs = modifyJobRecord.RecurrenceMs ?? ret.RecurrenceMs;
|
||||
ret.Enabled = modifyJobRecord.Enabled ?? ret.Enabled;
|
||||
|
||||
context.SaveChanges();
|
||||
return new AcceptedResult(ret.JobId, ret);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
@ -192,32 +326,52 @@ public class JobController(PgsqlContext context) : Controller
|
||||
/// <summary>
|
||||
/// Starts the Job with the requested ID
|
||||
/// </summary>
|
||||
/// <param name="id">Job-ID</param>
|
||||
/// <returns>Nothing</returns>
|
||||
[HttpPost("{id}/Start")]
|
||||
/// <param name="JobId">Job-ID</param>
|
||||
/// <param name="startDependencies">Start Jobs necessary for execution</param>
|
||||
/// <response code="202">Job started</response>
|
||||
/// <response code="404">Job with ID not found</response>
|
||||
/// <response code="409">Job was already running</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("{JobId}/Start")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public IActionResult StartJob(string id)
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult StartJob(string JobId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]bool startDependencies = false)
|
||||
{
|
||||
Job? ret = context.Jobs.Find(id);
|
||||
Job? ret = context.Jobs.Find(JobId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
List<Job> dependencies = startDependencies ? ret.GetDependenciesAndSelf() : [ret];
|
||||
|
||||
try
|
||||
{
|
||||
context.Update(ret);
|
||||
if(dependencies.Any(d => d.state >= JobState.Running && d.state < JobState.Completed))
|
||||
return new ConflictResult();
|
||||
dependencies.ForEach(d =>
|
||||
{
|
||||
d.LastExecution = DateTime.UnixEpoch;
|
||||
d.state = JobState.CompletedWaiting;
|
||||
});
|
||||
context.SaveChanges();
|
||||
return Accepted();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id}/Stop")]
|
||||
public IActionResult StopJob(string id)
|
||||
/// <summary>
|
||||
/// Stops the Job with the requested ID
|
||||
/// </summary>
|
||||
/// <param name="JobId">Job-ID</param>
|
||||
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
|
||||
[HttpPost("{JobId}/Stop")]
|
||||
[ProducesResponseType(Status501NotImplemented)]
|
||||
public IActionResult StopJob(string JobId)
|
||||
{
|
||||
return NotFound(new ProblemResponse("Not implemented")); //TODO
|
||||
return StatusCode(Status501NotImplemented);
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.LibraryConnectors;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
|
||||
@ -8,16 +10,15 @@ namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class LibraryConnectorController(PgsqlContext context) : Controller
|
||||
public class LibraryConnectorController(LibraryContext context, ILog Log) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all configured Library-Connectors
|
||||
/// Gets all configured ToLibrary-Connectors
|
||||
/// </summary>
|
||||
/// <returns>Array of configured Library-Connectors</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<LibraryConnector[]>(Status200OK)]
|
||||
[ProducesResponseType<LibraryConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetAllConnectors()
|
||||
{
|
||||
LibraryConnector[] connectors = context.LibraryConnectors.ToArray();
|
||||
@ -25,16 +26,17 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Library-Connector with requested ID
|
||||
/// Returns ToLibrary-Connector with requested ID
|
||||
/// </summary>
|
||||
/// <param name="id">Library-Connector-ID</param>
|
||||
/// <returns>Library-Connector</returns>
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType<LibraryConnector>(Status200OK)]
|
||||
/// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Connector with ID not found.</response>
|
||||
[HttpGet("{LibraryControllerId}")]
|
||||
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetConnector(string id)
|
||||
public IActionResult GetConnector(string LibraryControllerId)
|
||||
{
|
||||
LibraryConnector? ret = context.LibraryConnectors.Find(id);
|
||||
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
|
||||
return (ret is not null) switch
|
||||
{
|
||||
true => Ok(ret),
|
||||
@ -43,13 +45,14 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Library-Connector
|
||||
/// Creates a new ToLibrary-Connector
|
||||
/// </summary>
|
||||
/// <param name="libraryConnector">Library-Connector</param>
|
||||
/// <returns>Nothing</returns>
|
||||
/// <param name="libraryConnector">ToLibrary-Connector</param>
|
||||
/// <response code="201"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
[ProducesResponseType(Status201Created)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector)
|
||||
{
|
||||
try
|
||||
@ -60,35 +63,37 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the Library-Connector with the requested ID
|
||||
/// Deletes the ToLibrary-Connector with the requested ID
|
||||
/// </summary>
|
||||
/// <param name="id">Library-Connector-ID</param>
|
||||
/// <returns>Nothing</returns>
|
||||
[HttpDelete("{id}")]
|
||||
/// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Connector with ID not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{LibraryControllerId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public IActionResult DeleteConnector(string id)
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult DeleteConnector(string LibraryControllerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
LibraryConnector? ret = context.LibraryConnectors.Find(id);
|
||||
switch (ret is not null)
|
||||
{
|
||||
case true:
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
case false: return NotFound();
|
||||
}
|
||||
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
164
API/Controllers/LocalLibrariesController.cs
Normal file
164
API/Controllers/LocalLibrariesController.cs
Normal file
@ -0,0 +1,164 @@
|
||||
using API.APIEndpointRecords;
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class LocalLibrariesController(PgsqlContext context, ILog Log) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType<LocalLibrary[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetLocalLibraries()
|
||||
{
|
||||
return Ok(context.LocalLibraries);
|
||||
}
|
||||
|
||||
[HttpGet("{LibraryId}")]
|
||||
[ProducesResponseType<LocalLibrary>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetLocalLibrary(string LibraryId)
|
||||
{
|
||||
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
|
||||
if (library is null)
|
||||
return NotFound();
|
||||
return Ok(library);
|
||||
}
|
||||
|
||||
[HttpPatch("{LibraryId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult UpdateLocalLibrary(string LibraryId, [FromBody]NewLibraryRecord record)
|
||||
{
|
||||
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
|
||||
if (library is null)
|
||||
return NotFound();
|
||||
if (record.Validate() == false)
|
||||
return BadRequest();
|
||||
|
||||
try
|
||||
{
|
||||
library.LibraryName = record.name;
|
||||
library.BasePath = record.path;
|
||||
context.SaveChanges();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("{LibraryId}/ChangeBasePath")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult ChangeLibraryBasePath(string LibraryId, [FromBody] string newBasePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
|
||||
if (library is null)
|
||||
return NotFound();
|
||||
|
||||
if (false) //TODO implement path check
|
||||
return BadRequest();
|
||||
|
||||
library.BasePath = newBasePath;
|
||||
context.SaveChanges();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("{LibraryId}/ChangeName")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult ChangeLibraryName(string LibraryId, [FromBody] string newName)
|
||||
{
|
||||
try
|
||||
{
|
||||
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
|
||||
if (library is null)
|
||||
return NotFound();
|
||||
|
||||
if(newName.Length < 1)
|
||||
return BadRequest();
|
||||
|
||||
library.LibraryName = newName;
|
||||
context.SaveChanges();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[ProducesResponseType<LocalLibrary>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateNewLibrary([FromBody]NewLibraryRecord library)
|
||||
{
|
||||
if (library.Validate() == false)
|
||||
return BadRequest();
|
||||
try
|
||||
{
|
||||
LocalLibrary newLibrary = new (library.path, library.name);
|
||||
context.LocalLibraries.Add(newLibrary);
|
||||
context.SaveChanges();
|
||||
|
||||
return Ok(newLibrary);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{LibraryId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult DeleteLocalLibrary(string LibraryId)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
LocalLibrary? library = context.LocalLibraries.Find(LibraryId);
|
||||
if (library is null)
|
||||
return NotFound();
|
||||
context.Remove(library);
|
||||
context.SaveChanges();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
107
API/Controllers/MangaConnectorController.cs
Normal file
107
API/Controllers/MangaConnectorController.cs
Normal file
@ -0,0 +1,107 @@
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.MangaConnectors;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class MangaConnectorController(PgsqlContext context, ILog Log) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all available Connectors (Scanlation-Sites)
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetConnectors()
|
||||
{
|
||||
MangaConnector[] connectors = context.MangaConnectors.ToArray();
|
||||
return Ok(connectors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the MangaConnector with the requested Name
|
||||
/// </summary>
|
||||
/// <param name="MangaConnectorName"></param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Connector with ID not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpGet("{MangaConnectorName}")]
|
||||
[ProducesResponseType<MangaConnector>(Status200OK, "application/json")]
|
||||
public IActionResult GetConnector(string MangaConnectorName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
|
||||
return NotFound();
|
||||
|
||||
return Ok(connector);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all enabled Connectors (Scanlation-Sites)
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("enabled")]
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetEnabledConnectors()
|
||||
{
|
||||
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == true).ToArray();
|
||||
return Ok(connectors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all disabled Connectors (Scanlation-Sites)
|
||||
/// </summary>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("disabled")]
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetDisabledConnectors()
|
||||
{
|
||||
MangaConnector[] connectors = context.MangaConnectors.Where(c => c.Enabled == false).ToArray();
|
||||
return Ok(connectors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enabled or disables a Connector
|
||||
/// </summary>
|
||||
/// <param name="MangaConnectorName">ID of the connector</param>
|
||||
/// <param name="enabled">Set true to enable</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Connector with ID not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{MangaConnectorName}/SetEnabled/{enabled}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult SetEnabled(string MangaConnectorName, bool enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
MangaConnector? connector = context.MangaConnectors.Find(MangaConnectorName);
|
||||
if (connector is null)
|
||||
return NotFound();
|
||||
|
||||
connector.Enabled = enabled;
|
||||
context.SaveChanges();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +1,32 @@
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.Jobs;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class MangaController(PgsqlContext context) : Controller
|
||||
public class MangaController(PgsqlContext context, ILog Log) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all cached Manga
|
||||
/// </summary>
|
||||
/// <returns>Array of Manga</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<Manga[]>(Status200OK)]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetAllManga()
|
||||
{
|
||||
Manga[] ret = context.Manga.ToArray();
|
||||
Manga[] ret = context.Mangas.ToArray();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
@ -27,137 +34,319 @@ public class MangaController(PgsqlContext context) : Controller
|
||||
/// Returns all cached Manga with IDs
|
||||
/// </summary>
|
||||
/// <param name="ids">Array of Manga-IDs</param>
|
||||
/// <returns>Array of Manga</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpPost("WithIDs")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK)]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetManga([FromBody]string[] ids)
|
||||
{
|
||||
Manga[] ret = context.Manga.Where(m => ids.Contains(m.MangaId)).ToArray();
|
||||
Manga[] ret = context.Mangas.Where(m => ids.Contains(m.MangaId)).ToArray();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return Manga with ID
|
||||
/// </summary>
|
||||
/// <param name="id">Manga-ID</param>
|
||||
/// <returns>Manga</returns>
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType<Manga>(Status200OK)]
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Manga with ID not found</response>
|
||||
[HttpGet("{MangaId}")]
|
||||
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetManga(string id)
|
||||
public IActionResult GetManga(string MangaId)
|
||||
{
|
||||
Manga? ret = context.Manga.Find(id);
|
||||
return (ret is not null) switch
|
||||
{
|
||||
true => Ok(ret),
|
||||
false => NotFound()
|
||||
};
|
||||
Manga? ret = context.Mangas.Find(MangaId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete Manga with ID
|
||||
/// </summary>
|
||||
/// <param name="id">Manga-ID</param>
|
||||
/// <returns>Nothing</returns>
|
||||
[HttpDelete("{id}")]
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Manga with ID not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{MangaId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public IActionResult DeleteManga(string id)
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult DeleteManga(string MangaId)
|
||||
{
|
||||
try
|
||||
{
|
||||
Manga? ret = context.Manga.Find(id);
|
||||
switch (ret is not null)
|
||||
{
|
||||
case true:
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
case false: return NotFound();
|
||||
}
|
||||
Manga? ret = context.Mangas.Find(MangaId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns URL of Cover of Manga
|
||||
/// Returns Cover of Manga
|
||||
/// </summary>
|
||||
/// <param name="id">Manga-ID</param>
|
||||
/// <returns>URL of Cover</returns>
|
||||
[HttpGet("{id}/Cover")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
public IActionResult GetCover(string id)
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="width">If width is provided, height needs to also be provided</param>
|
||||
/// <param name="height">If height is provided, width needs to also be provided</param>
|
||||
/// <response code="200">JPEG Image</response>
|
||||
/// <response code="204">Cover not loaded</response>
|
||||
/// <response code="400">The formatting-request was invalid</response>
|
||||
/// <response code="404">Manga with ID not found</response>
|
||||
/// <response code="503">Retry later, downloading cover</response>
|
||||
[HttpGet("{MangaId}/Cover")]
|
||||
[ProducesResponseType<byte[]>(Status200OK,"image/jpeg")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||
public IActionResult GetCover(string MangaId, [FromQuery]int? width, [FromQuery]int? height)
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
if(context.Mangas.Find(MangaId) is not { } m)
|
||||
return NotFound();
|
||||
|
||||
if (!System.IO.File.Exists(m.CoverFileNameInCache))
|
||||
{
|
||||
List<Job> coverDownloadJobs = context.Jobs.Where(j => j.JobType == JobType.DownloadMangaCoverJob).ToList();
|
||||
if (coverDownloadJobs.Any(j => j is DownloadMangaCoverJob dmc && dmc.MangaId == MangaId && dmc.state < JobState.Completed))
|
||||
{
|
||||
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000:D}");
|
||||
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * coverDownloadJobs.Count() * 2 / 1000);
|
||||
}
|
||||
else
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
Image image = Image.Load(m.CoverFileNameInCache);
|
||||
|
||||
if (width is { } w && height is { } h)
|
||||
{
|
||||
if (width < 10 || height < 10 || width > 65535 || height > 65535)
|
||||
return BadRequest();
|
||||
image.Mutate(i => i.ApplyProcessor(new ResizeProcessor(new ResizeOptions()
|
||||
{
|
||||
Mode = ResizeMode.Max,
|
||||
Size = new Size(w, h)
|
||||
}, image.Size)));
|
||||
}
|
||||
|
||||
using MemoryStream ms = new();
|
||||
image.Save(ms, new JpegEncoder(){Quality = 100});
|
||||
return File(ms.GetBuffer(), "image/jpeg");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Chapters of Manga
|
||||
/// </summary>
|
||||
/// <param name="id">Manga-ID</param>
|
||||
/// <returns>Array of Chapters</returns>
|
||||
[HttpGet("{id}/Chapters")]
|
||||
[ProducesResponseType<Chapter[]>(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound)]
|
||||
public IActionResult GetChapters(string id)
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Manga with ID not found</response>
|
||||
[HttpGet("{MangaId}/Chapters")]
|
||||
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetChapters(string MangaId)
|
||||
{
|
||||
Manga? m = context.Manga.Find(id);
|
||||
if (m is null)
|
||||
return NotFound("Manga could not be found");
|
||||
Chapter[] ret = context.Chapters.Where(c => c.ParentManga.MangaId == m.MangaId).ToArray();
|
||||
return Ok(ret);
|
||||
if(context.Mangas.Find(MangaId) is not { } m)
|
||||
return NotFound();
|
||||
|
||||
Chapter[] chapters = m.Chapters.ToArray();
|
||||
return Ok(chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the latest Chapter of requested Manga
|
||||
/// Returns all downloaded Chapters for Manga with ID
|
||||
/// </summary>
|
||||
/// <param name="id">Manga-ID</param>
|
||||
/// <returns>Latest Chapter</returns>
|
||||
[HttpGet("{id}/Chapter/Latest")]
|
||||
[ProducesResponseType<Chapter>(Status200OK)]
|
||||
[ProducesResponseType<string>(Status404NotFound)]
|
||||
public IActionResult GetLatestChapter(string id)
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="204">No available chapters</response>
|
||||
/// <response code="404">Manga with ID not found.</response>
|
||||
[HttpGet("{MangaId}/Chapters/Downloaded")]
|
||||
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetChaptersDownloaded(string MangaId)
|
||||
{
|
||||
Manga? m = context.Manga.Find(id);
|
||||
if (m is null)
|
||||
return NotFound("Manga could not be found");
|
||||
List<Chapter> chapters = context.Chapters.Where(c => c.ParentManga.MangaId == m.MangaId).ToList();
|
||||
if(context.Mangas.Find(MangaId) is not { } m)
|
||||
return NotFound();
|
||||
|
||||
List<Chapter> chapters = m.Chapters.ToList();
|
||||
if (chapters.Count == 0)
|
||||
return NoContent();
|
||||
|
||||
return Ok(chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Chapters not downloaded for Manga with ID
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="204">No available chapters</response>
|
||||
/// <response code="404">Manga with ID not found.</response>
|
||||
[HttpGet("{MangaId}/Chapters/NotDownloaded")]
|
||||
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetChaptersNotDownloaded(string MangaId)
|
||||
{
|
||||
if(context.Mangas.Find(MangaId) is not { } m)
|
||||
return NotFound();
|
||||
|
||||
List<Chapter> chapters = m.Chapters.ToList();
|
||||
if (chapters.Count == 0)
|
||||
return NoContent();
|
||||
|
||||
return Ok(chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the latest Chapter of requested Manga available on Website
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="204">No available chapters</response>
|
||||
/// <response code="404">Manga with ID not found.</response>
|
||||
/// <response code="500">Could not retrieve the maximum chapter-number</response>
|
||||
/// <response code="503">Retry after timeout, updating value</response>
|
||||
[HttpGet("{MangaId}/Chapter/LatestAvailable")]
|
||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||
public IActionResult GetLatestChapter(string MangaId)
|
||||
{
|
||||
if(context.Mangas.Find(MangaId) is not { } m)
|
||||
return NotFound();
|
||||
|
||||
List<Chapter> chapters = m.Chapters.ToList();
|
||||
if (chapters.Count == 0)
|
||||
{
|
||||
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
|
||||
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
|
||||
{
|
||||
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
|
||||
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2/ 1000);
|
||||
}else
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
Chapter? max = chapters.Max();
|
||||
if (max is null)
|
||||
return NotFound("Chapter could not be found");
|
||||
return StatusCode(500, "Max chapter could not be found");
|
||||
|
||||
return Ok(max);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the latest Chapter of requested Manga that is downloaded
|
||||
/// </summary>
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="204">No available chapters</response>
|
||||
/// <response code="404">Manga with ID not found.</response>
|
||||
/// <response code="500">Could not retrieve the maximum chapter-number</response>
|
||||
/// <response code="503">Retry after timeout, updating value</response>
|
||||
[HttpGet("{MangaId}/Chapter/LatestDownloaded")]
|
||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status204NoContent)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
[ProducesResponseType<int>(Status503ServiceUnavailable, "text/plain")]
|
||||
public IActionResult GetLatestChapterDownloaded(string MangaId)
|
||||
{
|
||||
if(context.Mangas.Find(MangaId) is not { } m)
|
||||
return NotFound();
|
||||
|
||||
List<Chapter> chapters = m.Chapters.ToList();
|
||||
if (chapters.Count == 0)
|
||||
{
|
||||
List<Job> retrieveChapterJobs = context.Jobs.Where(j => j.JobType == JobType.RetrieveChaptersJob).ToList();
|
||||
if (retrieveChapterJobs.Any(j => j is RetrieveChaptersJob rcj && rcj.MangaId == MangaId && rcj.state < JobState.Completed))
|
||||
{
|
||||
Response.Headers.Append("Retry-After", $"{TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000:D}");
|
||||
return StatusCode(Status503ServiceUnavailable, TrangaSettings.startNewJobTimeoutMs * retrieveChapterJobs.Count() * 2 / 1000);
|
||||
}else
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
Chapter? max = chapters.Max();
|
||||
if (max is null)
|
||||
return StatusCode(500, "Max chapter could not be found");
|
||||
|
||||
return Ok(max);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the cut-off for Manga
|
||||
/// </summary>
|
||||
/// <remarks>This is important for the DownloadNewChapters-Job</remarks>
|
||||
/// <param name="id">Manga-ID</param>
|
||||
/// <returns>Nothing</returns>
|
||||
[HttpPatch("{id}/IgnoreChaptersBefore")]
|
||||
[ProducesResponseType<float>(Status200OK)]
|
||||
public IActionResult IgnoreChaptersBefore(string id)
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="chapterThreshold">Threshold (Chapter Number)</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Manga with ID not found.</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("{MangaId}/IgnoreChaptersBefore")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold)
|
||||
{
|
||||
Manga? m = context.Manga.Find(id);
|
||||
Manga? m = context.Mangas.Find(MangaId);
|
||||
if (m is null)
|
||||
return NotFound("Manga could not be found");
|
||||
return Ok(m.IgnoreChapterBefore);
|
||||
return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
m.IgnoreChaptersBefore = chapterThreshold;
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Move the Directory the .cbz-files are located in
|
||||
/// Move Manga to different ToLibrary
|
||||
/// </summary>
|
||||
/// <param name="id">Manga-ID</param>
|
||||
/// <param name="folder">New Directory-Path</param>
|
||||
/// <returns>Nothing</returns>
|
||||
[HttpPost("{id}/MoveFolder")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
public IActionResult MoveFolder(string id, [FromBody]string folder)
|
||||
/// <param name="MangaId">Manga-ID</param>
|
||||
/// <param name="LibraryId">ToLibrary-Id</param>
|
||||
/// <response code="202">Folder is going to be moved</response>
|
||||
/// <response code="404">MangaId or LibraryId not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult MoveFolder(string MangaId, string LibraryId)
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
if (context.Mangas.Find(MangaId) is not { } manga)
|
||||
return NotFound();
|
||||
if(context.LocalLibraries.Find(LibraryId) is not { } library)
|
||||
return NotFound();
|
||||
|
||||
MoveMangaLibraryJob moveLibrary = new(manga, library);
|
||||
UpdateChaptersDownloadedJob updateDownloadedFiles = new(manga, 0, dependsOnJobs: [moveLibrary]);
|
||||
|
||||
try
|
||||
{
|
||||
context.Jobs.AddRange(moveLibrary, updateDownloadedFiles);
|
||||
context.SaveChanges();
|
||||
return Accepted();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
using API.Schema;
|
||||
using API.Schema.MangaConnectors;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("v{v:apiVersion}")]
|
||||
public class MiscController(PgsqlContext context) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all available Connectors (Scanlation-Sites)
|
||||
/// </summary>
|
||||
/// <returns>Array of MangaConnector</returns>
|
||||
[HttpGet("GetConnectors")]
|
||||
[ProducesResponseType<MangaConnector[]>(Status200OK)]
|
||||
public IActionResult GetConnectors()
|
||||
{
|
||||
MangaConnector[] connectors = context.MangaConnectors.ToArray();
|
||||
return Ok(connectors);
|
||||
}
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
using API.Schema;
|
||||
using System.Text;
|
||||
using API.APIEndpointRecords;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.NotificationConnectors;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
|
||||
@ -10,14 +13,14 @@ namespace API.Controllers;
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class NotificationConnectorController(PgsqlContext context) : Controller
|
||||
public class NotificationConnectorController(NotificationsContext context, ILog Log) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all configured Notification-Connectors
|
||||
/// </summary>
|
||||
/// <returns>Array of configured Notification-Connectors</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<NotificationConnector[]>(Status200OK)]
|
||||
[ProducesResponseType<NotificationConnector[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetAllConnectors()
|
||||
{
|
||||
NotificationConnector[] ret = context.NotificationConnectors.ToArray();
|
||||
@ -27,14 +30,15 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
|
||||
/// <summary>
|
||||
/// Returns Notification-Connector with requested ID
|
||||
/// </summary>
|
||||
/// <param name="id">Notification-Connector-ID</param>
|
||||
/// <returns>Notification-Connector</returns>
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType<NotificationConnector>(Status200OK)]
|
||||
/// <param name="NotificationConnectorId">Notification-Connector-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">NotificationConnector with ID not found</response>
|
||||
[HttpGet("{NotificationConnectorId}")]
|
||||
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetConnector(string id)
|
||||
public IActionResult GetConnector(string NotificationConnectorId)
|
||||
{
|
||||
NotificationConnector? ret = context.NotificationConnectors.Find(id);
|
||||
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId);
|
||||
return (ret is not null) switch
|
||||
{
|
||||
true => Ok(ret),
|
||||
@ -43,52 +47,145 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Notification-Connector
|
||||
/// Creates a new REST-Notification-Connector
|
||||
/// </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>
|
||||
/// <param name="notificationConnector">Notification-Connector</param>
|
||||
/// <returns>Nothing</returns>
|
||||
/// <response code="201">ID of new connector</response>
|
||||
/// <response code="409">A NotificationConnector with name already exists</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut]
|
||||
[ProducesResponseType<NotificationConnector[]>(Status200OK)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector)
|
||||
{
|
||||
if (context.NotificationConnectors.Find(notificationConnector.Name) is not null)
|
||||
return Conflict();
|
||||
try
|
||||
{
|
||||
context.NotificationConnectors.Add(notificationConnector);
|
||||
context.SaveChanges();
|
||||
return Created();
|
||||
return Created(notificationConnector.Name, notificationConnector);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Gotify-Notification-Connector
|
||||
/// </summary>
|
||||
/// <remarks>Priority needs to be between 0 and 10</remarks>
|
||||
/// <response code="201">ID of new connector</response>
|
||||
/// <response code="400"></response>
|
||||
/// <response code="409">A NotificationConnector with name already exists</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("Gotify")]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData)
|
||||
{
|
||||
if(!gotifyData.Validate())
|
||||
return BadRequest();
|
||||
|
||||
NotificationConnector gotifyConnector = new NotificationConnector(TokenGen.CreateToken("Gotify"),
|
||||
gotifyData.endpoint,
|
||||
new Dictionary<string, string>() { { "X-Gotify-Key", gotifyData.appToken } },
|
||||
"POST",
|
||||
$"{{\"message\": \"%text\", \"title\": \"%title\", \"priority\": {gotifyData.priority}}}");
|
||||
return CreateConnector(gotifyConnector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Ntfy-Notification-Connector
|
||||
/// </summary>
|
||||
/// <remarks>Priority needs to be between 1 and 5</remarks>
|
||||
/// <response code="201">ID of new connector</response>
|
||||
/// <response code="400"></response>
|
||||
/// <response code="409">A NotificationConnector with name already exists</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("Ntfy")]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreateNtfyConnector([FromBody]NtfyRecord ntfyRecord)
|
||||
{
|
||||
if(!ntfyRecord.Validate())
|
||||
return BadRequest();
|
||||
|
||||
string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ntfyRecord.username}:{ntfyRecord.password}"));
|
||||
string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=","");
|
||||
|
||||
NotificationConnector ntfyConnector = new (TokenGen.CreateToken("Ntfy"),
|
||||
$"{ntfyRecord.endpoint}?auth={auth}",
|
||||
new Dictionary<string, string>()
|
||||
{
|
||||
{"Title", "%title"},
|
||||
{"Priority", ntfyRecord.priority.ToString()},
|
||||
},
|
||||
"POST",
|
||||
"%text");
|
||||
return CreateConnector(ntfyConnector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Pushover-Notification-Connector
|
||||
/// </summary>
|
||||
/// <remarks>https://pushover.net/api</remarks>
|
||||
/// <response code="201">ID of new connector</response>
|
||||
/// <response code="400"></response>
|
||||
/// <response code="409">A NotificationConnector with name already exists</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPut("Pushover")]
|
||||
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType(Status409Conflict)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CreatePushoverConnector([FromBody]PushoverRecord pushoverRecord)
|
||||
{
|
||||
if(!pushoverRecord.Validate())
|
||||
return BadRequest();
|
||||
|
||||
NotificationConnector pushoverConnector = new (TokenGen.CreateToken("Pushover"),
|
||||
$"https://api.pushover.net/1/messages.json",
|
||||
new Dictionary<string, string>(),
|
||||
"POST",
|
||||
$"{{\"token\": \"{pushoverRecord.apptoken}\", \"user\": \"{pushoverRecord.user}\", \"message:\":\"%text\", \"%title\" }}");
|
||||
return CreateConnector(pushoverConnector);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the Notification-Connector with the requested ID
|
||||
/// </summary>
|
||||
/// <param name="id">Notification-Connector-ID</param>
|
||||
/// <returns>Nothing</returns>
|
||||
[HttpDelete("{id}")]
|
||||
/// <param name="NotificationConnectorId">Notification-Connector-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">NotificationConnector with ID not found</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpDelete("{NotificationConnectorId}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status500InternalServerError)]
|
||||
public IActionResult DeleteConnector(string id)
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult DeleteConnector(string NotificationConnectorId)
|
||||
{
|
||||
try
|
||||
{
|
||||
NotificationConnector? ret = context.NotificationConnectors.Find(id);
|
||||
switch (ret is not null)
|
||||
{
|
||||
case true:
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
case false: return NotFound();
|
||||
}
|
||||
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId);
|
||||
if(ret is null)
|
||||
return NotFound();
|
||||
|
||||
context.Remove(ret);
|
||||
context.SaveChanges();
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e.Message);
|
||||
}
|
||||
}
|
||||
|
112
API/Controllers/QueryController.cs
Normal file
112
API/Controllers/QueryController.cs
Normal file
@ -0,0 +1,112 @@
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class QueryController(PgsqlContext context, ILog Log) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the Author-Information for Author-ID
|
||||
/// </summary>
|
||||
/// <param name="AuthorId">Author-Id</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Author with ID not found</response>
|
||||
[HttpGet("Author/{AuthorId}")]
|
||||
[ProducesResponseType<Author>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetAuthor(string AuthorId)
|
||||
{
|
||||
Author? ret = context.Authors.Find(AuthorId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Mangas which where Authored by Author with AuthorId
|
||||
/// </summary>
|
||||
/// <param name="AuthorId">Author-ID</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Author not found</response>
|
||||
[HttpGet("Mangas/WithAuthorId/{AuthorId}")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetMangaWithAuthorIds(string AuthorId)
|
||||
{
|
||||
if(context.Authors.Find(AuthorId) is not { } a)
|
||||
return NotFound();
|
||||
return Ok(context.Mangas.Where(m => m.Authors.Contains(a)));
|
||||
}
|
||||
/*
|
||||
/// <summary>
|
||||
/// Returns Link-Information for Link-Id
|
||||
/// </summary>
|
||||
/// <param name="LinkId"></param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Link with ID not found</response>
|
||||
[HttpGet("Link/{LinkId}")]
|
||||
[ProducesResponseType<Link>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetLink(string LinkId)
|
||||
{
|
||||
Link? ret = context.Links.Find(LinkId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns AltTitle-Information for AltTitle-Id
|
||||
/// </summary>
|
||||
/// <param name="AltTitleId"></param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">AltTitle with ID not found</response>
|
||||
[HttpGet("AltTitle/{AltTitleId}")]
|
||||
[ProducesResponseType<MangaAltTitle>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetAltTitle(string AltTitleId)
|
||||
{
|
||||
MangaAltTitle? ret = context.AltTitles.Find(AltTitleId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
return Ok(ret);
|
||||
}*/
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Manga with Tag
|
||||
/// </summary>
|
||||
/// <param name="Tag"></param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Tag not found</response>
|
||||
[HttpGet("Mangas/WithTag/{Tag}")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
public IActionResult GetMangasWithTag(string Tag)
|
||||
{
|
||||
if(context.Tags.Find(Tag) is not { } t)
|
||||
return NotFound();
|
||||
return Ok(context.Mangas.Where(m => m.MangaTags.Contains(t)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Chapter-Information for Chapter-Id
|
||||
/// </summary>
|
||||
/// <param name="ChapterId"></param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">Chapter with ID not found</response>
|
||||
[HttpGet("Chapter/{ChapterId}")]
|
||||
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
|
||||
public IActionResult GetChapter(string ChapterId)
|
||||
{
|
||||
Chapter? ret = context.Chapters.Find(ChapterId);
|
||||
if (ret is null)
|
||||
return NotFound();
|
||||
return Ok(ret);
|
||||
}
|
||||
}
|
@ -1,150 +1,148 @@
|
||||
using API.Schema;
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.Jobs;
|
||||
using API.Schema.MangaConnectors;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Soenneker.Utils.String.NeedlemanWunsch;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class SearchController(PgsqlContext context) : Controller
|
||||
public class SearchController(PgsqlContext context, ILog Log) : Controller
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Initiate a search for a Manga on all Connectors
|
||||
/// </summary>
|
||||
/// <param name="name">Name/Title of the Manga</param>
|
||||
/// <returns>Array of Manga</returns>
|
||||
[HttpPost("{name}")]
|
||||
[ProducesResponseType<Manga[]>(Status500InternalServerError)]
|
||||
public IActionResult SearchMangaGlobal(string name)
|
||||
{
|
||||
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> allManga = new();
|
||||
foreach (MangaConnector contextMangaConnector in context.MangaConnectors)
|
||||
allManga.AddRange(contextMangaConnector.GetManga(name));
|
||||
List<Manga> retMangas = new();
|
||||
foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in allManga)
|
||||
{
|
||||
try
|
||||
{
|
||||
Manga? add = AddMangaToContext(manga, authors, tags, links, altTitles);
|
||||
if(add is not null)
|
||||
retMangas.Add(add);
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
return StatusCode(500, new ProblemResponse("An error occurred while processing your request."));
|
||||
}
|
||||
}
|
||||
return Ok(retMangas.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiate a search for a Manga on a specific Connector
|
||||
/// </summary>
|
||||
/// <param name="id">Manga-Connector-ID</param>
|
||||
/// <param name="name">Name/Title of the Manga</param>
|
||||
/// <returns>Manga</returns>
|
||||
[HttpPost("{id}/{name}")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK)]
|
||||
[ProducesResponseType<ProblemResponse>(Status404NotFound)]
|
||||
[ProducesResponseType<ProblemResponse>(Status500InternalServerError)]
|
||||
public IActionResult SearchManga(string id, string name)
|
||||
/// <param name="MangaConnectorName"></param>
|
||||
/// <param name="Query"></param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="404">MangaConnector with ID not found</response>
|
||||
/// <response code="406">MangaConnector with ID is disabled</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpGet("{MangaConnectorName}/{Query}")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
[ProducesResponseType(Status406NotAcceptable)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult SearchManga(string MangaConnectorName, string Query)
|
||||
{
|
||||
MangaConnector? connector = context.MangaConnectors.Find(id);
|
||||
if (connector is null)
|
||||
return NotFound(new ProblemResponse("Connector not found."));
|
||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] mangas = connector.GetManga(name);
|
||||
if(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
|
||||
return NotFound();
|
||||
else if (connector.Enabled is false)
|
||||
return StatusCode(Status406NotAcceptable);
|
||||
|
||||
Manga[] mangas = connector.SearchManga(Query);
|
||||
List<Manga> retMangas = new();
|
||||
foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in mangas)
|
||||
foreach (Manga manga in mangas)
|
||||
{
|
||||
try
|
||||
{
|
||||
Manga? add = AddMangaToContext(manga, authors, tags, links, altTitles);
|
||||
if(add is not null)
|
||||
if(AddMangaToContext(manga) is { } add)
|
||||
retMangas.Add(add);
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
return StatusCode(500, new ProblemResponse("An error occurred while processing your request.", e.Message));
|
||||
Log.Error(e);
|
||||
return StatusCode(Status500InternalServerError, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(retMangas.ToArray());
|
||||
}
|
||||
|
||||
private Manga? AddMangaToContext(Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links,
|
||||
List<MangaAltTitle>? altTitles)
|
||||
|
||||
/// <summary>
|
||||
/// Search for a known Manga
|
||||
/// </summary>
|
||||
/// <param name="Query"></param>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("Local/{Query}")]
|
||||
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||
public IActionResult SearchMangaLocally(string Query)
|
||||
{
|
||||
if (manga is null)
|
||||
Dictionary<Manga, double> distance = context.Mangas
|
||||
.ToArray()
|
||||
.ToDictionary(m => m, m => NeedlemanWunschStringUtil.CalculateSimilarityPercentage(Query, m.Name));
|
||||
return Ok(distance.Where(kv => kv.Value > 50).OrderByDescending(kv => kv.Value).Select(kv => kv.Key).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Manga from MangaConnector associated with URL
|
||||
/// </summary>
|
||||
/// <param name="url">Manga-Page URL</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="300">Multiple connectors found for URL</response>
|
||||
/// <response code="400">No Manga at URL</response>
|
||||
/// <response code="404">No connector found for URL</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("Url")]
|
||||
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult GetMangaFromUrl([FromBody]string url)
|
||||
{
|
||||
if (context.MangaConnectors.Find("Global") is not { } connector)
|
||||
return StatusCode(Status500InternalServerError, "Could not find Global Connector.");
|
||||
|
||||
if(connector.GetMangaFromUrl(url) is not { } manga)
|
||||
return BadRequest();
|
||||
try
|
||||
{
|
||||
if(AddMangaToContext(manga) is { } add)
|
||||
return Ok(add);
|
||||
return StatusCode(Status500InternalServerError);
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(Status500InternalServerError, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private Manga? AddMangaToContext(Manga manga)
|
||||
{
|
||||
context.Mangas.Load();
|
||||
context.Authors.Load();
|
||||
context.Tags.Load();
|
||||
context.MangaConnectors.Load();
|
||||
|
||||
IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
|
||||
{
|
||||
MangaTag? inDb = context.Tags.Find(mt.Tag);
|
||||
return inDb ?? mt;
|
||||
});
|
||||
manga.MangaTags = mergedTags.ToList();
|
||||
|
||||
IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
|
||||
{
|
||||
Author? inDb = context.Authors.Find(ma.AuthorId);
|
||||
return inDb ?? ma;
|
||||
});
|
||||
manga.Authors = mergedAuthors.ToList();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
if (context.Mangas.Find(manga.MangaId) is { } r)
|
||||
{
|
||||
context.Mangas.Remove(r);
|
||||
context.SaveChanges();
|
||||
}
|
||||
context.Mangas.Add(manga);
|
||||
context.Jobs.Add(new DownloadMangaCoverJob(manga));
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return null;
|
||||
Manga? existing = context.Manga.FirstOrDefault(m =>
|
||||
m.MangaConnector == manga.MangaConnector && m.ConnectorId == manga.ConnectorId);
|
||||
|
||||
if (tags is not null)
|
||||
{
|
||||
IEnumerable<MangaTag> mergedTags = tags.Select(mt =>
|
||||
{
|
||||
MangaTag? inDb = context.Tags.FirstOrDefault(t => t.Equals(mt));
|
||||
return inDb ?? mt;
|
||||
});
|
||||
manga.Tags = mergedTags.ToList();
|
||||
IEnumerable<MangaTag> newTags = manga.Tags.Where(mt => !context.Tags.Any(t => t.Tag.Equals(mt.Tag)));
|
||||
context.Tags.AddRange(newTags);
|
||||
}
|
||||
|
||||
if (authors is not null)
|
||||
{
|
||||
IEnumerable<Author> mergedAuthors = authors.Select(ma =>
|
||||
{
|
||||
Author? inDb = context.Authors.FirstOrDefault(a => a.AuthorName == ma.AuthorName);
|
||||
return inDb ?? ma;
|
||||
});
|
||||
manga.Authors = mergedAuthors.ToList();
|
||||
IEnumerable<Author> newAuthors = manga.Authors.Where(ma => !context.Authors.Any(a =>
|
||||
a.AuthorName == ma.AuthorName));
|
||||
context.Authors.AddRange(newAuthors);
|
||||
}
|
||||
|
||||
if (links is not null)
|
||||
{
|
||||
IEnumerable<Link> mergedLinks = links.Select(ml =>
|
||||
{
|
||||
Link? inDb = context.Link.FirstOrDefault(l =>
|
||||
l.LinkProvider == ml.LinkProvider && l.LinkUrl == ml.LinkUrl);
|
||||
return inDb ?? ml;
|
||||
});
|
||||
manga.Links = mergedLinks.ToList();
|
||||
IEnumerable<Link> newLinks = manga.Links.Where(ml => !context.Link.Any(l =>
|
||||
l.LinkProvider == ml.LinkProvider && l.LinkUrl == ml.LinkUrl));
|
||||
context.Link.AddRange(newLinks);
|
||||
}
|
||||
|
||||
if (altTitles is not null)
|
||||
{
|
||||
IEnumerable<MangaAltTitle> mergedAltTitles = altTitles.Select(mat =>
|
||||
{
|
||||
MangaAltTitle? inDb = context.AltTitles.FirstOrDefault(at =>
|
||||
at.Language == mat.Language && at.Title == mat.Title);
|
||||
return inDb ?? mat;
|
||||
});
|
||||
manga.AltTitles = mergedAltTitles.ToList();
|
||||
IEnumerable<MangaAltTitle> newAltTitles = manga.AltTitles.Where(mat =>
|
||||
!context.AltTitles.Any(at => at.Language == mat.Language && at.Title == mat.Title));
|
||||
context.AltTitles.AddRange(newAltTitles);
|
||||
}
|
||||
|
||||
existing?.UpdateWithInfo(manga);
|
||||
if(existing is not null)
|
||||
context.Manga.Update(existing);
|
||||
else
|
||||
context.Manga.Add(manga);
|
||||
|
||||
context.SaveChanges();
|
||||
return existing ?? manga;
|
||||
return manga;
|
||||
}
|
||||
}
|
@ -1,149 +1,192 @@
|
||||
using API.Schema;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.Jobs;
|
||||
using Asp.Versioning;
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiVersion(2)]
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("v{v:apiVersion}/[controller]")]
|
||||
public class SettingsController(PgsqlContext context) : Controller
|
||||
public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all Settings
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
[ProducesResponseType<JObject>(Status200OK, "application/json")]
|
||||
public IActionResult GetSettings()
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
return Ok(JObject.Parse(TrangaSettings.Serialize()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current UserAgent used by Tranga
|
||||
/// </summary>
|
||||
/// <returns>UserAgent as string</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("UserAgent")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
public IActionResult GetUserAgent()
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
return Ok(TrangaSettings.userAgent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a new UserAgent
|
||||
/// </summary>
|
||||
/// <returns>Nothing</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpPatch("UserAgent")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
public IActionResult SetUserAgent()
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public IActionResult SetUserAgent([FromBody]string userAgent)
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
TrangaSettings.UpdateUserAgent(userAgent);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the UserAgent to default
|
||||
/// </summary>
|
||||
/// <returns>Nothing</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpDelete("UserAgent")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public IActionResult ResetUserAgent()
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
TrangaSettings.UpdateUserAgent(TrangaSettings.DefaultUserAgent);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all Request-Limits
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("RequestLimits")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
[ProducesResponseType<Dictionary<RequestType,int>>(Status200OK, "application/json")]
|
||||
public IActionResult GetRequestLimits()
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
return Ok(TrangaSettings.requestLimits);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update all Request-Limits to new values
|
||||
/// </summary>
|
||||
/// <returns>Nothing</returns>
|
||||
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
|
||||
[HttpPatch("RequestLimits")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
[ProducesResponseType(Status501NotImplemented)]
|
||||
public IActionResult SetRequestLimits()
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
return StatusCode(501);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a Request-Limit value
|
||||
/// </summary>
|
||||
/// <param name="RequestType">Type of Request</param>
|
||||
/// <param name="requestLimit">New limit in Requests/Minute</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="400">Limit needs to be greater than 0</response>
|
||||
[HttpPatch("RequestLimits/{RequestType}")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
public IActionResult SetRequestLimit(RequestType RequestType, [FromBody]int requestLimit)
|
||||
{
|
||||
if (requestLimit <= 0)
|
||||
return BadRequest();
|
||||
TrangaSettings.UpdateRequestLimit(RequestType, requestLimit);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset all Request-Limits
|
||||
/// Reset Request-Limit
|
||||
/// </summary>
|
||||
/// <returns>Nothing</returns>
|
||||
/// <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>
|
||||
/// <response code="200"></response>
|
||||
[HttpDelete("RequestLimits")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
[ProducesResponseType<string>(Status200OK)]
|
||||
public IActionResult ResetRequestLimits()
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
TrangaSettings.ResetRequestLimits();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Level of Image-Compression for Images
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <response code="200">JPEG compression-level as Integer</response>
|
||||
[HttpGet("ImageCompression")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
[ProducesResponseType<int>(Status200OK, "text/plain")]
|
||||
public IActionResult GetImageCompression()
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
return Ok(TrangaSettings.compression);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the Image-Compression-Level for Images
|
||||
/// </summary>
|
||||
/// <param name="percentage">100 to disable, 0-99 for JPEG compression-Level</param>
|
||||
/// <returns>Nothing</returns>
|
||||
/// <param name="level">100 to disable, 0-99 for JPEG compression-Level</param>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="400">Level outside permitted range</response>
|
||||
[HttpPatch("ImageCompression")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
public IActionResult SetImageCompression(int percentage)
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status400BadRequest)]
|
||||
public IActionResult SetImageCompression([FromBody]int level)
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
if (level < 1 || level > 100)
|
||||
return BadRequest();
|
||||
TrangaSettings.UpdateCompressImages(level);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get state of Black/White-Image setting
|
||||
/// </summary>
|
||||
/// <returns>True if enabled</returns>
|
||||
/// <response code="200">True if enabled</response>
|
||||
[HttpGet("BWImages")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
[ProducesResponseType<bool>(Status200OK, "text/plain")]
|
||||
public IActionResult GetBwImagesToggle()
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
return Ok(TrangaSettings.bwImages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable/Disable conversion of Images to Black and White
|
||||
/// </summary>
|
||||
/// <param name="enabled">true to enable</param>
|
||||
/// <returns>Nothing</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpPatch("BWImages")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
public IActionResult SetBwImagesToggle(bool enabled)
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public IActionResult SetBwImagesToggle([FromBody]bool enabled)
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
TrangaSettings.UpdateBwImages(enabled);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get state of April Fools Mode
|
||||
/// </summary>
|
||||
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
|
||||
/// <returns>True if enabled</returns>
|
||||
/// <response code="200">True if enabled</response>
|
||||
[HttpGet("AprilFoolsMode")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
[ProducesResponseType<bool>(Status200OK, "text/plain")]
|
||||
public IActionResult GetAprilFoolsMode()
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
return Ok(TrangaSettings.aprilFoolsMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -151,11 +194,101 @@ public class SettingsController(PgsqlContext context) : Controller
|
||||
/// </summary>
|
||||
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
|
||||
/// <param name="enabled">true to enable</param>
|
||||
/// <returns>Nothing</returns>
|
||||
/// <response code="200"></response>
|
||||
[HttpPatch("AprilFoolsMode")]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
public IActionResult SetAprilFoolsMode(bool enabled)
|
||||
[ProducesResponseType(Status200OK)]
|
||||
public IActionResult SetAprilFoolsMode([FromBody]bool enabled)
|
||||
{
|
||||
return StatusCode(500, "Not implemented"); //TODO
|
||||
TrangaSettings.UpdateAprilFoolsMode(enabled);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Chapter Naming Scheme
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Placeholders:
|
||||
/// %M Manga Name
|
||||
/// %V Volume
|
||||
/// %C Chapter
|
||||
/// %T Title
|
||||
/// %A Author (first in list)
|
||||
/// %I Chapter Internal ID
|
||||
/// %i Manga Internal ID
|
||||
/// %Y Year (Manga)
|
||||
///
|
||||
/// ?_(...) replace _ with a value from above:
|
||||
/// Everything inside the braces will only be added if the value of %_ is not null
|
||||
/// </remarks>
|
||||
/// <response code="200"></response>
|
||||
[HttpGet("ChapterNamingScheme")]
|
||||
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||
public IActionResult GetCustomNamingScheme()
|
||||
{
|
||||
return Ok(TrangaSettings.chapterNamingScheme);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Chapter Naming Scheme
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Placeholders:
|
||||
/// %M Manga Name
|
||||
/// %V Volume
|
||||
/// %C Chapter
|
||||
/// %T Title
|
||||
/// %A Author (first in list)
|
||||
/// %I Chapter Internal ID
|
||||
/// %i Manga Internal ID
|
||||
/// %Y Year (Manga)
|
||||
///
|
||||
/// ?_(...) replace _ with a value from above:
|
||||
/// Everything inside the braces will only be added if the value of %_ is not null
|
||||
/// </remarks>
|
||||
/// <response code="200"></response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPatch("ChapterNamingScheme")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult SetCustomNamingScheme([FromBody]string namingScheme)
|
||||
{
|
||||
try
|
||||
{
|
||||
Dictionary<Chapter, string> oldPaths = context.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
|
||||
TrangaSettings.UpdateChapterNamingScheme(namingScheme);
|
||||
MoveFileOrFolderJob[] newJobs = oldPaths
|
||||
.Select(kv => new MoveFileOrFolderJob(kv.Value, kv.Key.FullArchiveFilePath)).ToArray();
|
||||
context.Jobs.AddRange(newJobs);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a UpdateCoverJob for all Manga
|
||||
/// </summary>
|
||||
/// <response code="200">Array of JobIds</response>
|
||||
/// <response code="500">Error during Database Operation</response>
|
||||
[HttpPost("CleanupCovers")]
|
||||
[ProducesResponseType<string[]>(Status200OK)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||
public IActionResult CleanupCovers()
|
||||
{
|
||||
try
|
||||
{
|
||||
Tranga.RemoveStaleFiles(context);
|
||||
List<UpdateCoverJob> newJobs = context.Mangas.ToList().Select(m => new UpdateCoverJob(m, 0)).ToList();
|
||||
context.Jobs.AddRange(newJobs);
|
||||
return Ok(newJobs.Select(j => j.JobId));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(500, e);
|
||||
}
|
||||
}
|
||||
}
|
35
API/HttpRequestTimeFeature.cs
Normal file
35
API/HttpRequestTimeFeature.cs
Normal file
@ -0,0 +1,35 @@
|
||||
namespace API;
|
||||
|
||||
public interface IHttpRequestTimeFeature
|
||||
{
|
||||
DateTime RequestTime { get; }
|
||||
}
|
||||
|
||||
public class HttpRequestTimeFeature : IHttpRequestTimeFeature
|
||||
{
|
||||
public DateTime RequestTime { get; }
|
||||
|
||||
public HttpRequestTimeFeature()
|
||||
{
|
||||
RequestTime = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
public class RequestTimeMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public RequestTimeMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var httpRequestTimeFeature = new HttpRequestTimeFeature();
|
||||
context.Features.Set<IHttpRequestTimeFeature>(httpRequestTimeFeature);
|
||||
|
||||
// Call the next delegate/middleware in the pipeline
|
||||
return this._next(context);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using HtmlAgilityPack;
|
||||
using log4net;
|
||||
using PuppeteerSharp;
|
||||
|
||||
namespace API.MangaDownloadClients;
|
||||
@ -10,8 +11,10 @@ internal class ChromiumDownloadClient : DownloadClient
|
||||
{
|
||||
private static IBrowser? _browser;
|
||||
private readonly HttpDownloadClient _httpDownloadClient;
|
||||
private readonly Thread _closeStalePagesThread;
|
||||
private readonly List<KeyValuePair<IPage, DateTime>> _openPages = new ();
|
||||
|
||||
private static async Task<IBrowser> StartBrowser()
|
||||
private static async Task<IBrowser> StartBrowser(ILog log)
|
||||
{
|
||||
return await Puppeteer.LaunchAsync(new LaunchOptions
|
||||
{
|
||||
@ -22,43 +25,36 @@ internal class ChromiumDownloadClient : DownloadClient
|
||||
"--disable-setuid-sandbox",
|
||||
"--no-sandbox"},
|
||||
Timeout = 30000
|
||||
}, new LoggerFactory([new LogProvider()])); //TODO
|
||||
}
|
||||
|
||||
private class LogProvider : ILoggerProvider
|
||||
{
|
||||
//TODO
|
||||
public void Dispose() { }
|
||||
|
||||
public ILogger CreateLogger(string categoryName) => new Logger();
|
||||
}
|
||||
|
||||
private class Logger : ILogger
|
||||
{
|
||||
public Logger() : base() { }
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (logLevel <= LogLevel.Information)
|
||||
return;
|
||||
//TODO
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
}, new LoggerFactory([new Provider(log)]));
|
||||
}
|
||||
|
||||
public ChromiumDownloadClient()
|
||||
{
|
||||
_httpDownloadClient = new();
|
||||
if(_browser is null)
|
||||
_browser = StartBrowser().Result;
|
||||
_browser = StartBrowser(Log).Result;
|
||||
_closeStalePagesThread = new Thread(CheckStalePages);
|
||||
_closeStalePagesThread.Start();
|
||||
}
|
||||
|
||||
private void CheckStalePages()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromHours(1));
|
||||
Log.Debug("Removing stale pages");
|
||||
foreach ((IPage? key, DateTime value) in _openPages.Where(kv => kv.Value.Subtract(DateTime.Now) > TimeSpan.FromHours(1)))
|
||||
{
|
||||
Log.Debug($"Closing {key.Url}");
|
||||
key.CloseAsync().Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Regex _imageUrlRex = new(@"https?:\/\/.*\.(?:p?jpe?g|gif|a?png|bmp|avif|webp)(\?.*)?");
|
||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
Log.Debug($"Requesting {url}");
|
||||
return _imageUrlRex.IsMatch(url)
|
||||
? _httpDownloadClient.MakeRequestInternal(url, referrer)
|
||||
: MakeRequestBrowser(url, referrer, clickButton);
|
||||
@ -69,17 +65,20 @@ internal class ChromiumDownloadClient : DownloadClient
|
||||
if (_browser is null)
|
||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||
IPage page = _browser.NewPageAsync().Result;
|
||||
page.DefaultTimeout = 10000;
|
||||
_openPages.Add(new(page, DateTime.Now));
|
||||
page.SetExtraHttpHeadersAsync(new() { { "Referer", referrer } });
|
||||
page.DefaultTimeout = 30000;
|
||||
IResponse response;
|
||||
try
|
||||
{
|
||||
response = page.GoToAsync(url, WaitUntilNavigation.Networkidle0).Result;
|
||||
//Log($"Page loaded. {url}");
|
||||
Log.Debug($"Page loaded. {url}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//Log($"Could not load Page {url}\n{e.Message}");
|
||||
Log.Info($"Could not load Page {url}\n{e.Message}");
|
||||
page.CloseAsync();
|
||||
_openPages.Remove(_openPages.Find(i => i.Key == page));
|
||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||
}
|
||||
|
||||
@ -103,11 +102,50 @@ internal class ChromiumDownloadClient : DownloadClient
|
||||
}
|
||||
else
|
||||
{
|
||||
page.CloseAsync();
|
||||
page.CloseAsync().Wait();
|
||||
_openPages.Remove(_openPages.Find(i => i.Key == page));
|
||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||
}
|
||||
|
||||
page.CloseAsync();
|
||||
page.CloseAsync().Wait();
|
||||
_openPages.Remove(_openPages.Find(i => i.Key == page));
|
||||
return new RequestResult(response.Status, document, stream, false, "");
|
||||
}
|
||||
|
||||
private class Provider(ILog log) : ILoggerProvider
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new ChromiumLogger(log);
|
||||
}
|
||||
}
|
||||
|
||||
private class ChromiumLogger(ILog log) : ILogger
|
||||
{
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
string message = formatter.Invoke(state, exception);
|
||||
switch(logLevel)
|
||||
{
|
||||
case LogLevel.Critical: log.Fatal(message); break;
|
||||
case LogLevel.Error: log.Error(message); break;
|
||||
case LogLevel.Warning: log.Warn(message); break;
|
||||
case LogLevel.Information: log.Info(message); break;
|
||||
case LogLevel.Debug: log.Debug(message); break;
|
||||
default: log.Info(message); break;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,21 @@
|
||||
using System.Net;
|
||||
using API.Schema;
|
||||
using log4net;
|
||||
|
||||
namespace API.MangaDownloadClients;
|
||||
|
||||
internal abstract class DownloadClient
|
||||
{
|
||||
private readonly Dictionary<RequestType, DateTime> _lastExecutedRateLimit;
|
||||
private static readonly Dictionary<RequestType, DateTime> LastExecutedRateLimit = new();
|
||||
protected ILog Log { get; init; }
|
||||
|
||||
protected DownloadClient()
|
||||
{
|
||||
this._lastExecutedRateLimit = new();
|
||||
this.Log = LogManager.GetLogger(GetType());
|
||||
}
|
||||
|
||||
public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
Log.Debug($"Requesting {requestType} {url}");
|
||||
if (!TrangaSettings.requestLimits.ContainsKey(requestType))
|
||||
{
|
||||
return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
|
||||
@ -24,17 +26,20 @@ internal abstract class DownloadClient
|
||||
: TrangaSettings.requestLimits[requestType];
|
||||
|
||||
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
|
||||
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(timeBetweenRequests));
|
||||
|
||||
TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType]));
|
||||
DateTime now = DateTime.Now;
|
||||
LastExecutedRateLimit.TryAdd(requestType, now.Subtract(timeBetweenRequests));
|
||||
|
||||
TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(now.Subtract(LastExecutedRateLimit[requestType]));
|
||||
Log.Debug($"Request limit {requestType} {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff} Timeout: {rateLimitTimeout:ss'.'fffff}");
|
||||
|
||||
if (rateLimitTimeout > TimeSpan.Zero)
|
||||
{
|
||||
Thread.Sleep(rateLimitTimeout);
|
||||
}
|
||||
|
||||
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
|
||||
_lastExecutedRateLimit[requestType] = DateTime.Now;
|
||||
LastExecutedRateLimit[requestType] = DateTime.UtcNow;
|
||||
Log.Debug($"Result {url}: {result}");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Net;
|
||||
using API.Schema;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace API.MangaDownloadClients;
|
||||
@ -18,16 +17,15 @@ internal class HttpDownloadClient : DownloadClient
|
||||
|
||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||
{
|
||||
//TODO
|
||||
//if (clickButton is not null)
|
||||
//Log("Can not click button on static site.");
|
||||
if (clickButton is not null)
|
||||
Log.Warn("Can not click button on static site.");
|
||||
HttpResponseMessage? response = null;
|
||||
while (response is null)
|
||||
{
|
||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
||||
if (referrer is not null)
|
||||
requestMessage.Headers.Referrer = new Uri(referrer);
|
||||
//Log($"Requesting {requestType} {url}");
|
||||
Log.Debug($"Requesting {url}");
|
||||
try
|
||||
{
|
||||
response = Client.Send(requestMessage);
|
||||
@ -48,8 +46,17 @@ internal class HttpDownloadClient : DownloadClient
|
||||
{
|
||||
return new RequestResult(response.StatusCode, null, Stream.Null);
|
||||
}
|
||||
|
||||
Stream stream = response.Content.ReadAsStream();
|
||||
|
||||
Stream stream;
|
||||
try
|
||||
{
|
||||
stream = response.Content.ReadAsStream();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||
}
|
||||
|
||||
HtmlDocument? document = null;
|
||||
|
||||
|
@ -24,4 +24,10 @@ public struct RequestResult
|
||||
this.hasBeenRedirected = hasBeenRedirected;
|
||||
redirectedToUrl = redirectedTo;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return
|
||||
$"{(int)statusCode} {statusCode.ToString()} {(hasBeenRedirected ? "Redirected: " : "")} {redirectedToUrl}";
|
||||
}
|
||||
}
|
@ -1,447 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Authors",
|
||||
columns: table => new
|
||||
{
|
||||
AuthorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
AuthorName = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Authors", x => x.AuthorId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LibraryConnectors",
|
||||
columns: table => new
|
||||
{
|
||||
LibraryConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
LibraryType = table.Column<byte>(type: "smallint", nullable: false),
|
||||
BaseUrl = table.Column<string>(type: "text", nullable: false),
|
||||
Auth = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LibraryConnectors", x => x.LibraryConnectorId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MangaConnectors",
|
||||
columns: table => new
|
||||
{
|
||||
Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
SupportedLanguages = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
BaseUris = table.Column<string[]>(type: "text[]", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MangaConnectors", x => x.Name);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NotificationConnectors",
|
||||
columns: table => new
|
||||
{
|
||||
NotificationConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
NotificationConnectorType = table.Column<byte>(type: "smallint", nullable: false),
|
||||
Endpoint = table.Column<string>(type: "text", nullable: true),
|
||||
AppToken = table.Column<string>(type: "text", nullable: true),
|
||||
Id = table.Column<string>(type: "text", nullable: true),
|
||||
Ntfy_Endpoint = table.Column<string>(type: "text", nullable: true),
|
||||
Auth = table.Column<string>(type: "text", nullable: true),
|
||||
Topic = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NotificationConnectors", x => x.NotificationConnectorId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Tags",
|
||||
columns: table => new
|
||||
{
|
||||
Tag = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Tags", x => x.Tag);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AltTitles",
|
||||
columns: table => new
|
||||
{
|
||||
AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
|
||||
Title = table.Column<string>(type: "text", nullable: false),
|
||||
MangaId = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
AltTitleIds = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AltTitles", x => x.AltTitleId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Chapters",
|
||||
columns: table => new
|
||||
{
|
||||
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
VolumeNumber = table.Column<float>(type: "real", nullable: true),
|
||||
ChapterNumber = table.Column<float>(type: "real", nullable: false),
|
||||
Url = table.Column<string>(type: "text", nullable: false),
|
||||
Title = table.Column<string>(type: "text", nullable: true),
|
||||
ArchiveFileName = table.Column<string>(type: "text", nullable: false),
|
||||
Downloaded = table.Column<bool>(type: "boolean", nullable: false),
|
||||
ParentMangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
ChapterIds = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Chapters", x => x.ChapterId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Manga",
|
||||
columns: table => new
|
||||
{
|
||||
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
ConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Description = table.Column<string>(type: "text", nullable: false),
|
||||
CoverUrl = table.Column<string>(type: "text", nullable: false),
|
||||
CoverFileNameInCache = table.Column<string>(type: "text", nullable: true),
|
||||
year = table.Column<long>(type: "bigint", nullable: false),
|
||||
OriginalLanguage = table.Column<string>(type: "text", nullable: true),
|
||||
ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
|
||||
FolderName = table.Column<string>(type: "text", nullable: false),
|
||||
IgnoreChapterBefore = table.Column<float>(type: "real", nullable: false),
|
||||
LatestChapterDownloadedId = table.Column<string>(type: "character varying(64)", nullable: true),
|
||||
LatestChapterAvailableId = table.Column<string>(type: "character varying(64)", nullable: true),
|
||||
MangaConnectorName = table.Column<string>(type: "character varying(32)", nullable: false),
|
||||
AuthorIds = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
TagIds = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
LinkIds = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
AltTitleIds = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
MangaIds = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Manga", x => x.MangaId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Manga_Chapters_LatestChapterAvailableId",
|
||||
column: x => x.LatestChapterAvailableId,
|
||||
principalTable: "Chapters",
|
||||
principalColumn: "ChapterId");
|
||||
table.ForeignKey(
|
||||
name: "FK_Manga_Chapters_LatestChapterDownloadedId",
|
||||
column: x => x.LatestChapterDownloadedId,
|
||||
principalTable: "Chapters",
|
||||
principalColumn: "ChapterId");
|
||||
table.ForeignKey(
|
||||
name: "FK_Manga_MangaConnectors_MangaConnectorName",
|
||||
column: x => x.MangaConnectorName,
|
||||
principalTable: "MangaConnectors",
|
||||
principalColumn: "Name",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Jobs",
|
||||
columns: table => new
|
||||
{
|
||||
JobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
ParentJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
DependsOnJobIds = table.Column<string[]>(type: "text[]", maxLength: 64, nullable: true),
|
||||
JobType = table.Column<byte>(type: "smallint", nullable: false),
|
||||
RecurrenceMs = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
LastExecution = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
NextExecution = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
state = table.Column<int>(type: "integer", nullable: false),
|
||||
JobId1 = table.Column<string>(type: "character varying(64)", nullable: true),
|
||||
ImagesLocation = table.Column<string>(type: "text", nullable: true),
|
||||
ComicInfoLocation = table.Column<string>(type: "text", nullable: true),
|
||||
CreateArchiveJob_ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
Path = table.Column<string>(type: "text", nullable: true),
|
||||
CreateComicInfoXmlJob_ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
FromLocation = table.Column<string>(type: "text", nullable: true),
|
||||
ToLocation = table.Column<string>(type: "text", nullable: true),
|
||||
ProcessImagesJob_Path = table.Column<string>(type: "text", nullable: true),
|
||||
Bw = table.Column<bool>(type: "boolean", nullable: true),
|
||||
Compression = table.Column<int>(type: "integer", nullable: true),
|
||||
SearchString = table.Column<string>(type: "text", nullable: true),
|
||||
MangaConnectorName = table.Column<string>(type: "text", nullable: true),
|
||||
UpdateMetadataJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Jobs", x => x.JobId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Chapters_ChapterId",
|
||||
column: x => x.ChapterId,
|
||||
principalTable: "Chapters",
|
||||
principalColumn: "ChapterId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Chapters_CreateArchiveJob_ChapterId",
|
||||
column: x => x.CreateArchiveJob_ChapterId,
|
||||
principalTable: "Chapters",
|
||||
principalColumn: "ChapterId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Chapters_CreateComicInfoXmlJob_ChapterId",
|
||||
column: x => x.CreateComicInfoXmlJob_ChapterId,
|
||||
principalTable: "Chapters",
|
||||
principalColumn: "ChapterId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Jobs_JobId1",
|
||||
column: x => x.JobId1,
|
||||
principalTable: "Jobs",
|
||||
principalColumn: "JobId");
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Manga_MangaId",
|
||||
column: x => x.MangaId,
|
||||
principalTable: "Manga",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Manga_UpdateMetadataJob_MangaId",
|
||||
column: x => x.UpdateMetadataJob_MangaId,
|
||||
principalTable: "Manga",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Link",
|
||||
columns: table => new
|
||||
{
|
||||
LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
LinkProvider = table.Column<string>(type: "text", nullable: false),
|
||||
LinkUrl = table.Column<string>(type: "text", nullable: false),
|
||||
MangaId = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
LinkIds = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Link", x => x.LinkId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Link_Manga_MangaId",
|
||||
column: x => x.MangaId,
|
||||
principalTable: "Manga",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MangaAuthor",
|
||||
columns: table => new
|
||||
{
|
||||
MangaId = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
AuthorId = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
AuthorIds = table.Column<string>(type: "text", nullable: true),
|
||||
MangaIds = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MangaAuthor", x => new { x.MangaId, x.AuthorId });
|
||||
table.ForeignKey(
|
||||
name: "FK_MangaAuthor_Authors_AuthorId",
|
||||
column: x => x.AuthorId,
|
||||
principalTable: "Authors",
|
||||
principalColumn: "AuthorId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MangaAuthor_Manga_MangaId",
|
||||
column: x => x.MangaId,
|
||||
principalTable: "Manga",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MangaTag",
|
||||
columns: table => new
|
||||
{
|
||||
MangaId = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
Tag = table.Column<string>(type: "text", nullable: false),
|
||||
MangaIds = table.Column<string>(type: "text", nullable: false),
|
||||
TagIds = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MangaTag", x => new { x.MangaId, x.Tag });
|
||||
table.ForeignKey(
|
||||
name: "FK_MangaTag_Manga_MangaId",
|
||||
column: x => x.MangaId,
|
||||
principalTable: "Manga",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MangaTag_Tags_MangaIds",
|
||||
column: x => x.MangaIds,
|
||||
principalTable: "Tags",
|
||||
principalColumn: "Tag",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MangaTag_Tags_Tag",
|
||||
column: x => x.Tag,
|
||||
principalTable: "Tags",
|
||||
principalColumn: "Tag",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AltTitles_MangaId",
|
||||
table: "AltTitles",
|
||||
column: "MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Chapters_ParentMangaId",
|
||||
table: "Chapters",
|
||||
column: "ParentMangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_ChapterId",
|
||||
table: "Jobs",
|
||||
column: "ChapterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_CreateArchiveJob_ChapterId",
|
||||
table: "Jobs",
|
||||
column: "CreateArchiveJob_ChapterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_CreateComicInfoXmlJob_ChapterId",
|
||||
table: "Jobs",
|
||||
column: "CreateComicInfoXmlJob_ChapterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_JobId1",
|
||||
table: "Jobs",
|
||||
column: "JobId1");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_MangaId",
|
||||
table: "Jobs",
|
||||
column: "MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_UpdateMetadataJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "UpdateMetadataJob_MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Link_MangaId",
|
||||
table: "Link",
|
||||
column: "MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Manga_LatestChapterAvailableId",
|
||||
table: "Manga",
|
||||
column: "LatestChapterAvailableId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Manga_LatestChapterDownloadedId",
|
||||
table: "Manga",
|
||||
column: "LatestChapterDownloadedId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Manga_MangaConnectorName",
|
||||
table: "Manga",
|
||||
column: "MangaConnectorName");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaAuthor_AuthorId",
|
||||
table: "MangaAuthor",
|
||||
column: "AuthorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaTag_MangaIds",
|
||||
table: "MangaTag",
|
||||
column: "MangaIds");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaTag_Tag",
|
||||
table: "MangaTag",
|
||||
column: "Tag");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AltTitles_Manga_MangaId",
|
||||
table: "AltTitles",
|
||||
column: "MangaId",
|
||||
principalTable: "Manga",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Chapters_Manga_ParentMangaId",
|
||||
table: "Chapters",
|
||||
column: "ParentMangaId",
|
||||
principalTable: "Manga",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Chapters_Manga_ParentMangaId",
|
||||
table: "Chapters");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AltTitles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Jobs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LibraryConnectors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaAuthor");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaTag");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "NotificationConnectors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Authors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Manga");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Chapters");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaConnectors");
|
||||
}
|
||||
}
|
||||
}
|
71
API/Migrations/library/20250515120732_Initial.Designer.cs
generated
Normal file
71
API/Migrations/library/20250515120732_Initial.Designer.cs
generated
Normal file
@ -0,0 +1,71 @@
|
||||
// <auto-generated />
|
||||
using API.Schema.Contexts;
|
||||
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.library
|
||||
{
|
||||
[DbContext(typeof(LibraryContext))]
|
||||
[Migration("20250515120732_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
|
||||
{
|
||||
b.Property<string>("LibraryConnectorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Auth")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("BaseUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<byte>("LibraryType")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("LibraryConnectorId");
|
||||
|
||||
b.ToTable("LibraryConnectors");
|
||||
|
||||
b.HasDiscriminator<byte>("LibraryType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
35
API/Migrations/library/20250515120732_Initial.cs
Normal file
35
API/Migrations/library/20250515120732_Initial.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.library
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LibraryConnectors",
|
||||
columns: table => new
|
||||
{
|
||||
LibraryConnectorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
LibraryType = table.Column<byte>(type: "smallint", nullable: false),
|
||||
BaseUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Auth = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LibraryConnectors", x => x.LibraryConnectorId);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "LibraryConnectors");
|
||||
}
|
||||
}
|
||||
}
|
68
API/Migrations/library/LibraryContextModelSnapshot.cs
Normal file
68
API/Migrations/library/LibraryContextModelSnapshot.cs
Normal file
@ -0,0 +1,68 @@
|
||||
// <auto-generated />
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.library
|
||||
{
|
||||
[DbContext(typeof(LibraryContext))]
|
||||
partial class LibraryContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
|
||||
{
|
||||
b.Property<string>("LibraryConnectorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Auth")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("BaseUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<byte>("LibraryType")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("LibraryConnectorId");
|
||||
|
||||
b.ToTable("LibraryConnectors");
|
||||
|
||||
b.HasDiscriminator<byte>("LibraryType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
89
API/Migrations/notifications/20250515120746_Initial.Designer.cs
generated
Normal file
89
API/Migrations/notifications/20250515120746_Initial.Designer.cs
generated
Normal file
@ -0,0 +1,89 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Schema.Contexts;
|
||||
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.notifications
|
||||
{
|
||||
[DbContext(typeof(NotificationsContext))]
|
||||
[Migration("20250515120746_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
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()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
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()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)");
|
||||
|
||||
b.Property<Dictionary<string, string>>("Headers")
|
||||
.IsRequired()
|
||||
.HasColumnType("hstore");
|
||||
|
||||
b.Property<string>("HttpMethod")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("NotificationConnectors");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
59
API/Migrations/notifications/20250515120746_Initial.cs
Normal file
59
API/Migrations/notifications/20250515120746_Initial.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.notifications
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:PostgresExtension:hstore", ",,");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NotificationConnectors",
|
||||
columns: table => new
|
||||
{
|
||||
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||
Headers = table.Column<Dictionary<string, string>>(type: "hstore", nullable: false),
|
||||
HttpMethod = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
|
||||
Body = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NotificationConnectors", x => x.Name);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Notifications",
|
||||
columns: table => new
|
||||
{
|
||||
NotificationId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Urgency = table.Column<byte>(type: "smallint", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
Message = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Notifications", x => x.NotificationId);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "NotificationConnectors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Notifications");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.notifications
|
||||
{
|
||||
[DbContext(typeof(NotificationsContext))]
|
||||
partial class NotificationsContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
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()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
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()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)");
|
||||
|
||||
b.Property<Dictionary<string, string>>("Headers")
|
||||
.IsRequired()
|
||||
.HasColumnType("hstore");
|
||||
|
||||
b.Property<string>("HttpMethod")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("NotificationConnectors");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +1,26 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
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
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
[DbContext(typeof(PgsqlContext))]
|
||||
partial class PgsqlContextModelSnapshot : ModelSnapshot
|
||||
[Migration("20250515120724_Initial-1")]
|
||||
partial class Initial1
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@ -30,7 +33,8 @@ namespace API.Migrations
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("AuthorId");
|
||||
|
||||
@ -43,29 +47,34 @@ namespace API.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ArchiveFileName")
|
||||
b.Property<string>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<float>("ChapterNumber")
|
||||
.HasColumnType("real");
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<float?>("VolumeNumber")
|
||||
.HasColumnType("real");
|
||||
b.Property<int?>("VolumeNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("ChapterId");
|
||||
|
||||
@ -80,12 +89,8 @@ namespace API.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<string>("JobId1")
|
||||
.HasColumnType("character varying(64)");
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<byte>("JobType")
|
||||
.HasColumnType("smallint");
|
||||
@ -100,13 +105,11 @@ namespace API.Migrations
|
||||
b.Property<decimal>("RecurrenceMs")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<int>("state")
|
||||
.HasColumnType("integer");
|
||||
b.Property<byte>("state")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("JobId");
|
||||
|
||||
b.HasIndex("JobId1");
|
||||
|
||||
b.HasIndex("ParentJobId");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
@ -116,54 +119,25 @@ namespace API.Migrations
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b =>
|
||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
||||
{
|
||||
b.Property<string>("LibraryConnectorId")
|
||||
b.Property<string>("LocalLibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Auth")
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("BaseUrl")
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<byte>("LibraryType")
|
||||
.HasColumnType("smallint");
|
||||
b.HasKey("LocalLibraryId");
|
||||
|
||||
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");
|
||||
b.ToTable("LocalLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
@ -172,80 +146,68 @@ namespace API.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ConnectorId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("FolderName")
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<float>("IgnoreChapterBefore")
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("MangaConnectorId")
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<long>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("MangaId");
|
||||
|
||||
b.HasIndex("MangaConnectorId");
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("Manga");
|
||||
});
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
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");
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
||||
@ -256,10 +218,20 @@ namespace API.Migrations
|
||||
|
||||
b.PrimitiveCollection<string[]>("BaseUris")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.HasKey("Name");
|
||||
@ -274,87 +246,60 @@ namespace API.Migrations
|
||||
modelBuilder.Entity("API.Schema.MangaTag", b =>
|
||||
{
|
||||
b.Property<string>("Tag")
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Notification", b =>
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("NotificationId")
|
||||
.HasMaxLength(64)
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.Property<byte>("Urgency")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("NotificationId");
|
||||
|
||||
b.ToTable("Notifications");
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.Property<string>("NotificationConnectorId")
|
||||
.HasMaxLength(64)
|
||||
b.Property<string>("DependsOnJobsJobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<byte>("NotificationConnectorType")
|
||||
.HasColumnType("smallint");
|
||||
b.Property<string>("JobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("NotificationConnectorId");
|
||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
||||
|
||||
b.ToTable("NotificationConnectors");
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.HasDiscriminator<byte>("NotificationConnectorType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
b.ToTable("JobJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorManga", b =>
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorsAuthorId")
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("AuthorsAuthorId", "MangaId");
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorManga");
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaMangaTag", b =>
|
||||
{
|
||||
b.Property<string>("MangaId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("TagsTag")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("MangaId", "TagsTag");
|
||||
|
||||
b.HasIndex("TagsTag");
|
||||
|
||||
b.ToTable("MangaMangaTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b =>
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
@ -365,9 +310,29 @@ namespace API.Migrations
|
||||
|
||||
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");
|
||||
@ -388,16 +353,70 @@ namespace API.Migrations
|
||||
|
||||
b.Property<string>("FromLocation")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ToLocation")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)3);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ToLibraryId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasIndex("ToLibraryId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("MoveMangaLibraryJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)7);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
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.UpdateChaptersDownloadedJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
@ -411,38 +430,17 @@ namespace API.Migrations
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("UpdateMetadataJob_MangaId");
|
||||
.HasColumnName("UpdateFilesDownloadedJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)2);
|
||||
b.HasDiscriminator().HasValue((byte)6);
|
||||
});
|
||||
|
||||
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 =>
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", 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");
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
||||
@ -452,117 +450,10 @@ namespace API.Migrations
|
||||
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.MangaLife", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Manga4Life");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Manganato", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Manganato");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Mangasee", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Mangasee");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Mangaworld");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("ManhuaPlus");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Weebcentral");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.NotificationConnectors.Gotify", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector");
|
||||
|
||||
b.Property<string>("AppToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Endpoint")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)0);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.NotificationConnectors.Lunasea", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector");
|
||||
|
||||
b.Property<string>("Id")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)1);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.NotificationConnectors.Ntfy", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector");
|
||||
|
||||
b.Property<string>("Auth")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Endpoint")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Topic")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.ToTable("NotificationConnectors", t =>
|
||||
{
|
||||
t.Property("Endpoint")
|
||||
.HasColumnName("Ntfy_Endpoint");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)2);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
||||
.WithMany()
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
@ -572,73 +463,153 @@ namespace API.Migrations
|
||||
|
||||
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");
|
||||
.HasForeignKey("ParentJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
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.LocalLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaConnectorId")
|
||||
.HasForeignKey("MangaConnectorName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("API.Schema.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("LinkId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.HasKey("LinkId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("AltTitleId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("AltTitleId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("MangaAltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany("AltTitles")
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorManga", b =>
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorsAuthorId")
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaMangaTag", b =>
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DependsOnJobsJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsTag")
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b =>
|
||||
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()
|
||||
@ -660,7 +631,26 @@ namespace API.Migrations
|
||||
b.Navigation("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b =>
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToLibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("ToLibrary");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
@ -671,16 +661,20 @@ namespace API.Migrations
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateChaptersDownloadedJob", b =>
|
||||
{
|
||||
b.Navigation("DependsOnJobs");
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Links");
|
||||
b.Navigation("Chapters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
433
API/Migrations/pgsql/20250515120724_Initial-1.cs
Normal file
433
API/Migrations/pgsql/20250515120724_Initial-1.cs
Normal file
@ -0,0 +1,433 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial1 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Authors",
|
||||
columns: table => new
|
||||
{
|
||||
AuthorId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
AuthorName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Authors", x => x.AuthorId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LocalLibraries",
|
||||
columns: table => new
|
||||
{
|
||||
LocalLibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
BasePath = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
LibraryName = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LocalLibraries", x => x.LocalLibraryId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MangaConnectors",
|
||||
columns: table => new
|
||||
{
|
||||
Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
SupportedLanguages = table.Column<string[]>(type: "text[]", maxLength: 8, nullable: false),
|
||||
IconUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||
BaseUris = table.Column<string[]>(type: "text[]", maxLength: 256, nullable: false),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MangaConnectors", x => x.Name);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Tags",
|
||||
columns: table => new
|
||||
{
|
||||
Tag = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Tags", x => x.Tag);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Mangas",
|
||||
columns: table => new
|
||||
{
|
||||
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
Description = table.Column<string>(type: "text", nullable: false),
|
||||
WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
CoverUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
|
||||
LibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
IgnoreChaptersBefore = table.Column<float>(type: "real", nullable: false),
|
||||
DirectoryName = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
CoverFileNameInCache = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
Year = table.Column<long>(type: "bigint", nullable: false),
|
||||
OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Mangas", x => x.MangaId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Mangas_LocalLibraries_LibraryId",
|
||||
column: x => x.LibraryId,
|
||||
principalTable: "LocalLibraries",
|
||||
principalColumn: "LocalLibraryId",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_Mangas_MangaConnectors_MangaConnectorName",
|
||||
column: x => x.MangaConnectorName,
|
||||
principalTable: "MangaConnectors",
|
||||
principalColumn: "Name",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AuthorToManga",
|
||||
columns: table => new
|
||||
{
|
||||
AuthorIds = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
MangaIds = table.Column<string>(type: "character varying(64)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AuthorToManga", x => new { x.AuthorIds, x.MangaIds });
|
||||
table.ForeignKey(
|
||||
name: "FK_AuthorToManga_Authors_AuthorIds",
|
||||
column: x => x.AuthorIds,
|
||||
principalTable: "Authors",
|
||||
principalColumn: "AuthorId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AuthorToManga_Mangas_MangaIds",
|
||||
column: x => x.MangaIds,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Chapters",
|
||||
columns: table => new
|
||||
{
|
||||
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
ParentMangaId = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
VolumeNumber = table.Column<int>(type: "integer", nullable: true),
|
||||
ChapterNumber = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Downloaded = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Chapters", x => x.ChapterId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Chapters_Mangas_ParentMangaId",
|
||||
column: x => x.ParentMangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Link",
|
||||
columns: table => new
|
||||
{
|
||||
LinkId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||
MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Link", x => x.LinkId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Link_Mangas_MangaId",
|
||||
column: x => x.MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MangaAltTitle",
|
||||
columns: table => new
|
||||
{
|
||||
AltTitleId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
MangaId = table.Column<string>(type: "character varying(64)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MangaAltTitle", x => x.AltTitleId);
|
||||
table.ForeignKey(
|
||||
name: "FK_MangaAltTitle_Mangas_MangaId",
|
||||
column: x => x.MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MangaTagToManga",
|
||||
columns: table => new
|
||||
{
|
||||
MangaTagIds = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
MangaIds = table.Column<string>(type: "character varying(64)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MangaTagToManga", x => new { x.MangaTagIds, x.MangaIds });
|
||||
table.ForeignKey(
|
||||
name: "FK_MangaTagToManga_Mangas_MangaIds",
|
||||
column: x => x.MangaIds,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MangaTagToManga_Tags_MangaTagIds",
|
||||
column: x => x.MangaTagIds,
|
||||
principalTable: "Tags",
|
||||
principalColumn: "Tag",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Jobs",
|
||||
columns: table => new
|
||||
{
|
||||
JobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
ParentJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
JobType = table.Column<byte>(type: "smallint", nullable: false),
|
||||
RecurrenceMs = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
LastExecution = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
state = table.Column<byte>(type: "smallint", nullable: false),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DownloadAvailableChaptersJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
ChapterId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
FromLocation = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ToLocation = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
MoveMangaLibraryJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
ToLibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
RetrieveChaptersJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true),
|
||||
UpdateFilesDownloadedJob_MangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Jobs", x => x.JobId);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Chapters_ChapterId",
|
||||
column: x => x.ChapterId,
|
||||
principalTable: "Chapters",
|
||||
principalColumn: "ChapterId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Jobs_ParentJobId",
|
||||
column: x => x.ParentJobId,
|
||||
principalTable: "Jobs",
|
||||
principalColumn: "JobId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_LocalLibraries_ToLibraryId",
|
||||
column: x => x.ToLibraryId,
|
||||
principalTable: "LocalLibraries",
|
||||
principalColumn: "LocalLibraryId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Mangas_DownloadAvailableChaptersJob_MangaId",
|
||||
column: x => x.DownloadAvailableChaptersJob_MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Mangas_MangaId",
|
||||
column: x => x.MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Mangas_MoveMangaLibraryJob_MangaId",
|
||||
column: x => x.MoveMangaLibraryJob_MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Mangas_RetrieveChaptersJob_MangaId",
|
||||
column: x => x.RetrieveChaptersJob_MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
|
||||
column: x => x.UpdateFilesDownloadedJob_MangaId,
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "JobJob",
|
||||
columns: table => new
|
||||
{
|
||||
DependsOnJobsJobId = table.Column<string>(type: "character varying(64)", nullable: false),
|
||||
JobId = table.Column<string>(type: "character varying(64)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_JobJob", x => new { x.DependsOnJobsJobId, x.JobId });
|
||||
table.ForeignKey(
|
||||
name: "FK_JobJob_Jobs_DependsOnJobsJobId",
|
||||
column: x => x.DependsOnJobsJobId,
|
||||
principalTable: "Jobs",
|
||||
principalColumn: "JobId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_JobJob_Jobs_JobId",
|
||||
column: x => x.JobId,
|
||||
principalTable: "Jobs",
|
||||
principalColumn: "JobId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuthorToManga_MangaIds",
|
||||
table: "AuthorToManga",
|
||||
column: "MangaIds");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Chapters_ParentMangaId",
|
||||
table: "Chapters",
|
||||
column: "ParentMangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_JobJob_JobId",
|
||||
table: "JobJob",
|
||||
column: "JobId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_ChapterId",
|
||||
table: "Jobs",
|
||||
column: "ChapterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_DownloadAvailableChaptersJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "DownloadAvailableChaptersJob_MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_MangaId",
|
||||
table: "Jobs",
|
||||
column: "MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_MoveMangaLibraryJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "MoveMangaLibraryJob_MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_ParentJobId",
|
||||
table: "Jobs",
|
||||
column: "ParentJobId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_RetrieveChaptersJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "RetrieveChaptersJob_MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_ToLibraryId",
|
||||
table: "Jobs",
|
||||
column: "ToLibraryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "UpdateFilesDownloadedJob_MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Link_MangaId",
|
||||
table: "Link",
|
||||
column: "MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaAltTitle_MangaId",
|
||||
table: "MangaAltTitle",
|
||||
column: "MangaId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Mangas_LibraryId",
|
||||
table: "Mangas",
|
||||
column: "LibraryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Mangas_MangaConnectorName",
|
||||
table: "Mangas",
|
||||
column: "MangaConnectorName");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaTagToManga_MangaIds",
|
||||
table: "MangaTagToManga",
|
||||
column: "MangaIds");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AuthorToManga");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "JobJob");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Link");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaAltTitle");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaTagToManga");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Authors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Jobs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Tags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Chapters");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Mangas");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LocalLibraries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MangaConnectors");
|
||||
}
|
||||
}
|
||||
}
|
688
API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs
generated
Normal file
688
API/Migrations/pgsql/20250516121442_AltTitle-Owned.Designer.cs
generated
Normal file
@ -0,0 +1,688 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Schema.Contexts;
|
||||
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.pgsql
|
||||
{
|
||||
[DbContext(typeof(PgsqlContext))]
|
||||
[Migration("20250516121442_AltTitle-Owned")]
|
||||
partial class AltTitleOwned
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.Author", b =>
|
||||
{
|
||||
b.Property<string>("AuthorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
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>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
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.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
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("ParentJobId");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
|
||||
b.HasDiscriminator<byte>("JobType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
||||
{
|
||||
b.Property<string>("LocalLibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("LocalLibraryId");
|
||||
|
||||
b.ToTable("LocalLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Property<string>("MangaId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<long>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("MangaId");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("BaseUris")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.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")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.Property<string>("DependsOnJobsJobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("JobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.ToTable("JobJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
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()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ToLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)3);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ToLibraryId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasIndex("ToLibraryId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("MoveMangaLibraryJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)7);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
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.UpdateChaptersDownloadedJob", 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.MangaConnectors.ComickIo", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("ComickIo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("ParentJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaConnectorName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("API.Schema.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("LinkId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.HasKey("LinkId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("MangaId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("MangaId", "Id");
|
||||
|
||||
b1.ToTable("MangaAltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DependsOnJobsJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.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.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToLibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("ToLibrary");
|
||||
});
|
||||
|
||||
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.UpdateChaptersDownloadedJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
70
API/Migrations/pgsql/20250516121442_AltTitle-Owned.cs
Normal file
70
API/Migrations/pgsql/20250516121442_AltTitle-Owned.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AltTitleOwned : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_MangaAltTitle",
|
||||
table: "MangaAltTitle");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MangaAltTitle_MangaId",
|
||||
table: "MangaAltTitle");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AltTitleId",
|
||||
table: "MangaAltTitle");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Id",
|
||||
table: "MangaAltTitle",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_MangaAltTitle",
|
||||
table: "MangaAltTitle",
|
||||
columns: new[] { "MangaId", "Id" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_MangaAltTitle",
|
||||
table: "MangaAltTitle");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Id",
|
||||
table: "MangaAltTitle");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AltTitleId",
|
||||
table: "MangaAltTitle",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_MangaAltTitle",
|
||||
table: "MangaAltTitle",
|
||||
column: "AltTitleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaAltTitle_MangaId",
|
||||
table: "MangaAltTitle",
|
||||
column: "MangaId");
|
||||
}
|
||||
}
|
||||
}
|
688
API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs
generated
Normal file
688
API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.Designer.cs
generated
Normal file
@ -0,0 +1,688 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Schema.Contexts;
|
||||
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.pgsql
|
||||
{
|
||||
[DbContext(typeof(PgsqlContext))]
|
||||
[Migration("20250516121725_Manga-Year-Nullable")]
|
||||
partial class MangaYearNullable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.Author", b =>
|
||||
{
|
||||
b.Property<string>("AuthorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
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>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
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.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
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("ParentJobId");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
|
||||
b.HasDiscriminator<byte>("JobType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
||||
{
|
||||
b.Property<string>("LocalLibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("LocalLibraryId");
|
||||
|
||||
b.ToTable("LocalLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Property<string>("MangaId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<long?>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("MangaId");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("BaseUris")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.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")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.Property<string>("DependsOnJobsJobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("JobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.ToTable("JobJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
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()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ToLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)3);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ToLibraryId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasIndex("ToLibraryId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("MoveMangaLibraryJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)7);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
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.UpdateChaptersDownloadedJob", 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.MangaConnectors.ComickIo", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("ComickIo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("ParentJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaConnectorName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("API.Schema.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("LinkId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.HasKey("LinkId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("MangaId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("MangaId", "Id");
|
||||
|
||||
b1.ToTable("MangaAltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DependsOnJobsJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.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.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToLibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("ToLibrary");
|
||||
});
|
||||
|
||||
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.UpdateChaptersDownloadedJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
36
API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.cs
Normal file
36
API/Migrations/pgsql/20250516121725_Manga-Year-Nullable.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MangaYearNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<long>(
|
||||
name: "Year",
|
||||
table: "Mangas",
|
||||
type: "bigint",
|
||||
nullable: true,
|
||||
oldClrType: typeof(long),
|
||||
oldType: "bigint");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<long>(
|
||||
name: "Year",
|
||||
table: "Mangas",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L,
|
||||
oldClrType: typeof(long),
|
||||
oldType: "bigint",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
689
API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs
generated
Normal file
689
API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.Designer.cs
generated
Normal file
@ -0,0 +1,689 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Schema.Contexts;
|
||||
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.pgsql
|
||||
{
|
||||
[DbContext(typeof(PgsqlContext))]
|
||||
[Migration("20250516122242_AltTitle-Owned-WithId")]
|
||||
partial class AltTitleOwnedWithId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.Author", b =>
|
||||
{
|
||||
b.Property<string>("AuthorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
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>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
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.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
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("ParentJobId");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
|
||||
b.HasDiscriminator<byte>("JobType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
||||
{
|
||||
b.Property<string>("LocalLibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("LocalLibraryId");
|
||||
|
||||
b.ToTable("LocalLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Property<string>("MangaId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<long?>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("MangaId");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("BaseUris")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.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")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.Property<string>("DependsOnJobsJobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("JobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.ToTable("JobJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
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()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ToLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)3);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ToLibraryId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasIndex("ToLibraryId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("MoveMangaLibraryJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)7);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
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.UpdateChaptersDownloadedJob", 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.MangaConnectors.ComickIo", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("ComickIo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("ParentJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaConnectorName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("API.Schema.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("LinkId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.HasKey("LinkId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("AltTitleId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("AltTitleId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("MangaAltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DependsOnJobsJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.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.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToLibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("ToLibrary");
|
||||
});
|
||||
|
||||
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.UpdateChaptersDownloadedJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
70
API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.cs
Normal file
70
API/Migrations/pgsql/20250516122242_AltTitle-Owned-WithId.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AltTitleOwnedWithId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_MangaAltTitle",
|
||||
table: "MangaAltTitle");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Id",
|
||||
table: "MangaAltTitle");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AltTitleId",
|
||||
table: "MangaAltTitle",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_MangaAltTitle",
|
||||
table: "MangaAltTitle",
|
||||
column: "AltTitleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MangaAltTitle_MangaId",
|
||||
table: "MangaAltTitle",
|
||||
column: "MangaId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_MangaAltTitle",
|
||||
table: "MangaAltTitle");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MangaAltTitle_MangaId",
|
||||
table: "MangaAltTitle");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AltTitleId",
|
||||
table: "MangaAltTitle");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Id",
|
||||
table: "MangaAltTitle",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_MangaAltTitle",
|
||||
table: "MangaAltTitle",
|
||||
columns: new[] { "MangaId", "Id" });
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,720 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Schema.Contexts;
|
||||
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.pgsql
|
||||
{
|
||||
[DbContext(typeof(PgsqlContext))]
|
||||
[Migration("20250516180953_Split-UpdateChaptersDownloadedJob-Into-UpdateSingleChapterDownloadedJob")]
|
||||
partial class SplitUpdateChaptersDownloadedJobIntoUpdateSingleChapterDownloadedJob
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.Author", b =>
|
||||
{
|
||||
b.Property<string>("AuthorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
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>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
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.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
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("ParentJobId");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
|
||||
b.HasDiscriminator<byte>("JobType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
||||
{
|
||||
b.Property<string>("LocalLibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("LocalLibraryId");
|
||||
|
||||
b.ToTable("LocalLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Property<string>("MangaId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<long?>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("MangaId");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("BaseUris")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.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")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.Property<string>("DependsOnJobsJobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("JobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.ToTable("JobJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
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()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ToLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)3);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ToLibraryId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasIndex("ToLibraryId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("MoveMangaLibraryJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)7);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
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.UpdateChaptersDownloadedJob", 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("UpdateChaptersDownloadedJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)6);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("ChapterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("ChapterId")
|
||||
.HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)8);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("ComickIo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("ParentJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaConnectorName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("API.Schema.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("LinkId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.HasKey("LinkId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("AltTitleId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("AltTitleId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("MangaAltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DependsOnJobsJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.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.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToLibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("ToLibrary");
|
||||
});
|
||||
|
||||
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.UpdateChaptersDownloadedJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Chapter", "Chapter")
|
||||
.WithMany()
|
||||
.HasForeignKey("ChapterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SplitUpdateChaptersDownloadedJobIntoUpdateSingleChapterDownloadedJob : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "UpdateFilesDownloadedJob_MangaId",
|
||||
table: "Jobs",
|
||||
newName: "UpdateChaptersDownloadedJob_MangaId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Jobs_UpdateFilesDownloadedJob_MangaId",
|
||||
table: "Jobs",
|
||||
newName: "IX_Jobs_UpdateChaptersDownloadedJob_MangaId");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs",
|
||||
column: "UpdateSingleChapterDownloadedJob_ChapterId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs",
|
||||
column: "UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
principalTable: "Chapters",
|
||||
principalColumn: "ChapterId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Jobs_Mangas_UpdateChaptersDownloadedJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "UpdateChaptersDownloadedJob_MangaId",
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Jobs_Mangas_UpdateChaptersDownloadedJob_MangaId",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "UpdateChaptersDownloadedJob_MangaId",
|
||||
table: "Jobs",
|
||||
newName: "UpdateFilesDownloadedJob_MangaId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Jobs_UpdateChaptersDownloadedJob_MangaId",
|
||||
table: "Jobs",
|
||||
newName: "IX_Jobs_UpdateFilesDownloadedJob_MangaId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Jobs_Mangas_UpdateFilesDownloadedJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "UpdateFilesDownloadedJob_MangaId",
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
724
API/Migrations/pgsql/20250518142903_Chapter-IdOnConnectorSite.Designer.cs
generated
Normal file
724
API/Migrations/pgsql/20250518142903_Chapter-IdOnConnectorSite.Designer.cs
generated
Normal file
@ -0,0 +1,724 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Schema.Contexts;
|
||||
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.pgsql
|
||||
{
|
||||
[DbContext(typeof(PgsqlContext))]
|
||||
[Migration("20250518142903_Chapter-IdOnConnectorSite")]
|
||||
partial class ChapterIdOnConnectorSite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.Author", b =>
|
||||
{
|
||||
b.Property<string>("AuthorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
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>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
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.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
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("ParentJobId");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
|
||||
b.HasDiscriminator<byte>("JobType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
||||
{
|
||||
b.Property<string>("LocalLibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("LocalLibraryId");
|
||||
|
||||
b.ToTable("LocalLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Property<string>("MangaId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<long?>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("MangaId");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("BaseUris")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.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")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.Property<string>("DependsOnJobsJobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("JobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.ToTable("JobJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
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()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ToLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)3);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ToLibraryId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasIndex("ToLibraryId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("MoveMangaLibraryJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)7);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
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.UpdateChaptersDownloadedJob", 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("UpdateChaptersDownloadedJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)6);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("ChapterId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("ChapterId")
|
||||
.HasColumnName("UpdateSingleChapterDownloadedJob_ChapterId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)8);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("ComickIo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("ParentJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaConnectorName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("API.Schema.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("LinkId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.HasKey("LinkId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("AltTitleId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("AltTitleId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("MangaAltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DependsOnJobsJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.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.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToLibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("ToLibrary");
|
||||
});
|
||||
|
||||
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.UpdateChaptersDownloadedJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateSingleChapterDownloadedJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Chapter", "Chapter")
|
||||
.WithMany()
|
||||
.HasForeignKey("ChapterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ChapterIdOnConnectorSite : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "IdOnConnectorSite",
|
||||
table: "Chapters",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IdOnConnectorSite",
|
||||
table: "Chapters");
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
50
API/Migrations/pgsql/20250518161710_UpdateCoverJob.cs
Normal file
50
API/Migrations/pgsql/20250518161710_UpdateCoverJob.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UpdateCoverJob : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "UpdateCoverJob_MangaId",
|
||||
table: "Jobs",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_UpdateCoverJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "UpdateCoverJob_MangaId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Jobs_Mangas_UpdateCoverJob_MangaId",
|
||||
table: "Jobs",
|
||||
column: "UpdateCoverJob_MangaId",
|
||||
principalTable: "Mangas",
|
||||
principalColumn: "MangaId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Jobs_Mangas_UpdateCoverJob_MangaId",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Jobs_UpdateCoverJob_MangaId",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UpdateCoverJob_MangaId",
|
||||
table: "Jobs");
|
||||
}
|
||||
}
|
||||
}
|
724
API/Migrations/pgsql/20250518183729_Remove-UpdateSingleChapterDownloaded-Job.Designer.cs
generated
Normal file
724
API/Migrations/pgsql/20250518183729_Remove-UpdateSingleChapterDownloaded-Job.Designer.cs
generated
Normal file
@ -0,0 +1,724 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Schema.Contexts;
|
||||
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.pgsql
|
||||
{
|
||||
[DbContext(typeof(PgsqlContext))]
|
||||
[Migration("20250518183729_Remove-UpdateSingleChapterDownloaded-Job")]
|
||||
partial class RemoveUpdateSingleChapterDownloadedJob
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.Author", b =>
|
||||
{
|
||||
b.Property<string>("AuthorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
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>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
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.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
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("ParentJobId");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
|
||||
b.HasDiscriminator<byte>("JobType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
||||
{
|
||||
b.Property<string>("LocalLibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("LocalLibraryId");
|
||||
|
||||
b.ToTable("LocalLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Property<string>("MangaId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<long?>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("MangaId");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("BaseUris")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.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")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.Property<string>("DependsOnJobsJobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("JobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.ToTable("JobJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
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()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ToLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)3);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ToLibraryId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasIndex("ToLibraryId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("MoveMangaLibraryJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)7);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
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.UpdateChaptersDownloadedJob", 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("UpdateChaptersDownloadedJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)6);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", 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("UpdateCoverJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)9);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("ComickIo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("ParentJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaConnectorName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("API.Schema.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("LinkId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.HasKey("LinkId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("AltTitleId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("AltTitleId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("MangaAltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DependsOnJobsJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.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.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToLibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("ToLibrary");
|
||||
});
|
||||
|
||||
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.UpdateChaptersDownloadedJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveUpdateSingleChapterDownloadedJob : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Jobs_UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs",
|
||||
column: "UpdateSingleChapterDownloadedJob_ChapterId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Jobs_Chapters_UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
table: "Jobs",
|
||||
column: "UpdateSingleChapterDownloadedJob_ChapterId",
|
||||
principalTable: "Chapters",
|
||||
principalColumn: "ChapterId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
721
API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
Normal file
721
API/Migrations/pgsql/PgsqlContextModelSnapshot.cs
Normal file
@ -0,0 +1,721 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations.pgsql
|
||||
{
|
||||
[DbContext(typeof(PgsqlContext))]
|
||||
partial class PgsqlContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Schema.Author", b =>
|
||||
{
|
||||
b.Property<string>("AuthorId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("AuthorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
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>("ChapterNumber")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<bool>("Downloaded")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ParentMangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
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.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
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("ParentJobId");
|
||||
|
||||
b.ToTable("Jobs");
|
||||
|
||||
b.HasDiscriminator<byte>("JobType");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.LocalLibrary", b =>
|
||||
{
|
||||
b.Property<string>("LocalLibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("LibraryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("LocalLibraryId");
|
||||
|
||||
b.ToTable("LocalLibraries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Property<string>("MangaId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("CoverFileNameInCache")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DirectoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("IdOnConnectorSite")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<float>("IgnoreChaptersBefore")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<string>("LibraryId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaConnectorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("OriginalLanguage")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b.Property<byte>("ReleaseStatus")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("WebsiteUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<long?>("Year")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("MangaId");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.HasIndex("MangaConnectorName");
|
||||
|
||||
b.ToTable("Mangas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("BaseUris")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.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")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Tag");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.Property<string>("AuthorIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("AuthorIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("AuthorToManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.Property<string>("DependsOnJobsJobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("JobId")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("DependsOnJobsJobId", "JobId");
|
||||
|
||||
b.HasIndex("JobId");
|
||||
|
||||
b.ToTable("JobJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.Property<string>("MangaTagIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("MangaIds")
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("MangaTagIds", "MangaIds");
|
||||
|
||||
b.HasIndex("MangaIds");
|
||||
|
||||
b.ToTable("MangaTagToManga");
|
||||
});
|
||||
|
||||
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()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ToLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)3);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("ToLibraryId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasIndex("MangaId");
|
||||
|
||||
b.HasIndex("ToLibraryId");
|
||||
|
||||
b.ToTable("Jobs", t =>
|
||||
{
|
||||
t.Property("MangaId")
|
||||
.HasColumnName("MoveMangaLibraryJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)7);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.RetrieveChaptersJob", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.Jobs.Job");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
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.UpdateChaptersDownloadedJob", 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("UpdateChaptersDownloadedJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)6);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", 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("UpdateCoverJob_MangaId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue((byte)9);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.ComickIo", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("ComickIo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("Global");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
||||
{
|
||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||
|
||||
b.HasDiscriminator().HasValue("MangaDex");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
||||
.WithMany("Chapters")
|
||||
.HasForeignKey("ParentMangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentManga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("ParentJob");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.LocalLibrary", "Library")
|
||||
.WithMany()
|
||||
.HasForeignKey("LibraryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaConnectorName")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("API.Schema.Link", "Links", b1 =>
|
||||
{
|
||||
b1.Property<string>("LinkId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("LinkUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.HasKey("LinkId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("Link");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.OwnsMany("API.Schema.MangaAltTitle", "AltTitles", b1 =>
|
||||
{
|
||||
b1.Property<string>("AltTitleId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("character varying(8)");
|
||||
|
||||
b1.Property<string>("MangaId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b1.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b1.HasKey("AltTitleId");
|
||||
|
||||
b1.HasIndex("MangaId");
|
||||
|
||||
b1.ToTable("MangaAltTitle");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MangaId");
|
||||
});
|
||||
|
||||
b.Navigation("AltTitles");
|
||||
|
||||
b.Navigation("Library");
|
||||
|
||||
b.Navigation("Links");
|
||||
|
||||
b.Navigation("MangaConnector");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AuthorToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Author", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DependsOnJobsJobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.Jobs.Job", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("JobId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MangaTagToManga", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaIds")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.MangaTag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaTagIds")
|
||||
.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.MoveMangaLibraryJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Schema.LocalLibrary", "ToLibrary")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToLibraryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
|
||||
b.Navigation("ToLibrary");
|
||||
});
|
||||
|
||||
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.UpdateChaptersDownloadedJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Jobs.UpdateCoverJob", b =>
|
||||
{
|
||||
b.HasOne("API.Schema.Manga", "Manga")
|
||||
.WithMany()
|
||||
.HasForeignKey("MangaId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Manga");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||
{
|
||||
b.Navigation("Chapters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
namespace API;
|
||||
|
||||
public record ProblemResponse(string title, string? message = null);
|
123
API/Program.cs
123
API/Program.cs
@ -1,13 +1,15 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
using API;
|
||||
using API.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using API.Schema.Jobs;
|
||||
using API.Schema.MangaConnectors;
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning.Builder;
|
||||
using Asp.Versioning.Conventions;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@ -25,25 +27,27 @@ builder.Services.AddCors(options =>
|
||||
});
|
||||
|
||||
builder.Services.AddApiVersioning(option =>
|
||||
{
|
||||
option.AssumeDefaultVersionWhenUnspecified = true;
|
||||
option.DefaultApiVersion = new ApiVersion(2);
|
||||
option.ReportApiVersions = true;
|
||||
option.ApiVersionReader = ApiVersionReader.Combine(
|
||||
new UrlSegmentApiVersionReader(),
|
||||
new QueryStringApiVersionReader("api-version"),
|
||||
new HeaderApiVersionReader("X-Version"),
|
||||
new MediaTypeApiVersionReader("x-version"));
|
||||
})
|
||||
.AddMvc(options =>
|
||||
{
|
||||
options.Conventions.Add(new VersionByNamespaceConvention());
|
||||
})
|
||||
.AddApiExplorer(options => {
|
||||
options.GroupNameFormat = "'v'V";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
{
|
||||
option.AssumeDefaultVersionWhenUnspecified = true;
|
||||
option.DefaultApiVersion = new ApiVersion(2);
|
||||
option.ReportApiVersions = true;
|
||||
option.ApiVersionReader = ApiVersionReader.Combine(
|
||||
new UrlSegmentApiVersionReader(),
|
||||
new QueryStringApiVersionReader("api-version"),
|
||||
new HeaderApiVersionReader("X-Version"),
|
||||
new MediaTypeApiVersionReader("x-version"));
|
||||
})
|
||||
.AddMvc(options =>
|
||||
{
|
||||
options.Conventions.Add(new VersionByNamespaceConvention());
|
||||
})
|
||||
.AddApiExplorer(options =>
|
||||
{
|
||||
options.GroupNameFormat = "'v'V";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGenNewtonsoftSupport();
|
||||
builder.Services.AddSwaggerGen(opt =>
|
||||
{
|
||||
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
@ -51,16 +55,28 @@ builder.Services.AddSwaggerGen(opt =>
|
||||
});
|
||||
builder.Services.ConfigureOptions<NamedSwaggerGenOptions>();
|
||||
|
||||
builder.Services.AddDbContext<PgsqlContext>(options =>
|
||||
options.UseNpgsql($"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST")??"localhost:5432"}; " +
|
||||
$"Database={Environment.GetEnvironmentVariable("POSTGRES_DB")??"postgres"}; " +
|
||||
$"Username={Environment.GetEnvironmentVariable("POSTGRES_USER")??"postgres"}; " +
|
||||
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")??"postgres"}"));
|
||||
string ConnectionString = $"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "localhost:5432"}; " +
|
||||
$"Database={Environment.GetEnvironmentVariable("POSTGRES_DB") ?? "postgres"}; " +
|
||||
$"Username={Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "postgres"}; " +
|
||||
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "postgres"}";
|
||||
|
||||
builder.Services.AddDbContext<PgsqlContext>(options =>
|
||||
options.UseNpgsql(ConnectionString));
|
||||
builder.Services.AddDbContext<NotificationsContext>(options =>
|
||||
options.UseNpgsql(ConnectionString));
|
||||
builder.Services.AddDbContext<LibraryContext>(options =>
|
||||
options.UseNpgsql(ConnectionString));
|
||||
|
||||
builder.Services.AddControllers(options =>
|
||||
{
|
||||
options.AllowEmptyInputInBodyModelBinding = true;
|
||||
});
|
||||
builder.Services.AddControllers().AddNewtonsoftJson(opts =>
|
||||
{
|
||||
opts.SerializerSettings.Converters.Add(new StringEnumConverter());
|
||||
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
||||
});
|
||||
builder.Services.AddScoped<ILog>(opts => LogManager.GetLogger("API"));
|
||||
|
||||
builder.WebHost.UseUrls("http://*:6531");
|
||||
|
||||
@ -87,33 +103,42 @@ app.UseSwaggerUI(options =>
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
||||
db.Database.Migrate();
|
||||
}
|
||||
app.UseMiddleware<RequestTimeMiddleware>();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
using (IServiceScope scope = app.Services.CreateScope())
|
||||
{
|
||||
PgsqlContext context = scope.ServiceProvider.GetService<PgsqlContext>()!;
|
||||
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
||||
context.Database.Migrate();
|
||||
|
||||
MangaConnector[] connectors =
|
||||
[
|
||||
new AsuraToon(),
|
||||
new Bato(),
|
||||
new MangaDex(),
|
||||
new MangaHere(),
|
||||
new MangaKatana(),
|
||||
new MangaLife(),
|
||||
new Manganato(),
|
||||
new Mangaworld(),
|
||||
new ManhuaPlus(),
|
||||
new Weebcentral()
|
||||
];
|
||||
[
|
||||
new MangaDex(),
|
||||
new ComickIo(),
|
||||
new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
|
||||
];
|
||||
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
|
||||
context.MangaConnectors.AddRange(newConnectors);
|
||||
|
||||
if (!context.LocalLibraries.Any())
|
||||
context.LocalLibraries.Add(new LocalLibrary(TrangaSettings.downloadLocation, "Default Library"));
|
||||
|
||||
context.Jobs.AddRange(context.Jobs.Where(j => j.JobType == JobType.DownloadAvailableChaptersJob)
|
||||
.Include(downloadAvailableChaptersJob => ((DownloadAvailableChaptersJob)downloadAvailableChaptersJob).Manga)
|
||||
.ToList()
|
||||
.Select(dacj => new UpdateChaptersDownloadedJob(((DownloadAvailableChaptersJob)dacj).Manga, 0, dacj)));
|
||||
context.Jobs.RemoveRange(context.Jobs.Where(j => j.state == JobState.Completed && j.RecurrenceMs < 1));
|
||||
foreach (Job job in context.Jobs.Where(j => j.state == JobState.Running))
|
||||
{
|
||||
job.state = JobState.FirstExecution;
|
||||
job.LastExecution = DateTime.UnixEpoch;
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
using (IServiceScope scope = app.Services.CreateScope())
|
||||
{
|
||||
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
|
||||
context.Database.Migrate();
|
||||
|
||||
string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
|
||||
context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High));
|
||||
@ -124,8 +149,14 @@ using (var scope = app.Services.CreateScope())
|
||||
|
||||
TrangaSettings.Load();
|
||||
Tranga.StartLogger();
|
||||
Tranga.JobStarterThread.Start(app.Services.CreateScope().ServiceProvider.GetService<PgsqlContext>());
|
||||
Tranga.NotificationSenderThread.Start(app.Services.CreateScope().ServiceProvider.GetService<PgsqlContext>());
|
||||
|
||||
using (IServiceScope scope = app.Services.CreateScope())
|
||||
{
|
||||
PgsqlContext context = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
||||
Tranga.RemoveStaleFiles(context);
|
||||
}
|
||||
Tranga.JobStarterThread.Start(app.Services);
|
||||
//Tranga.NotificationSenderThread.Start(app.Services); //TODO RE-ENABLE
|
||||
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
|
@ -6,7 +6,15 @@ namespace API.Schema;
|
||||
[PrimaryKey("AuthorId")]
|
||||
public class Author(string authorName)
|
||||
{
|
||||
[MaxLength(64)]
|
||||
public string AuthorId { get; init; } = TokenGen.CreateToken(typeof(Author), 64);
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string AuthorId { get; init; } = TokenGen.CreateToken(typeof(Author), authorName);
|
||||
[StringLength(128)]
|
||||
[Required]
|
||||
public string AuthorName { get; init; } = authorName;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{AuthorId} {AuthorName}";
|
||||
}
|
||||
}
|
@ -1,113 +1,197 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using API.Schema.Jobs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema;
|
||||
|
||||
[PrimaryKey("ChapterId")]
|
||||
public class Chapter : IComparable<Chapter>
|
||||
{
|
||||
[MaxLength(64)]
|
||||
public string ChapterId { get; init; } = TokenGen.CreateToken(typeof(Chapter), 64);
|
||||
[StringLength(64)] [Required] public string ChapterId { get; init; }
|
||||
|
||||
[StringLength(256)]public string? IdOnConnectorSite { get; init; }
|
||||
public string ParentMangaId { get; init; }
|
||||
[JsonIgnore] public Manga ParentManga { get; init; } = null!;
|
||||
|
||||
public int? VolumeNumber { get; private set; }
|
||||
public ChapterNumber ChapterNumber { get; private set; }
|
||||
public string Url { get; internal set; }
|
||||
public string? Title { get; private set; }
|
||||
public string ArchiveFileName { get; private set; }
|
||||
public bool Downloaded { get; internal set; } = false;
|
||||
|
||||
public string ParentMangaId { get; internal set; }
|
||||
public Manga? ParentManga { get; init; }
|
||||
[StringLength(10)] [Required] public string ChapterNumber { get; private set; }
|
||||
|
||||
public Chapter(Manga parentManga, string url, ChapterNumber chapterNumber, int? volumeNumber = null, string? title = null)
|
||||
: this(parentManga.MangaId, url, chapterNumber, volumeNumber, title)
|
||||
[StringLength(2048)] [Required] [Url] public string Url { get; internal set; }
|
||||
|
||||
[StringLength(256)] public string? Title { get; private set; }
|
||||
|
||||
[StringLength(256)] [Required] public string FileName { get; private set; }
|
||||
|
||||
[Required] public bool Downloaded { get; internal set; }
|
||||
[NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
|
||||
|
||||
public Chapter(Manga parentManga, string url, string chapterNumber, int? volumeNumber = null, string? idOnConnectorSite = null, string? title = null)
|
||||
{
|
||||
this.ChapterId = TokenGen.CreateToken(typeof(Chapter), parentManga.MangaId, chapterNumber);
|
||||
this.IdOnConnectorSite = idOnConnectorSite;
|
||||
this.ParentMangaId = parentManga.MangaId;
|
||||
this.ParentManga = parentManga;
|
||||
}
|
||||
|
||||
public Chapter(string parentMangaId, string url, ChapterNumber chapterNumber,
|
||||
int? volumeNumber = null, string? title = null)
|
||||
{
|
||||
this.ParentMangaId = parentMangaId;
|
||||
this.VolumeNumber = volumeNumber;
|
||||
this.ChapterNumber = chapterNumber;
|
||||
this.Url = url;
|
||||
this.ChapterNumber = chapterNumber;
|
||||
this.VolumeNumber = volumeNumber;
|
||||
this.Title = title;
|
||||
this.ArchiveFileName = BuildArchiveFileName();
|
||||
this.FileName = GetArchiveFilePath();
|
||||
this.Downloaded = false;
|
||||
}
|
||||
|
||||
public MoveFileOrFolderJob? UpdateChapterNumber(ChapterNumber chapterNumber)
|
||||
{
|
||||
this.ChapterNumber = chapterNumber;
|
||||
return UpdateArchiveFileName();
|
||||
}
|
||||
|
||||
public MoveFileOrFolderJob? UpdateVolumeNumber(int? volumeNumber)
|
||||
{
|
||||
this.VolumeNumber = volumeNumber;
|
||||
return UpdateArchiveFileName();
|
||||
}
|
||||
|
||||
public MoveFileOrFolderJob? UpdateTitle(string? title)
|
||||
{
|
||||
this.Title = title;
|
||||
return UpdateArchiveFileName();
|
||||
}
|
||||
|
||||
private string BuildArchiveFileName()
|
||||
{
|
||||
return $"{this.ParentManga.Name} - Vol.{this.VolumeNumber ?? 0} Ch.{this.ChapterNumber}{(this.Title is null ? "" : $" - {this.Title}")}.cbz";
|
||||
}
|
||||
|
||||
private MoveFileOrFolderJob? UpdateArchiveFileName()
|
||||
{
|
||||
string oldPath = GetArchiveFilePath();
|
||||
this.ArchiveFileName = BuildArchiveFileName();
|
||||
if (Downloaded)
|
||||
{
|
||||
return new MoveFileOrFolderJob(oldPath, GetArchiveFilePath());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates full file path of chapter-archive
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
/// <returns>Filepath</returns>
|
||||
internal string GetArchiveFilePath()
|
||||
internal Chapter(string chapterId, string parentMangaId, int? volumeNumber, string chapterNumber, string url, string? idOnConnectorSite, string? title, string fileName, bool downloaded)
|
||||
{
|
||||
return Path.Join(TrangaSettings.downloadLocation, ParentManga.FolderName, ArchiveFileName);
|
||||
}
|
||||
|
||||
public bool IsDownloaded()
|
||||
{
|
||||
string path = GetArchiveFilePath();
|
||||
return File.Exists(path);
|
||||
this.ChapterId = chapterId;
|
||||
this.IdOnConnectorSite = idOnConnectorSite;
|
||||
this.ParentMangaId = parentMangaId;
|
||||
this.VolumeNumber = volumeNumber;
|
||||
this.ChapterNumber = chapterNumber;
|
||||
this.Url = url;
|
||||
this.Title = title;
|
||||
this.FileName = fileName;
|
||||
this.Downloaded = downloaded;
|
||||
}
|
||||
|
||||
public int CompareTo(Chapter? other)
|
||||
{
|
||||
if(other is not { } otherChapter)
|
||||
if (other is not { } otherChapter)
|
||||
throw new ArgumentException($"{other} can not be compared to {this}");
|
||||
return this.VolumeNumber?.CompareTo(otherChapter.VolumeNumber) switch
|
||||
return VolumeNumber?.CompareTo(otherChapter.VolumeNumber) switch
|
||||
{
|
||||
<0 => -1,
|
||||
>0 => 1,
|
||||
_ => this.ChapterNumber.CompareTo(otherChapter.ChapterNumber)
|
||||
< 0 => -1,
|
||||
> 0 => 1,
|
||||
_ => CompareChapterNumbers(ChapterNumber, otherChapter.ChapterNumber)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks the filesystem if an archive at the ArchiveFilePath exists
|
||||
/// </summary>
|
||||
/// <returns>True if archive exists on disk</returns>
|
||||
public bool CheckDownloaded() => File.Exists(FullArchiveFilePath);
|
||||
|
||||
/// Placeholders:
|
||||
/// %M Manga Name
|
||||
/// %V Volume
|
||||
/// %C Chapter
|
||||
/// %T Title
|
||||
/// %A Author (first in list)
|
||||
/// %I Chapter Internal ID
|
||||
/// %i Manga Internal ID
|
||||
/// %Y Year (Manga)
|
||||
private static readonly Regex NullableRex = new(@"\?([a-zA-Z])\(([^\)]*)\)|(.+?)");
|
||||
private static readonly Regex ReplaceRexx = new(@"%([a-zA-Z])|(.+?)");
|
||||
private string GetArchiveFilePath()
|
||||
{
|
||||
string archiveNamingScheme = TrangaSettings.chapterNamingScheme;
|
||||
StringBuilder stringBuilder = new();
|
||||
foreach (Match nullable in NullableRex.Matches(archiveNamingScheme))
|
||||
{
|
||||
if (nullable.Groups[3].Success)
|
||||
{
|
||||
stringBuilder.Append(nullable.Groups[3].Value);
|
||||
continue;
|
||||
}
|
||||
|
||||
char placeholder = nullable.Groups[1].Value[0];
|
||||
bool isNull = placeholder switch
|
||||
{
|
||||
'M' => ParentManga?.Name is null,
|
||||
'V' => VolumeNumber is null,
|
||||
'C' => ChapterNumber is null,
|
||||
'T' => Title is null,
|
||||
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName is null,
|
||||
'I' => ChapterId is null,
|
||||
'i' => ParentManga?.MangaId is null,
|
||||
'Y' => ParentManga?.Year is null,
|
||||
_ => true
|
||||
};
|
||||
if(!isNull)
|
||||
stringBuilder.Append(nullable.Groups[2].Value);
|
||||
}
|
||||
|
||||
string checkedString = stringBuilder.ToString();
|
||||
stringBuilder = new();
|
||||
|
||||
foreach (Match replace in ReplaceRexx.Matches(checkedString))
|
||||
{
|
||||
if (replace.Groups[2].Success)
|
||||
{
|
||||
stringBuilder.Append(replace.Groups[2].Value);
|
||||
continue;
|
||||
}
|
||||
|
||||
char placeholder = replace.Groups[1].Value[0];
|
||||
string? value = placeholder switch
|
||||
{
|
||||
'M' => ParentManga?.Name,
|
||||
'V' => VolumeNumber?.ToString(),
|
||||
'C' => ChapterNumber,
|
||||
'T' => Title,
|
||||
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName,
|
||||
'I' => ChapterId,
|
||||
'i' => ParentManga?.MangaId,
|
||||
'Y' => ParentManga?.Year.ToString(),
|
||||
_ => null
|
||||
};
|
||||
stringBuilder.Append(value);
|
||||
}
|
||||
|
||||
stringBuilder.Append(".cbz");
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private static int CompareChapterNumbers(string ch1, string ch2)
|
||||
{
|
||||
int[] ch1Arr = ch1.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray();
|
||||
int[] ch2Arr = ch2.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray();
|
||||
|
||||
if (ch1Arr.Contains(-1) || ch2Arr.Contains(-1))
|
||||
throw new ArgumentException("Chapter number is not in correct format");
|
||||
|
||||
int i = 0, j = 0;
|
||||
|
||||
while (i < ch1Arr.Length && j < ch2Arr.Length)
|
||||
{
|
||||
if (ch1Arr[i] < ch2Arr[j])
|
||||
return -1;
|
||||
if (ch1Arr[i] > ch2Arr[j])
|
||||
return 1;
|
||||
i++;
|
||||
j++;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
internal string GetComicInfoXmlString()
|
||||
{
|
||||
XElement comicInfo = new XElement("ComicInfo",
|
||||
new XElement("Tags", string.Join(',', ParentManga.Tags.Select(tag => tag.Tag))),
|
||||
new XElement("LanguageISO", ParentManga.OriginalLanguage),
|
||||
new XElement("Title", this.Title),
|
||||
new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))),
|
||||
new XElement("Volume", this.VolumeNumber),
|
||||
new XElement("Number", this.ChapterNumber)
|
||||
XElement comicInfo = new("ComicInfo",
|
||||
new XElement("Number", ChapterNumber)
|
||||
);
|
||||
if(Title is not null)
|
||||
comicInfo.Add(new XElement("Title", Title));
|
||||
if(ParentManga.MangaTags.Count > 0)
|
||||
comicInfo.Add(new XElement("Tags", string.Join(',', ParentManga.MangaTags.Select(tag => tag.Tag))));
|
||||
if(VolumeNumber is not null)
|
||||
comicInfo.Add(new XElement("Volume", VolumeNumber));
|
||||
if(ParentManga.Authors.Count > 0)
|
||||
comicInfo.Add(new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))));
|
||||
if(ParentManga.OriginalLanguage is not null)
|
||||
comicInfo.Add(new XElement("LanguageISO", ParentManga.OriginalLanguage));
|
||||
return comicInfo.ToString();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ChapterId} Vol.{VolumeNumber} Ch.{ChapterNumber} - {Title}";
|
||||
}
|
||||
}
|
@ -1,305 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace API.Schema;
|
||||
|
||||
public readonly struct ChapterNumber : INumber<ChapterNumber>
|
||||
{
|
||||
private readonly uint[] _numbers;
|
||||
private readonly bool _naN;
|
||||
|
||||
private ChapterNumber(uint[] numbers, bool naN = false)
|
||||
{
|
||||
this._numbers = numbers;
|
||||
this._naN = naN;
|
||||
}
|
||||
|
||||
public ChapterNumber(string number)
|
||||
{
|
||||
if (!CanParse(number))
|
||||
{
|
||||
this._numbers = [];
|
||||
this._naN = true;
|
||||
}
|
||||
this._numbers = number.Split('.').Select(uint.Parse).ToArray();
|
||||
}
|
||||
|
||||
public ChapterNumber(float number) : this(number.ToString("F")) {}
|
||||
|
||||
public ChapterNumber(double number) : this((float)number) {}
|
||||
|
||||
public ChapterNumber(uint number)
|
||||
{
|
||||
this._numbers = [number];
|
||||
this._naN = false;
|
||||
}
|
||||
|
||||
public ChapterNumber(int number)
|
||||
{
|
||||
if (int.IsNegative(number))
|
||||
{
|
||||
this._numbers = [];
|
||||
this._naN = true;
|
||||
}
|
||||
this._numbers = [(uint)number];
|
||||
this._naN = false;
|
||||
}
|
||||
|
||||
public int CompareTo(ChapterNumber other)
|
||||
{
|
||||
byte index = 0;
|
||||
do
|
||||
{
|
||||
if (this._numbers[index] < other._numbers[index])
|
||||
return -1;
|
||||
else if (this._numbers[index] > other._numbers[index])
|
||||
return 1;
|
||||
}while(index < this._numbers.Length && index < other._numbers.Length);
|
||||
|
||||
if (index >= this._numbers.Length && index >= other._numbers.Length)
|
||||
return 0;
|
||||
else if (index >= this._numbers.Length)
|
||||
return -1;
|
||||
else if (index >= other._numbers.Length)
|
||||
return 1;
|
||||
throw new UnreachableException();
|
||||
}
|
||||
|
||||
private static readonly Regex Pattern = new(@"[0-9]+(?:\.[0-9]+)*");
|
||||
public static bool CanParse(string? number) => number is not null && Pattern.Match(number).Length == number.Length && number.Length > 0;
|
||||
|
||||
public bool Equals(ChapterNumber other) => CompareTo(other) == 0;
|
||||
|
||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||
{
|
||||
return string.Join('.', _numbers);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ChapterNumber other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(_numbers, _naN);
|
||||
}
|
||||
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public int CompareTo(object? obj)
|
||||
{
|
||||
if(obj is ChapterNumber other)
|
||||
return CompareTo(other);
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
public static ChapterNumber Parse(string s, IFormatProvider? provider)
|
||||
{
|
||||
if(!CanParse(s))
|
||||
throw new FormatException($"Invalid ChapterNumber-String: {s}");
|
||||
return new ChapterNumber(s);
|
||||
}
|
||||
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out ChapterNumber result)
|
||||
{
|
||||
result = new ChapterNumber([], true);;
|
||||
if (!CanParse(s))
|
||||
return false;
|
||||
if (s is null)
|
||||
return false;
|
||||
result = new ChapterNumber(s);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ChapterNumber Parse(ReadOnlySpan<char> s, IFormatProvider? provider) => Parse(s.ToString(), provider);
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, out ChapterNumber result) => TryParse(s.ToString(), provider, out result);
|
||||
|
||||
public static ChapterNumber operator +(ChapterNumber left, ChapterNumber right)
|
||||
{
|
||||
if (IsNaN(left) || IsNaN(right))
|
||||
return new ChapterNumber([], true);
|
||||
int size = left._numbers.Length > right._numbers.Length ? left._numbers.Length : right._numbers.Length;
|
||||
uint[] numbers = new uint[size];
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
if(left._numbers.Length < i)
|
||||
numbers[i] = right._numbers[i];
|
||||
else if(right._numbers.Length < i)
|
||||
numbers[i] = left._numbers[i];
|
||||
else
|
||||
numbers[i] = left._numbers[i] + right._numbers[i];
|
||||
}
|
||||
return new ChapterNumber(numbers);
|
||||
}
|
||||
|
||||
private static bool BothNotNaN(ChapterNumber left, ChapterNumber right) => !IsNaN(left) && !IsNaN(right);
|
||||
|
||||
public static ChapterNumber AdditiveIdentity => Zero;
|
||||
|
||||
public static bool operator ==(ChapterNumber left, ChapterNumber right) => BothNotNaN(left, right) && left.Equals(right);
|
||||
|
||||
public static bool operator !=(ChapterNumber left, ChapterNumber right) => !(left == right);
|
||||
|
||||
public static bool operator >(ChapterNumber left, ChapterNumber right) => BothNotNaN(left, right) && left.CompareTo(right) > 0;
|
||||
|
||||
public static bool operator >=(ChapterNumber left, ChapterNumber right) => BothNotNaN(left, right) && left.CompareTo(right) >= 0;
|
||||
|
||||
public static bool operator <(ChapterNumber left, ChapterNumber right) => BothNotNaN(left, right) && left.CompareTo(right) < 0;
|
||||
|
||||
public static bool operator <=(ChapterNumber left, ChapterNumber right) => BothNotNaN(left, right) && left.CompareTo(right) <= 0;
|
||||
|
||||
public static ChapterNumber operator %(ChapterNumber left, ChapterNumber right) => throw new ArithmeticException();
|
||||
|
||||
public static ChapterNumber operator +(ChapterNumber value) => throw new InvalidOperationException();
|
||||
|
||||
public static ChapterNumber operator --(ChapterNumber value)
|
||||
{
|
||||
if (IsNaN(value))
|
||||
return value;
|
||||
uint[] numbers = value._numbers;
|
||||
numbers[0]--;
|
||||
return new ChapterNumber(numbers);
|
||||
}
|
||||
|
||||
public static ChapterNumber operator /(ChapterNumber left, ChapterNumber right) => throw new InvalidOperationException();
|
||||
|
||||
public static ChapterNumber operator ++(ChapterNumber value)
|
||||
{
|
||||
if (IsNaN(value))
|
||||
return value;
|
||||
uint[] numbers = value._numbers;
|
||||
numbers[0]++;
|
||||
return new ChapterNumber(numbers);
|
||||
}
|
||||
|
||||
public static ChapterNumber MultiplicativeIdentity => One;
|
||||
public static ChapterNumber operator *(ChapterNumber left, ChapterNumber right) => throw new InvalidOperationException();
|
||||
|
||||
public static ChapterNumber operator -(ChapterNumber left, ChapterNumber right) => throw new InvalidOperationException();
|
||||
|
||||
public static ChapterNumber operator -(ChapterNumber value) => throw new InvalidOperationException();
|
||||
|
||||
public static ChapterNumber Abs(ChapterNumber value) => value;
|
||||
|
||||
public static bool IsCanonical(ChapterNumber value) => true;
|
||||
|
||||
public static bool IsComplexNumber(ChapterNumber value) => false;
|
||||
|
||||
public static bool IsEvenInteger(ChapterNumber value) => IsInteger(value) && uint.IsEvenInteger(value._numbers[0]);
|
||||
|
||||
public static bool IsFinite(ChapterNumber value) => true;
|
||||
|
||||
public static bool IsImaginaryNumber(ChapterNumber value) => false;
|
||||
|
||||
public static bool IsInfinity(ChapterNumber value) => false;
|
||||
|
||||
public static bool IsInteger(ChapterNumber value) => !IsNaN(value) && value._numbers.Length == 1;
|
||||
|
||||
public static bool IsNaN(ChapterNumber value) => value._naN;
|
||||
|
||||
public static bool IsNegative(ChapterNumber value) => false;
|
||||
|
||||
public static bool IsNegativeInfinity(ChapterNumber value) => false;
|
||||
|
||||
public static bool IsNormal(ChapterNumber value) => true;
|
||||
|
||||
public static bool IsOddInteger(ChapterNumber value) => false;
|
||||
|
||||
public static bool IsPositive(ChapterNumber value) => true;
|
||||
|
||||
public static bool IsPositiveInfinity(ChapterNumber value) => false;
|
||||
|
||||
public static bool IsRealNumber(ChapterNumber value) => false;
|
||||
|
||||
public static bool IsSubnormal(ChapterNumber value) => false;
|
||||
|
||||
public static bool IsZero(ChapterNumber value) => value._numbers.All(n => n == 0);
|
||||
|
||||
public static ChapterNumber MaxMagnitude(ChapterNumber x, ChapterNumber y)
|
||||
{
|
||||
if(IsNaN(x))
|
||||
return new ChapterNumber([], true);
|
||||
if (IsNaN(y))
|
||||
return new ChapterNumber([], true);
|
||||
return x >= y ? x : y;
|
||||
}
|
||||
|
||||
public static ChapterNumber MaxMagnitudeNumber(ChapterNumber x, ChapterNumber y)
|
||||
{
|
||||
if (IsNaN(x))
|
||||
return y;
|
||||
if (IsNaN(y))
|
||||
return x;
|
||||
return x >= y ? x : y;
|
||||
}
|
||||
|
||||
public static ChapterNumber MinMagnitude(ChapterNumber x, ChapterNumber y)
|
||||
{
|
||||
if(IsNaN(x))
|
||||
return new ChapterNumber([], true);
|
||||
if (IsNaN(y))
|
||||
return new ChapterNumber([], true);
|
||||
return x <= y ? x : y;
|
||||
}
|
||||
|
||||
public static ChapterNumber MinMagnitudeNumber(ChapterNumber x, ChapterNumber y)
|
||||
{
|
||||
if (IsNaN(x))
|
||||
return y;
|
||||
if (IsNaN(y))
|
||||
return x;
|
||||
return x <= y ? x : y;
|
||||
}
|
||||
|
||||
public static ChapterNumber Parse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider) => throw new NotImplementedException();
|
||||
|
||||
public static ChapterNumber Parse(string s, NumberStyles style, IFormatProvider? provider) => throw new NotImplementedException();
|
||||
|
||||
public static bool TryConvertFromChecked<TOther>(TOther value, out ChapterNumber result) where TOther : INumberBase<TOther>
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static bool TryConvertFromSaturating<TOther>(TOther value, out ChapterNumber result) where TOther : INumberBase<TOther>
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static bool TryConvertFromTruncating<TOther>(TOther value, out ChapterNumber result) where TOther : INumberBase<TOther>
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static bool TryConvertToChecked<TOther>(ChapterNumber value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static bool TryConvertToSaturating<TOther>(ChapterNumber value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static bool TryConvertToTruncating<TOther>(ChapterNumber value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider, out ChapterNumber result)
|
||||
=> TryParse(s.ToString(), style, provider, out result);
|
||||
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out ChapterNumber result)
|
||||
=> TryParse(s, provider, out result);
|
||||
|
||||
public static ChapterNumber One => new(1);
|
||||
public static int Radix => 10;
|
||||
public static ChapterNumber Zero => new(0);
|
||||
}
|
32
API/Schema/Contexts/LibraryContext.cs
Normal file
32
API/Schema/Contexts/LibraryContext.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using API.Schema.LibraryConnectors;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
|
||||
namespace API.Schema.Contexts;
|
||||
|
||||
public class LibraryContext(DbContextOptions<LibraryContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<LibraryConnector> LibraryConnectors { get; set; }
|
||||
|
||||
private ILog Log => LogManager.GetLogger(GetType());
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
optionsBuilder.EnableSensitiveDataLogging();
|
||||
optionsBuilder.LogTo(s =>
|
||||
{
|
||||
Log.Debug(s);
|
||||
}, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
//LibraryConnector Types
|
||||
modelBuilder.Entity<LibraryConnector>()
|
||||
.HasDiscriminator(l => l.LibraryType)
|
||||
.HasValue<Komga>(LibraryType.Komga)
|
||||
.HasValue<Kavita>(LibraryType.Kavita);
|
||||
}
|
||||
}
|
23
API/Schema/Contexts/NotificationsContext.cs
Normal file
23
API/Schema/Contexts/NotificationsContext.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using API.Schema.NotificationConnectors;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
|
||||
namespace API.Schema.Contexts;
|
||||
|
||||
public class NotificationsContext(DbContextOptions<NotificationsContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<NotificationConnector> NotificationConnectors { get; set; }
|
||||
public DbSet<Notification> Notifications { get; set; }
|
||||
|
||||
private ILog Log => LogManager.GetLogger(GetType());
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
optionsBuilder.EnableSensitiveDataLogging();
|
||||
optionsBuilder.LogTo(s =>
|
||||
{
|
||||
Log.Debug(s);
|
||||
}, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
|
||||
}
|
||||
}
|
195
API/Schema/Contexts/PgsqlContext.cs
Normal file
195
API/Schema/Contexts/PgsqlContext.cs
Normal file
@ -0,0 +1,195 @@
|
||||
using API.Schema.Jobs;
|
||||
using API.Schema.MangaConnectors;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
|
||||
namespace API.Schema.Contexts;
|
||||
|
||||
public class PgsqlContext(DbContextOptions<PgsqlContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<Job> Jobs { get; set; }
|
||||
public DbSet<MangaConnector> MangaConnectors { get; set; }
|
||||
public DbSet<Manga> Mangas { get; set; }
|
||||
public DbSet<LocalLibrary> LocalLibraries { get; set; }
|
||||
public DbSet<Chapter> Chapters { get; set; }
|
||||
public DbSet<Author> Authors { get; set; }
|
||||
public DbSet<MangaTag> Tags { get; set; }
|
||||
private ILog Log => LogManager.GetLogger(GetType());
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
optionsBuilder.EnableSensitiveDataLogging();
|
||||
optionsBuilder.LogTo(s =>
|
||||
{
|
||||
Log.Debug(s);
|
||||
}, [DbLoggerCategory.Query.Name], LogLevel.Trace, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
//Job Types
|
||||
modelBuilder.Entity<Job>()
|
||||
.HasDiscriminator(j => j.JobType)
|
||||
.HasValue<MoveFileOrFolderJob>(JobType.MoveFileOrFolderJob)
|
||||
.HasValue<MoveMangaLibraryJob>(JobType.MoveMangaLibraryJob)
|
||||
.HasValue<DownloadAvailableChaptersJob>(JobType.DownloadAvailableChaptersJob)
|
||||
.HasValue<DownloadSingleChapterJob>(JobType.DownloadSingleChapterJob)
|
||||
.HasValue<DownloadMangaCoverJob>(JobType.DownloadMangaCoverJob)
|
||||
.HasValue<RetrieveChaptersJob>(JobType.RetrieveChaptersJob)
|
||||
.HasValue<UpdateCoverJob>(JobType.UpdateCoverJob)
|
||||
.HasValue<UpdateChaptersDownloadedJob>(JobType.UpdateChaptersDownloadedJob);
|
||||
|
||||
//Job specification
|
||||
modelBuilder.Entity<DownloadAvailableChaptersJob>()
|
||||
.HasOne<Manga>(j => j.Manga)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.MangaId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<DownloadAvailableChaptersJob>()
|
||||
.Navigation(j => j.Manga)
|
||||
.EnableLazyLoading();
|
||||
modelBuilder.Entity<DownloadMangaCoverJob>()
|
||||
.HasOne<Manga>(j => j.Manga)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.MangaId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<DownloadMangaCoverJob>()
|
||||
.Navigation(j => j.Manga)
|
||||
.EnableLazyLoading();
|
||||
modelBuilder.Entity<DownloadSingleChapterJob>()
|
||||
.HasOne<Chapter>(j => j.Chapter)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.ChapterId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<DownloadSingleChapterJob>()
|
||||
.Navigation(j => j.Chapter)
|
||||
.EnableLazyLoading();
|
||||
modelBuilder.Entity<MoveMangaLibraryJob>()
|
||||
.HasOne<Manga>(j => j.Manga)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.MangaId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MoveMangaLibraryJob>()
|
||||
.Navigation(j => j.Manga)
|
||||
.EnableLazyLoading();
|
||||
modelBuilder.Entity<MoveMangaLibraryJob>()
|
||||
.HasOne<LocalLibrary>(j => j.ToLibrary)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.ToLibraryId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<MoveMangaLibraryJob>()
|
||||
.Navigation(j => j.ToLibrary)
|
||||
.EnableLazyLoading();
|
||||
modelBuilder.Entity<RetrieveChaptersJob>()
|
||||
.HasOne<Manga>(j => j.Manga)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.MangaId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<RetrieveChaptersJob>()
|
||||
.Navigation(j => j.Manga)
|
||||
.EnableLazyLoading();
|
||||
modelBuilder.Entity<UpdateChaptersDownloadedJob>()
|
||||
.HasOne<Manga>(j => j.Manga)
|
||||
.WithMany()
|
||||
.HasForeignKey(j => j.MangaId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<UpdateChaptersDownloadedJob>()
|
||||
.Navigation(j => j.Manga)
|
||||
.EnableLazyLoading();
|
||||
|
||||
//Job has possible ParentJob
|
||||
modelBuilder.Entity<Job>()
|
||||
.HasOne<Job>(childJob => childJob.ParentJob)
|
||||
.WithMany()
|
||||
.HasForeignKey(childjob => childjob.ParentJobId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
//Job might be dependent on other Jobs
|
||||
modelBuilder.Entity<Job>()
|
||||
.HasMany<Job>(root => root.DependsOnJobs)
|
||||
.WithMany();
|
||||
modelBuilder.Entity<Job>()
|
||||
.Navigation(j => j.DependsOnJobs)
|
||||
.AutoInclude(false)
|
||||
.EnableLazyLoading();
|
||||
|
||||
//MangaConnector Types
|
||||
modelBuilder.Entity<MangaConnector>()
|
||||
.HasDiscriminator(c => c.Name)
|
||||
.HasValue<Global>("Global")
|
||||
.HasValue<MangaDex>("MangaDex")
|
||||
.HasValue<ComickIo>("ComickIo");
|
||||
//MangaConnector is responsible for many Manga
|
||||
modelBuilder.Entity<MangaConnector>()
|
||||
.HasMany<Manga>()
|
||||
.WithOne(m => m.MangaConnector)
|
||||
.HasForeignKey(m => m.MangaConnectorName)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.MangaConnector)
|
||||
.AutoInclude();
|
||||
|
||||
//Manga has many Chapters
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasMany<Chapter>(m => m.Chapters)
|
||||
.WithOne(c => c.ParentManga)
|
||||
.HasForeignKey(c => c.ParentMangaId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<Chapter>()
|
||||
.Navigation(c => c.ParentManga)
|
||||
.AutoInclude();
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Chapters)
|
||||
.AutoInclude(false)
|
||||
.EnableLazyLoading();
|
||||
//Manga owns MangaAltTitles
|
||||
modelBuilder.Entity<Manga>()
|
||||
.OwnsMany<MangaAltTitle>(m => m.AltTitles)
|
||||
.WithOwner();
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.AltTitles)
|
||||
.AutoInclude();
|
||||
//Manga owns Links
|
||||
modelBuilder.Entity<Manga>()
|
||||
.OwnsMany<Link>(m => m.Links)
|
||||
.WithOwner();
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Links)
|
||||
.AutoInclude();
|
||||
//Manga has many Tags associated with many Manga
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasMany<MangaTag>(m => m.MangaTags)
|
||||
.WithMany()
|
||||
.UsingEntity("MangaTagToManga",
|
||||
l=> l.HasOne(typeof(MangaTag)).WithMany().HasForeignKey("MangaTagIds").HasPrincipalKey(nameof(MangaTag.Tag)),
|
||||
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.MangaId)),
|
||||
j => j.HasKey("MangaTagIds", "MangaIds")
|
||||
);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.MangaTags)
|
||||
.AutoInclude();
|
||||
//Manga has many Authors associated with many Manga
|
||||
modelBuilder.Entity<Manga>()
|
||||
.HasMany<Author>(m => m.Authors)
|
||||
.WithMany()
|
||||
.UsingEntity("AuthorToManga",
|
||||
l=> l.HasOne(typeof(Author)).WithMany().HasForeignKey("AuthorIds").HasPrincipalKey(nameof(Author.AuthorId)),
|
||||
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.MangaId)),
|
||||
j => j.HasKey("AuthorIds", "MangaIds")
|
||||
);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Authors)
|
||||
.AutoInclude();
|
||||
|
||||
//LocalLibrary has many Mangas
|
||||
modelBuilder.Entity<LocalLibrary>()
|
||||
.HasMany<Manga>()
|
||||
.WithOne(m => m.Library)
|
||||
.HasForeignKey(m => m.LibraryId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
modelBuilder.Entity<Manga>()
|
||||
.Navigation(m => m.Library)
|
||||
.AutoInclude();
|
||||
}
|
||||
}
|
42
API/Schema/Jobs/DownloadAvailableChaptersJob.cs
Normal file
42
API/Schema/Jobs/DownloadAvailableChaptersJob.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class DownloadAvailableChaptersJob : Job
|
||||
{
|
||||
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||
|
||||
private Manga _manga = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public Manga Manga
|
||||
{
|
||||
get => LazyLoader.Load(this, ref _manga);
|
||||
init => _manga = value;
|
||||
}
|
||||
|
||||
public DownloadAvailableChaptersJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(DownloadAvailableChaptersJob)), JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.MangaId = manga.MangaId;
|
||||
this.Manga = manga;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal DownloadAvailableChaptersJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string? parentJobId)
|
||||
: base(lazyLoader, jobId, JobType.DownloadAvailableChaptersJob, recurrenceMs, parentJobId)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
context.Entry(Manga).Reference<LocalLibrary>(m => m.Library).Load();
|
||||
return Manga.Chapters.Where(c => c.Downloaded == false).Select(chapter => new DownloadSingleChapterJob(chapter, this));
|
||||
}
|
||||
}
|
@ -1,136 +1,51 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema.MangaConnectors;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Processing.Processors.Binarization;
|
||||
using static System.IO.UnixFileMode;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class DownloadMangaCoverJob(string chapterId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
||||
: Job(TokenGen.CreateToken(typeof(DownloadMangaCoverJob), 64), JobType.DownloadMangaCoverJob, 0, parentJobId, dependsOnJobsIds)
|
||||
public class DownloadMangaCoverJob : Job
|
||||
{
|
||||
[MaxLength(64)]
|
||||
public string ChapterId { get; init; } = chapterId;
|
||||
public Chapter? Chapter { get; init; }
|
||||
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||
|
||||
private Manga _manga = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public Manga Manga
|
||||
{
|
||||
get => LazyLoader.Load(this, ref _manga);
|
||||
init => _manga = value;
|
||||
}
|
||||
|
||||
public DownloadMangaCoverJob(Manga manga, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(DownloadMangaCoverJob)), JobType.DownloadMangaCoverJob, 0, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.MangaId = manga.MangaId;
|
||||
this.Manga = manga;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal DownloadMangaCoverJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string? parentJobId)
|
||||
: base(lazyLoader, jobId, JobType.DownloadMangaCoverJob, recurrenceMs, parentJobId)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
MangaConnector connector = Chapter.ParentManga?.MangaConnector ?? context.MangaConnectors.Find(context.Manga.Find(Chapter.ParentMangaId)?.MangaId)!;
|
||||
DownloadChapterImages(Chapter, connector);
|
||||
try
|
||||
{
|
||||
Manga.CoverFileNameInCache = Manga.MangaConnector.SaveCoverImageToCache(Manga);
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private bool DownloadChapterImages(Chapter chapter, MangaConnector connector)
|
||||
{
|
||||
string[] imageUrls = connector.GetChapterImageUrls(Chapter);
|
||||
string saveArchiveFilePath = chapter.GetArchiveFilePath();
|
||||
|
||||
//Check if Publication Directory already exists
|
||||
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
||||
if (!Directory.Exists(directoryPath))
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
Directory.CreateDirectory(directoryPath,
|
||||
UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute );
|
||||
else
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
|
||||
if (File.Exists(saveArchiveFilePath)) //Don't download twice. Redownload
|
||||
File.Delete(saveArchiveFilePath);
|
||||
|
||||
//Create a temporary folder to store images
|
||||
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
|
||||
|
||||
int chapterNum = 0;
|
||||
//Download all Images to temporary Folder
|
||||
if (imageUrls.Length == 0)
|
||||
{
|
||||
Directory.Delete(tempFolder, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (string imageUrl in imageUrls)
|
||||
{
|
||||
string extension = imageUrl.Split('.')[^1].Split('?')[0];
|
||||
string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
|
||||
bool status = DownloadImage(imageUrl, imagePath);
|
||||
if (status is false)
|
||||
return false;
|
||||
}
|
||||
|
||||
CopyCoverFromCacheToDownloadLocation();
|
||||
|
||||
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
|
||||
|
||||
//ZIP-it and ship-it
|
||||
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
|
||||
Directory.Delete(tempFolder, true); //Cleanup
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ProcessImage(string imagePath)
|
||||
{
|
||||
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
|
||||
return;
|
||||
DateTime start = DateTime.Now;
|
||||
using Image image = Image.Load(imagePath);
|
||||
File.Delete(imagePath);
|
||||
if(TrangaSettings.bwImages)
|
||||
image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor()));
|
||||
image.SaveAsJpeg(imagePath, new JpegEncoder()
|
||||
{
|
||||
Quality = TrangaSettings.compression
|
||||
});
|
||||
}
|
||||
|
||||
private void CopyCoverFromCacheToDownloadLocation(int? retries = 1)
|
||||
{
|
||||
//Check if Publication already has a Folder and cover
|
||||
string publicationFolder = Chapter.ParentManga.CreatePublicationFolder();
|
||||
DirectoryInfo dirInfo = new (publicationFolder);
|
||||
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string? fileInCache = Chapter.ParentManga.CoverFileNameInCache;
|
||||
if (fileInCache is null || !File.Exists(fileInCache))
|
||||
{
|
||||
if (retries > 0 && Chapter.ParentManga.CoverUrl is not null)
|
||||
{
|
||||
Chapter.ParentManga.SaveCoverImageToCache();
|
||||
CopyCoverFromCacheToDownloadLocation(--retries);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
||||
File.Copy(fileInCache, newFilePath, true);
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite);
|
||||
}
|
||||
|
||||
private bool DownloadImage(string imageUrl, string savePath)
|
||||
{
|
||||
HttpDownloadClient downloadClient = new();
|
||||
RequestResult requestResult = downloadClient.MakeRequest(imageUrl, RequestType.MangaImage);
|
||||
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||
return false;
|
||||
if (requestResult.result == Stream.Null)
|
||||
return false;
|
||||
|
||||
FileStream fs = new (savePath, FileMode.Create);
|
||||
requestResult.result.CopyTo(fs);
|
||||
fs.Close();
|
||||
ProcessImage(savePath);
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.MangaConnectors;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class DownloadNewChaptersJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
||||
: Job(TokenGen.CreateToken(typeof(DownloadNewChaptersJob), 64), JobType.DownloadNewChaptersJob, recurrenceMs, parentJobId, dependsOnJobsIds)
|
||||
{
|
||||
[MaxLength(64)]
|
||||
public string MangaId { get; init; } = mangaId;
|
||||
public Manga? Manga { get; init; }
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
Manga m = Manga ?? context.Manga.Find(MangaId)!;
|
||||
MangaConnector connector = m.MangaConnector ?? context.MangaConnectors.Find(m.MangaConnectorId)!;
|
||||
Chapter[] newChapters = connector.GetNewChapters(m);
|
||||
context.Chapters.AddRangeAsync(newChapters).Wait();
|
||||
context.SaveChangesAsync().Wait();
|
||||
return newChapters.Select(chapter => new DownloadSingleChapterJob(chapter.ChapterId, this.JobId));
|
||||
}
|
||||
}
|
@ -2,7 +2,10 @@
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema.MangaConnectors;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
@ -11,77 +14,127 @@ using static System.IO.UnixFileMode;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
||||
: Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob), 64), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobsIds)
|
||||
public class DownloadSingleChapterJob : Job
|
||||
{
|
||||
[MaxLength(64)]
|
||||
public string ChapterId { get; init; } = chapterId;
|
||||
public Chapter? Chapter { get; init; }
|
||||
[StringLength(64)] [Required] public string ChapterId { get; init; }
|
||||
|
||||
private Chapter _chapter = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public Chapter Chapter
|
||||
{
|
||||
get => LazyLoader.Load(this, ref _chapter);
|
||||
init => _chapter = value;
|
||||
}
|
||||
|
||||
public DownloadSingleChapterJob(Chapter chapter, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(DownloadSingleChapterJob)), JobType.DownloadSingleChapterJob, 0, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.ChapterId = chapter.ChapterId;
|
||||
this.Chapter = chapter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal DownloadSingleChapterJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string chapterId, string? parentJobId)
|
||||
: base(lazyLoader, jobId, JobType.DownloadSingleChapterJob, recurrenceMs, parentJobId)
|
||||
{
|
||||
this.ChapterId = chapterId;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
Chapter c = Chapter ?? context.Chapters.Find(ChapterId)!;
|
||||
Manga m = c.ParentManga ?? context.Manga.Find(c.ParentMangaId)!;
|
||||
MangaConnector connector = m.MangaConnector ?? context.MangaConnectors.Find(m.MangaConnectorId)!;
|
||||
DownloadChapterImages(c, connector, m);
|
||||
return [];
|
||||
}
|
||||
|
||||
private bool DownloadChapterImages(Chapter chapter, MangaConnector connector, Manga manga)
|
||||
{
|
||||
string[] imageUrls = connector.GetChapterImageUrls(chapter);
|
||||
string saveArchiveFilePath = chapter.GetArchiveFilePath();
|
||||
string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
|
||||
if (imageUrls.Length < 1)
|
||||
{
|
||||
Log.Info($"No imageUrls for chapter {ChapterId}");
|
||||
return [];
|
||||
}
|
||||
context.Entry(Chapter.ParentManga).Reference<LocalLibrary>(m => m.Library).Load(); //Need to explicitly load, because we are not accessing navigation directly...
|
||||
string saveArchiveFilePath = Chapter.FullArchiveFilePath;
|
||||
Log.Debug($"Chapter path: {saveArchiveFilePath}");
|
||||
|
||||
//Check if Publication Directory already exists
|
||||
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
|
||||
string? directoryPath = Path.GetDirectoryName(saveArchiveFilePath);
|
||||
if (directoryPath is null)
|
||||
{
|
||||
Log.Error($"Directory path could not be found: {saveArchiveFilePath}");
|
||||
this.state = JobState.Failed;
|
||||
return [];
|
||||
}
|
||||
if (!Directory.Exists(directoryPath))
|
||||
{
|
||||
Log.Info($"Creating publication Directory: {directoryPath}");
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
Directory.CreateDirectory(directoryPath,
|
||||
UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute );
|
||||
else
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
}
|
||||
|
||||
if (File.Exists(saveArchiveFilePath)) //Don't download twice. Redownload
|
||||
{
|
||||
Log.Info($"Archive {saveArchiveFilePath} already existed, but deleting and re-downloading.");
|
||||
File.Delete(saveArchiveFilePath);
|
||||
}
|
||||
|
||||
//Create a temporary folder to store images
|
||||
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
|
||||
Log.Debug($"Created temp folder: {tempFolder}");
|
||||
|
||||
Log.Info($"Downloading images: {ChapterId}");
|
||||
int chapterNum = 0;
|
||||
//Download all Images to temporary Folder
|
||||
if (imageUrls.Length == 0)
|
||||
{
|
||||
Directory.Delete(tempFolder, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (string imageUrl in imageUrls)
|
||||
{
|
||||
string extension = imageUrl.Split('.')[^1].Split('?')[0];
|
||||
string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
|
||||
bool status = DownloadImage(imageUrl, imagePath);
|
||||
if (status is false)
|
||||
return false;
|
||||
{
|
||||
Log.Error($"Failed to download image: {imageUrl}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
CopyCoverFromCacheToDownloadLocation(manga);
|
||||
|
||||
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
|
||||
CopyCoverFromCacheToDownloadLocation(Chapter.ParentManga);
|
||||
|
||||
Log.Debug($"Creating ComicInfo.xml {ChapterId}");
|
||||
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), Chapter.GetComicInfoXmlString());
|
||||
|
||||
Log.Debug($"Packaging images to archive {ChapterId}");
|
||||
//ZIP-it and ship-it
|
||||
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
|
||||
Directory.Delete(tempFolder, true); //Cleanup
|
||||
|
||||
return true;
|
||||
Chapter.Downloaded = true;
|
||||
context.SaveChanges();
|
||||
|
||||
if (context.Jobs.ToList().Any(j =>
|
||||
{
|
||||
if (j.JobType != JobType.UpdateChaptersDownloadedJob)
|
||||
return false;
|
||||
UpdateChaptersDownloadedJob job = (UpdateChaptersDownloadedJob)j;
|
||||
return job.MangaId == this.Chapter.ParentMangaId;
|
||||
}))
|
||||
return [];
|
||||
|
||||
return [new UpdateChaptersDownloadedJob(Chapter.ParentManga, 0, this.ParentJob)];
|
||||
}
|
||||
|
||||
private void ProcessImage(string imagePath)
|
||||
{
|
||||
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
|
||||
{
|
||||
Log.Debug($"No processing requested for image");
|
||||
return;
|
||||
DateTime start = DateTime.Now;
|
||||
}
|
||||
|
||||
Log.Debug($"Processing image: {imagePath}");
|
||||
|
||||
using Image image = Image.Load(imagePath);
|
||||
File.Delete(imagePath);
|
||||
if(TrangaSettings.bwImages)
|
||||
@ -92,31 +145,30 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
|
||||
});
|
||||
}
|
||||
|
||||
private void CopyCoverFromCacheToDownloadLocation(Manga manga, int? retries = 1)
|
||||
private void CopyCoverFromCacheToDownloadLocation(Manga manga)
|
||||
{
|
||||
//Check if Publication already has a Folder and cover
|
||||
string publicationFolder = manga.CreatePublicationFolder();
|
||||
DirectoryInfo dirInfo = new (publicationFolder);
|
||||
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
Log.Debug($"Cover already exists at {publicationFolder}");
|
||||
return;
|
||||
}
|
||||
|
||||
string? fileInCache = manga.CoverFileNameInCache;
|
||||
if (fileInCache is null || !File.Exists(fileInCache))
|
||||
Log.Info($"Copying cover to {publicationFolder}");
|
||||
string? fileInCache = manga.CoverFileNameInCache ?? manga.MangaConnector.SaveCoverImageToCache(manga);
|
||||
if (fileInCache is null)
|
||||
{
|
||||
if (retries > 0)
|
||||
{
|
||||
manga.SaveCoverImageToCache();
|
||||
CopyCoverFromCacheToDownloadLocation(manga, --retries);
|
||||
}
|
||||
|
||||
Log.Error($"File {fileInCache} does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
||||
File.Copy(fileInCache, newFilePath, true);
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite);
|
||||
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite | OtherRead | OtherWrite);
|
||||
Log.Debug($"Copied cover from {fileInCache} to {newFilePath}");
|
||||
}
|
||||
|
||||
private bool DownloadImage(string imageUrl, string savePath)
|
||||
@ -129,7 +181,7 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
|
||||
if (requestResult.result == Stream.Null)
|
||||
return false;
|
||||
|
||||
FileStream fs = new (savePath, FileMode.Create);
|
||||
FileStream fs = new (savePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
requestResult.result.CopyTo(fs);
|
||||
fs.Close();
|
||||
ProcessImage(savePath);
|
||||
|
@ -1,6 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using API.Schema.Contexts;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
@ -8,48 +11,126 @@ namespace API.Schema.Jobs;
|
||||
[PrimaryKey("JobId")]
|
||||
public abstract class Job
|
||||
{
|
||||
[MaxLength(64)]
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string JobId { get; init; }
|
||||
|
||||
[MaxLength(64)]
|
||||
public string? ParentJobId { get; init; }
|
||||
public Job? ParentJob { get; init; }
|
||||
|
||||
[MaxLength(64)]
|
||||
public ICollection<string>? DependsOnJobsIds { get; init; }
|
||||
public ICollection<Job>? DependsOnJobs { get; init; }
|
||||
|
||||
public JobType JobType { get; init; }
|
||||
public ulong RecurrenceMs { get; set; }
|
||||
public DateTime LastExecution { get; internal set; } = DateTime.UnixEpoch;
|
||||
|
||||
[NotMapped]
|
||||
public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs);
|
||||
public JobState state { get; internal set; } = JobState.Waiting;
|
||||
|
||||
public Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList())
|
||||
[StringLength(64)] public string? ParentJobId { get; private set; }
|
||||
[JsonIgnore] public Job? ParentJob { get; internal set; }
|
||||
private ICollection<Job> _dependsOnJobs = null!;
|
||||
[JsonIgnore] public ICollection<Job> DependsOnJobs
|
||||
{
|
||||
get => LazyLoader.Load(this, ref _dependsOnJobs);
|
||||
init => _dependsOnJobs = value;
|
||||
}
|
||||
|
||||
[Required] public JobType JobType { get; init; }
|
||||
|
||||
[Required] public ulong RecurrenceMs { get; set; }
|
||||
|
||||
[Required] public DateTime LastExecution { get; internal set; } = DateTime.UnixEpoch;
|
||||
|
||||
[NotMapped] [Required] public DateTime NextExecution => LastExecution.AddMilliseconds(RecurrenceMs);
|
||||
[Required] public JobState state { get; internal set; } = JobState.FirstExecution;
|
||||
[Required] public bool Enabled { get; internal set; } = true;
|
||||
|
||||
[JsonIgnore] [NotMapped] internal bool IsCompleted => state is >= (JobState)128 and < (JobState)192;
|
||||
|
||||
[NotMapped] [JsonIgnore] protected ILog Log { get; init; }
|
||||
[NotMapped] [JsonIgnore] protected ILazyLoader LazyLoader { get; init; }
|
||||
|
||||
protected Job(string jobId, JobType jobType, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
{
|
||||
this.JobId = jobId;
|
||||
this.JobType = jobType;
|
||||
this.RecurrenceMs = recurrenceMs;
|
||||
this.ParentJobId = parentJob?.JobId;
|
||||
this.ParentJob = parentJob;
|
||||
this.DependsOnJobs = dependsOnJobs;
|
||||
this.DependsOnJobs = dependsOnJobs ?? [];
|
||||
|
||||
this.Log = LogManager.GetLogger(this.GetType());
|
||||
}
|
||||
|
||||
public Job(string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
protected internal Job(ILazyLoader lazyLoader, string jobId, JobType jobType, ulong recurrenceMs, string? parentJobId)
|
||||
{
|
||||
JobId = jobId;
|
||||
ParentJobId = parentJobId;
|
||||
DependsOnJobsIds = dependsOnJobsIds;
|
||||
JobType = jobType;
|
||||
RecurrenceMs = recurrenceMs;
|
||||
this.LazyLoader = lazyLoader;
|
||||
this.JobId = jobId;
|
||||
this.JobType = jobType;
|
||||
this.RecurrenceMs = recurrenceMs;
|
||||
this.ParentJobId = parentJobId;
|
||||
this.DependsOnJobs = [];
|
||||
|
||||
this.Log = LogManager.GetLogger(this.GetType());
|
||||
}
|
||||
|
||||
public IEnumerable<Job> Run(PgsqlContext context)
|
||||
public IEnumerable<Job> Run(PgsqlContext context, ref bool running)
|
||||
{
|
||||
this.state = JobState.Running;
|
||||
IEnumerable<Job> newJobs = RunInternal(context);
|
||||
this.state = JobState.Completed;
|
||||
return newJobs;
|
||||
Log.Info($"Running job {JobId}");
|
||||
DateTime jobStart = DateTime.UtcNow;
|
||||
Job[]? ret = null;
|
||||
|
||||
try
|
||||
{
|
||||
this.state = JobState.Running;
|
||||
context.SaveChanges();
|
||||
running = true;
|
||||
ret = RunInternal(context).ToArray();
|
||||
Log.Info($"Job {JobId} completed. Generated {ret.Length} new jobs.");
|
||||
this.state = this.RecurrenceMs > 0 ? JobState.CompletedWaiting : JobState.Completed;
|
||||
this.LastExecution = DateTime.UtcNow;
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is not DbUpdateException)
|
||||
{
|
||||
Log.Error($"Failed to run job {JobId}", e);
|
||||
this.state = JobState.Failed;
|
||||
this.Enabled = false;
|
||||
this.LastExecution = DateTime.UtcNow;
|
||||
context.SaveChanges();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"Failed to update Database {JobId}", e);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (ret != null)
|
||||
{
|
||||
context.Jobs.AddRange(ret);
|
||||
context.SaveChanges();
|
||||
}
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error($"Failed to update Database {JobId}", e);
|
||||
}
|
||||
|
||||
Log.Info($"Finished Job {JobId}! (took {DateTime.UtcNow.Subtract(jobStart).TotalMilliseconds}ms)");
|
||||
return ret ?? [];
|
||||
}
|
||||
|
||||
protected abstract IEnumerable<Job> RunInternal(PgsqlContext context);
|
||||
|
||||
public List<Job> GetDependenciesAndSelf()
|
||||
{
|
||||
List<Job> ret = new ();
|
||||
foreach (Job job in DependsOnJobs)
|
||||
{
|
||||
ret.AddRange(job.GetDependenciesAndSelf());
|
||||
}
|
||||
ret.Add(this);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{JobId}";
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using JsonSerializer = Newtonsoft.Json.JsonSerializer;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class JobJsonDeserializer : JsonConverter<Job>
|
||||
{
|
||||
public override bool CanWrite { get; } = false;
|
||||
|
||||
public override void WriteJson(JsonWriter writer, Job? value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Job? ReadJson(JsonReader reader, Type objectType, Job? existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject j = JObject.Load(reader);
|
||||
JobType? type = Enum.Parse<JobType>(j.GetValue("jobType")!.Value<string>()!);
|
||||
return type switch
|
||||
{
|
||||
JobType.DownloadSingleChapterJob => j.ToObject<DownloadSingleChapterJob>(),
|
||||
JobType.DownloadNewChaptersJob => j.ToObject<DownloadNewChaptersJob>(),
|
||||
JobType.UpdateMetaDataJob => j.ToObject<UpdateMetadataJob>(),
|
||||
JobType.MoveFileOrFolderJob => j.ToObject<MoveFileOrFolderJob>(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
@ -1,8 +1,14 @@
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public enum JobState
|
||||
public enum JobState : byte
|
||||
{
|
||||
Waiting,
|
||||
Running,
|
||||
Completed
|
||||
//Values 0-63 Preparation Stages
|
||||
FirstExecution = 0,
|
||||
//64-127 Running Stages
|
||||
Running = 64,
|
||||
//128-191 Completion Stages
|
||||
Completed = 128,
|
||||
CompletedWaiting = 159,
|
||||
//192-255 Error stages
|
||||
Failed = 192
|
||||
}
|
@ -4,8 +4,11 @@
|
||||
public enum JobType : byte
|
||||
{
|
||||
DownloadSingleChapterJob = 0,
|
||||
DownloadNewChaptersJob = 1,
|
||||
UpdateMetaDataJob = 2,
|
||||
DownloadAvailableChaptersJob = 1,
|
||||
MoveFileOrFolderJob = 3,
|
||||
DownloadMangaCoverJob = 4
|
||||
DownloadMangaCoverJob = 4,
|
||||
RetrieveChaptersJob = 5,
|
||||
UpdateChaptersDownloadedJob = 6,
|
||||
MoveMangaLibraryJob = 7,
|
||||
UpdateCoverJob = 9,
|
||||
}
|
@ -1,13 +1,71 @@
|
||||
namespace API.Schema.Jobs;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
public class MoveFileOrFolderJob(string fromLocation, string toLocation, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
||||
: Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob), 64), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobsIds)
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class MoveFileOrFolderJob : Job
|
||||
{
|
||||
public string FromLocation { get; init; } = fromLocation;
|
||||
public string ToLocation { get; init; } = toLocation;
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string FromLocation { get; init; }
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string ToLocation { get; init; }
|
||||
|
||||
public MoveFileOrFolderJob(string fromLocation, string toLocation, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(MoveFileOrFolderJob)), JobType.MoveFileOrFolderJob, 0, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.FromLocation = fromLocation;
|
||||
this.ToLocation = toLocation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal MoveFileOrFolderJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string fromLocation, string toLocation, string? parentJobId)
|
||||
: base(lazyLoader, jobId, JobType.MoveFileOrFolderJob, recurrenceMs, parentJobId)
|
||||
{
|
||||
this.FromLocation = fromLocation;
|
||||
this.ToLocation = toLocation;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
try
|
||||
{
|
||||
FileInfo fi = new (FromLocation);
|
||||
if (!fi.Exists)
|
||||
{
|
||||
Log.Error($"File does not exist at {FromLocation}");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (File.Exists(ToLocation))//Do not override existing
|
||||
{
|
||||
Log.Error($"File already exists at {ToLocation}");
|
||||
return [];
|
||||
}
|
||||
if(fi.Attributes.HasFlag(FileAttributes.Directory))
|
||||
MoveDirectory(fi, ToLocation);
|
||||
else
|
||||
MoveFile(fi, ToLocation);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(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);
|
||||
}
|
||||
}
|
60
API/Schema/Jobs/MoveMangaLibraryJob.cs
Normal file
60
API/Schema/Jobs/MoveMangaLibraryJob.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class MoveMangaLibraryJob : Job
|
||||
{
|
||||
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||
|
||||
private Manga _manga = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public Manga Manga
|
||||
{
|
||||
get => LazyLoader.Load(this, ref _manga);
|
||||
init => _manga = value;
|
||||
}
|
||||
[StringLength(64)] [Required] public string ToLibraryId { get; init; }
|
||||
public LocalLibrary ToLibrary { get; init; } = null!;
|
||||
|
||||
public MoveMangaLibraryJob(Manga manga, LocalLibrary toLibrary, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(MoveMangaLibraryJob)), JobType.MoveMangaLibraryJob, 0, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.MangaId = manga.MangaId;
|
||||
this.Manga = manga;
|
||||
this.ToLibraryId = toLibrary.LocalLibraryId;
|
||||
this.ToLibrary = toLibrary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal MoveMangaLibraryJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string toLibraryId, string? parentJobId)
|
||||
: base(lazyLoader, jobId, JobType.MoveMangaLibraryJob, recurrenceMs, parentJobId)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
this.ToLibraryId = toLibraryId;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
context.Entry(Manga).Reference<LocalLibrary>(m => m.Library).Load();
|
||||
Dictionary<Chapter, string> oldPath = Manga.Chapters.ToDictionary(c => c, c => c.FullArchiveFilePath);
|
||||
Manga.Library = ToLibrary;
|
||||
try
|
||||
{
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return [];
|
||||
}
|
||||
|
||||
return Manga.Chapters.Select(c => new MoveFileOrFolderJob(oldPath[c], c.FullArchiveFilePath));
|
||||
}
|
||||
}
|
61
API/Schema/Jobs/RetrieveChaptersJob.cs
Normal file
61
API/Schema/Jobs/RetrieveChaptersJob.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class RetrieveChaptersJob : Job
|
||||
{
|
||||
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||
|
||||
private Manga _manga = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public Manga Manga
|
||||
{
|
||||
get => LazyLoader.Load(this, ref _manga);
|
||||
init => _manga = value;
|
||||
}
|
||||
[StringLength(8)] [Required] public string Language { get; private set; }
|
||||
|
||||
public RetrieveChaptersJob(Manga manga, string language, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(RetrieveChaptersJob)), JobType.RetrieveChaptersJob, recurrenceMs, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.MangaId = manga.MangaId;
|
||||
this.Manga = manga;
|
||||
this.Language = language;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal RetrieveChaptersJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string language, string? parentJobId)
|
||||
: base(lazyLoader, jobId, JobType.RetrieveChaptersJob, recurrenceMs, parentJobId)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
this.Language = language;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
// This gets all chapters that are not downloaded
|
||||
Chapter[] allChapters = Manga.MangaConnector.GetChapters(Manga, Language).DistinctBy(c => c.ChapterId).ToArray();
|
||||
Chapter[] newChapters = allChapters.Where(chapter => Manga.Chapters.Select(c => c.ChapterId).Contains(chapter.ChapterId) == false).ToArray();
|
||||
Log.Info($"{Manga.Chapters.Count} existing + {newChapters.Length} new chapters.");
|
||||
|
||||
try
|
||||
{
|
||||
foreach (Chapter newChapter in newChapters)
|
||||
Manga.Chapters.Add(newChapter);
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
56
API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
Normal file
56
API/Schema/Jobs/UpdateChaptersDownloadedJob.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class UpdateChaptersDownloadedJob : Job
|
||||
{
|
||||
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||
|
||||
private Manga _manga = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public Manga Manga
|
||||
{
|
||||
get => LazyLoader.Load(this, ref _manga);
|
||||
init => _manga = value;
|
||||
}
|
||||
|
||||
public UpdateChaptersDownloadedJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(UpdateChaptersDownloadedJob)), JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.MangaId = manga.MangaId;
|
||||
this.Manga = manga;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal UpdateChaptersDownloadedJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string? parentJobId)
|
||||
: base(lazyLoader, jobId, JobType.UpdateChaptersDownloadedJob, recurrenceMs, parentJobId)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
context.Entry(Manga).Reference<LocalLibrary>(m => m.Library).Load();
|
||||
foreach (Chapter mangaChapter in Manga.Chapters)
|
||||
{
|
||||
mangaChapter.Downloaded = mangaChapter.CheckDownloaded();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
65
API/Schema/Jobs/UpdateCoverJob.cs
Normal file
65
API/Schema/Jobs/UpdateCoverJob.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Schema.Contexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class UpdateCoverJob : Job
|
||||
{
|
||||
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||
|
||||
private Manga _manga = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public Manga Manga
|
||||
{
|
||||
get => LazyLoader.Load(this, ref _manga);
|
||||
init => _manga = value;
|
||||
}
|
||||
|
||||
|
||||
public UpdateCoverJob(Manga manga, ulong recurrenceMs, Job? parentJob = null, ICollection<Job>? dependsOnJobs = null)
|
||||
: base(TokenGen.CreateToken(typeof(UpdateCoverJob)), JobType.UpdateCoverJob, recurrenceMs, parentJob, dependsOnJobs)
|
||||
{
|
||||
this.MangaId = manga.MangaId;
|
||||
this.Manga = manga;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
internal UpdateCoverJob(ILazyLoader lazyLoader, string jobId, ulong recurrenceMs, string mangaId, string? parentJobId)
|
||||
: base(lazyLoader, jobId, JobType.UpdateCoverJob, recurrenceMs, parentJobId)
|
||||
{
|
||||
this.MangaId = mangaId;
|
||||
}
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
bool keepCover = context.Jobs
|
||||
.Any(job => job.JobType == JobType.DownloadAvailableChaptersJob
|
||||
&& ((DownloadAvailableChaptersJob)job).MangaId == MangaId);
|
||||
if (!keepCover)
|
||||
{
|
||||
if(File.Exists(Manga.CoverFileNameInCache))
|
||||
File.Delete(Manga.CoverFileNameInCache);
|
||||
try
|
||||
{
|
||||
Manga.CoverFileNameInCache = null;
|
||||
context.Jobs.Remove(this);
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (DbUpdateException e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return [new DownloadMangaCoverJob(Manga, this)];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Schema.Jobs;
|
||||
|
||||
public class UpdateMetadataJob(ulong recurrenceMs, string mangaId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
||||
: Job(TokenGen.CreateToken(typeof(UpdateMetadataJob), 64), JobType.UpdateMetaDataJob, recurrenceMs, parentJobId, dependsOnJobsIds)
|
||||
{
|
||||
[MaxLength(64)]
|
||||
public string MangaId { get; init; } = mangaId;
|
||||
public virtual Manga Manga { get; init; }
|
||||
|
||||
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ namespace API.Schema.LibraryConnectors;
|
||||
public class Kavita : LibraryConnector
|
||||
{
|
||||
|
||||
public Kavita(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Kavita), 64), LibraryType.Kavita, baseUrl, auth)
|
||||
public Kavita(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Kavita), baseUrl), LibraryType.Kavita, baseUrl, auth)
|
||||
{
|
||||
}
|
||||
|
||||
@ -53,13 +53,13 @@ public class Kavita : LibraryConnector
|
||||
protected override void UpdateLibraryInternal()
|
||||
{
|
||||
foreach (KavitaLibrary lib in GetLibraries())
|
||||
NetClient.MakePost($"{BaseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", Auth);
|
||||
NetClient.MakePost($"{BaseUrl}/api/ToLibrary/scan?libraryId={lib.id}", "Bearer", Auth);
|
||||
}
|
||||
|
||||
internal override bool Test()
|
||||
{
|
||||
foreach (KavitaLibrary lib in GetLibraries())
|
||||
if (NetClient.MakePost($"{BaseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", Auth))
|
||||
if (NetClient.MakePost($"{BaseUrl}/api/ToLibrary/scan?libraryId={lib.id}", "Bearer", Auth))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
@ -70,15 +70,17 @@ public class Kavita : LibraryConnector
|
||||
/// <returns>Array of KavitaLibrary</returns>
|
||||
private IEnumerable<KavitaLibrary> GetLibraries()
|
||||
{
|
||||
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/Library/libraries", "Bearer", Auth);
|
||||
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/ToLibrary/libraries", "Bearer", Auth);
|
||||
if (data == Stream.Null)
|
||||
{
|
||||
return Array.Empty<KavitaLibrary>();
|
||||
Log.Info("No libraries found");
|
||||
return [];
|
||||
}
|
||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
||||
if (result is null)
|
||||
{
|
||||
return Array.Empty<KavitaLibrary>();
|
||||
Log.Info("No libraries found");
|
||||
return [];
|
||||
}
|
||||
|
||||
List<KavitaLibrary> ret = new();
|
||||
|
@ -5,7 +5,7 @@ namespace API.Schema.LibraryConnectors;
|
||||
|
||||
public class Komga : LibraryConnector
|
||||
{
|
||||
public Komga(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Komga), 64), LibraryType.Komga,
|
||||
public Komga(string baseUrl, string auth) : base(TokenGen.CreateToken(typeof(Komga), baseUrl), LibraryType.Komga,
|
||||
baseUrl, auth)
|
||||
{
|
||||
}
|
||||
@ -38,12 +38,14 @@ public class Komga : LibraryConnector
|
||||
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/v1/libraries", "Basic", Auth);
|
||||
if (data == Stream.Null)
|
||||
{
|
||||
return Array.Empty<KomgaLibrary>();
|
||||
Log.Info("No libraries found");
|
||||
return [];
|
||||
}
|
||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
||||
if (result is null)
|
||||
{
|
||||
return Array.Empty<KomgaLibrary>();
|
||||
Log.Info("No libraries found");
|
||||
return [];
|
||||
}
|
||||
|
||||
HashSet<KomgaLibrary> ret = new();
|
||||
|
@ -1,18 +1,32 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.LibraryConnectors;
|
||||
|
||||
[PrimaryKey("LibraryConnectorId")]
|
||||
public abstract class LibraryConnector(string libraryConnectorId, LibraryType libraryType, string baseUrl, string auth)
|
||||
{
|
||||
[MaxLength(64)]
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string LibraryConnectorId { get; } = libraryConnectorId;
|
||||
|
||||
[Required]
|
||||
public LibraryType LibraryType { get; init; } = libraryType;
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
[Url]
|
||||
public string BaseUrl { get; init; } = baseUrl;
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string Auth { get; init; } = auth;
|
||||
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
protected ILog Log { get; init; } = LogManager.GetLogger($"{libraryType.ToString()} {baseUrl}");
|
||||
|
||||
protected abstract void UpdateLibraryInternal();
|
||||
internal abstract bool Test();
|
||||
}
|
@ -1,48 +1,52 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using log4net;
|
||||
|
||||
namespace API.Schema.LibraryConnectors;
|
||||
|
||||
public class NetClient
|
||||
{
|
||||
private static ILog Log = LogManager.GetLogger(typeof(NetClient));
|
||||
|
||||
public static Stream MakeRequest(string url, string authScheme, string auth)
|
||||
{
|
||||
Log.Debug($"Requesting {url}");
|
||||
HttpClient client = new();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, auth);
|
||||
|
||||
HttpRequestMessage requestMessage = new()
|
||||
{
|
||||
HttpClient client = new();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, auth);
|
||||
|
||||
HttpRequestMessage requestMessage = new ()
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(url)
|
||||
};
|
||||
try
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(url)
|
||||
};
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = client.Send(requestMessage);
|
||||
|
||||
HttpResponseMessage response = client.Send(requestMessage);
|
||||
|
||||
if (response.StatusCode is HttpStatusCode.Unauthorized &&
|
||||
response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
||||
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth);
|
||||
else if (response.IsSuccessStatusCode)
|
||||
return response.Content.ReadAsStream();
|
||||
else
|
||||
return Stream.Null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
case HttpRequestException:
|
||||
|
||||
break;
|
||||
default:
|
||||
throw;
|
||||
}
|
||||
if (response.StatusCode is HttpStatusCode.Unauthorized &&
|
||||
response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
||||
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth);
|
||||
else if (response.IsSuccessStatusCode)
|
||||
return response.Content.ReadAsStream();
|
||||
else
|
||||
return Stream.Null;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
case HttpRequestException:
|
||||
Log.Debug(e);
|
||||
break;
|
||||
default:
|
||||
throw;
|
||||
}
|
||||
Log.Info("Failed to make request");
|
||||
return Stream.Null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool MakePost(string url, string authScheme, string auth)
|
||||
public static bool MakePost(string url, string authScheme, string auth)
|
||||
{
|
||||
HttpClient client = new()
|
||||
{
|
||||
|
@ -6,15 +6,19 @@ namespace API.Schema;
|
||||
[PrimaryKey("LinkId")]
|
||||
public class Link(string linkProvider, string linkUrl)
|
||||
{
|
||||
[MaxLength(64)]
|
||||
public string LinkId { get; init; } = TokenGen.CreateToken(typeof(Link), 64);
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string LinkId { get; init; } = TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl);
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string LinkProvider { get; init; } = linkProvider;
|
||||
[StringLength(2048)]
|
||||
[Required]
|
||||
[Url]
|
||||
public string LinkUrl { get; init; } = linkUrl;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
public override string ToString()
|
||||
{
|
||||
if (obj is not Link other)
|
||||
return false;
|
||||
return other.LinkProvider == LinkProvider && other.LinkUrl == LinkUrl;
|
||||
return $"{LinkId} {LinkProvider} {LinkUrl}";
|
||||
}
|
||||
}
|
22
API/Schema/LocalLibrary.cs
Normal file
22
API/Schema/LocalLibrary.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.Schema;
|
||||
|
||||
public class LocalLibrary(string basePath, string libraryName)
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string LocalLibraryId { get; init; } = TokenGen.CreateToken(typeof(LocalLibrary), basePath);
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string BasePath { get; internal set; } = basePath;
|
||||
|
||||
[StringLength(512)]
|
||||
[Required]
|
||||
public string LibraryName { get; internal set; } = libraryName;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{LocalLibraryId} {LibraryName} - {BasePath}";
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using API.Schema.Jobs;
|
||||
using System.Text;
|
||||
using API.Schema.MangaConnectors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
using static System.IO.UnixFileMode;
|
||||
|
||||
namespace API.Schema;
|
||||
@ -13,118 +13,149 @@ namespace API.Schema;
|
||||
[PrimaryKey("MangaId")]
|
||||
public class Manga
|
||||
{
|
||||
[MaxLength(64)]
|
||||
public string MangaId { get; init; } = TokenGen.CreateToken(typeof(Manga), 64);
|
||||
[MaxLength(64)]
|
||||
public string ConnectorId { get; init; }
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string MangaId { get; init; }
|
||||
[StringLength(256)] [Required] public string IdOnConnectorSite { get; init; }
|
||||
[StringLength(512)] [Required] public string Name { get; internal set; }
|
||||
[Required] public string Description { get; internal set; }
|
||||
[Url] [StringLength(512)] [Required] public string WebsiteUrl { get; internal init; }
|
||||
[JsonIgnore] [Url] [StringLength(512)] public string CoverUrl { get; internal set; }
|
||||
[Required] public MangaReleaseStatus ReleaseStatus { get; internal set; }
|
||||
|
||||
[StringLength(64)]
|
||||
public string? LibraryId { get; init; }
|
||||
[JsonIgnore] public LocalLibrary? Library { get; internal set; }
|
||||
|
||||
[StringLength(32)]
|
||||
[Required]
|
||||
public string MangaConnectorName { get; init; }
|
||||
[JsonIgnore] public MangaConnector MangaConnector { get; init; } = null!;
|
||||
|
||||
public string Name { get; internal set; }
|
||||
public string Description { get; internal set; }
|
||||
public string WebsiteUrl { get; internal set; }
|
||||
public string CoverUrl { get; internal set; }
|
||||
public string? CoverFileNameInCache { get; internal set; }
|
||||
public uint Year { get; internal set; }
|
||||
public string? OriginalLanguage { get; internal set; }
|
||||
public MangaReleaseStatus ReleaseStatus { get; internal set; }
|
||||
public string FolderName { get; private set; }
|
||||
public float IgnoreChapterBefore { get; internal set; }
|
||||
public ICollection<Author> Authors { get; internal set; }= null!;
|
||||
public ICollection<MangaTag> MangaTags { get; internal set; }= null!;
|
||||
public ICollection<Link> Links { get; internal set; }= null!;
|
||||
public ICollection<MangaAltTitle> AltTitles { get; internal set; } = null!;
|
||||
[Required] public float IgnoreChaptersBefore { get; internal set; }
|
||||
[StringLength(1024)] [Required] public string DirectoryName { get; private set; }
|
||||
|
||||
public string MangaConnectorId { get; private set; }
|
||||
public MangaConnector? MangaConnector { get; private set; }
|
||||
|
||||
public ICollection<Author>? Authors { get; internal set; }
|
||||
|
||||
public ICollection<MangaTag>? Tags { get; internal set; }
|
||||
|
||||
public ICollection<Link>? Links { get; internal set; }
|
||||
|
||||
public ICollection<MangaAltTitle>? AltTitles { get; internal set; }
|
||||
[JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; } = null;
|
||||
public uint? Year { get; internal init; }
|
||||
[StringLength(8)] public string? OriginalLanguage { get; internal init; }
|
||||
|
||||
public Manga(string connectorId, string name, string description, string websiteUrl, string coverUrl,
|
||||
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
|
||||
float ignoreChapterBefore, MangaConnector mangaConnector, ICollection<Author> authors,
|
||||
ICollection<MangaTag> tags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles)
|
||||
: this(connectorId, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage,
|
||||
releaseStatus, ignoreChapterBefore, mangaConnector.Name)
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
|
||||
|
||||
[NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.ChapterId).ToList();
|
||||
private readonly ILazyLoader _lazyLoader = null!;
|
||||
private ICollection<Chapter> _chapters = null!;
|
||||
[JsonIgnore]
|
||||
public ICollection<Chapter> Chapters
|
||||
{
|
||||
get => _lazyLoader.Load(this, ref _chapters);
|
||||
init => _chapters = value;
|
||||
}
|
||||
|
||||
public Manga(string idOnConnector, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
|
||||
MangaConnector mangaConnector, ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles,
|
||||
LocalLibrary? library = null, float ignoreChaptersBefore = 0f, uint? year = null, string? originalLanguage = null)
|
||||
{
|
||||
this.MangaId = TokenGen.CreateToken(typeof(Manga), mangaConnector.Name, idOnConnector);
|
||||
this.IdOnConnectorSite = idOnConnector;
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
this.WebsiteUrl = websiteUrl;
|
||||
this.CoverUrl = coverUrl;
|
||||
this.ReleaseStatus = releaseStatus;
|
||||
this.LibraryId = library?.LocalLibraryId;
|
||||
this.Library = library;
|
||||
this.MangaConnectorName = mangaConnector.Name;
|
||||
this.MangaConnector = mangaConnector;
|
||||
this.Authors = authors;
|
||||
this.Tags = tags;
|
||||
this.MangaTags = mangaTags;
|
||||
this.Links = links;
|
||||
this.AltTitles = altTitles;
|
||||
this.IgnoreChaptersBefore = ignoreChaptersBefore;
|
||||
this.DirectoryName = CleanDirectoryName(name);
|
||||
this.Year = year;
|
||||
this.OriginalLanguage = originalLanguage;
|
||||
this.Chapters = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
public Manga(ILazyLoader lazyLoader, string mangaId, string idOnConnectorSite, string name, string description, string websiteUrl, string coverUrl, MangaReleaseStatus releaseStatus,
|
||||
string mangaConnectorName, string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
|
||||
{
|
||||
this._lazyLoader = lazyLoader;
|
||||
this.MangaId = mangaId;
|
||||
this.IdOnConnectorSite = idOnConnectorSite;
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
this.WebsiteUrl = websiteUrl;
|
||||
this.CoverUrl = coverUrl;
|
||||
this.ReleaseStatus = releaseStatus;
|
||||
this.MangaConnectorName = mangaConnectorName;
|
||||
this.DirectoryName = directoryName;
|
||||
this.LibraryId = libraryId;
|
||||
this.IgnoreChaptersBefore = ignoreChaptersBefore;
|
||||
this.Year = year;
|
||||
this.OriginalLanguage = originalLanguage;
|
||||
}
|
||||
|
||||
public Manga(string connectorId, string name, string description, string websiteUrl, string coverUrl,
|
||||
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
|
||||
float ignoreChapterBefore, string mangaConnectorId)
|
||||
{
|
||||
ConnectorId = connectorId;
|
||||
Name = name;
|
||||
Description = description;
|
||||
WebsiteUrl = websiteUrl;
|
||||
CoverUrl = coverUrl;
|
||||
CoverFileNameInCache = coverFileNameInCache;
|
||||
Year = year;
|
||||
OriginalLanguage = originalLanguage;
|
||||
ReleaseStatus = releaseStatus;
|
||||
IgnoreChapterBefore = ignoreChapterBefore;
|
||||
MangaConnectorId = mangaConnectorId;
|
||||
FolderName = BuildFolderName(name);
|
||||
}
|
||||
|
||||
public MoveFileOrFolderJob UpdateFolderName(string downloadLocation, string newName)
|
||||
{
|
||||
string oldName = this.FolderName;
|
||||
this.FolderName = newName;
|
||||
return new MoveFileOrFolderJob(Path.Join(downloadLocation, oldName), Path.Join(downloadLocation, this.FolderName));
|
||||
}
|
||||
|
||||
internal void UpdateWithInfo(Manga other)
|
||||
{
|
||||
this.Name = other.Name;
|
||||
this.Year = other.Year;
|
||||
this.Description = other.Description;
|
||||
this.CoverUrl = other.CoverUrl;
|
||||
this.OriginalLanguage = other.OriginalLanguage;
|
||||
this.Authors = other.Authors;
|
||||
this.Links = other.Links;
|
||||
this.Tags = other.Tags;
|
||||
this.AltTitles = other.AltTitles;
|
||||
this.ReleaseStatus = other.ReleaseStatus;
|
||||
}
|
||||
|
||||
private static string BuildFolderName(string mangaName)
|
||||
{
|
||||
return mangaName;
|
||||
}
|
||||
|
||||
internal string SaveCoverImageToCache()
|
||||
{
|
||||
Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
|
||||
//https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
|
||||
Match match = urlRex.Match(CoverUrl);
|
||||
string filename = $"{match.Groups[1].Value}-{MangaId}.{match.Groups[3].Value}";
|
||||
string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
|
||||
|
||||
if (File.Exists(saveImagePath))
|
||||
return saveImagePath;
|
||||
|
||||
RequestResult coverResult = new HttpDownloadClient().MakeRequest(CoverUrl, RequestType.MangaCover);
|
||||
using MemoryStream ms = new();
|
||||
coverResult.result.CopyTo(ms);
|
||||
Directory.CreateDirectory(TrangaSettings.coverImageCache);
|
||||
File.WriteAllBytes(saveImagePath, ms.ToArray());
|
||||
return saveImagePath;
|
||||
}
|
||||
|
||||
public string CreatePublicationFolder()
|
||||
{
|
||||
string publicationFolder = Path.Join(TrangaSettings.downloadLocation, this.FolderName);
|
||||
string? publicationFolder = FullDirectoryPath;
|
||||
if (publicationFolder is null)
|
||||
throw new DirectoryNotFoundException("Publication folder not found");
|
||||
if(!Directory.Exists(publicationFolder))
|
||||
Directory.CreateDirectory(publicationFolder);
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
File.SetUnixFileMode(publicationFolder, GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute | UserRead | UserWrite | UserExecute);
|
||||
return publicationFolder;
|
||||
}
|
||||
|
||||
//TODO onchanges create job to update metadata files in archives, etc.
|
||||
|
||||
//https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
|
||||
//less than 32 is control *forbidden*
|
||||
//34 is " *forbidden*
|
||||
//42 is * *forbidden*
|
||||
//47 is / *forbidden*
|
||||
//58 is : *forbidden*
|
||||
//60 is < *forbidden*
|
||||
//62 is > *forbidden*
|
||||
//63 is ? *forbidden*
|
||||
//92 is \ *forbidden*
|
||||
//124 is | *forbidden*
|
||||
//127 is delete *forbidden*
|
||||
//Below 127 all except *******
|
||||
private static readonly int[] ForbiddenCharsBelow127 = [34, 42, 47, 58, 60, 62, 63, 92, 124, 127];
|
||||
//Above 127 none except *******
|
||||
private static readonly int[] IncludeCharsAbove127 = [128, 138, 142];
|
||||
//128 is € include
|
||||
//138 is Š include
|
||||
//142 is Ž include
|
||||
//152 through 255 looks fine except 157, 172, 173, 175 *******
|
||||
private static readonly int[] ForbiddenCharsAbove152 = [157, 172, 173, 175];
|
||||
private static string CleanDirectoryName(string name)
|
||||
{
|
||||
StringBuilder sb = new ();
|
||||
foreach (char c in name)
|
||||
{
|
||||
if (c >= 32 && c < 127 && ForbiddenCharsBelow127.Contains(c) == false)
|
||||
sb.Append(c);
|
||||
else if (c > 127 && c < 152 && IncludeCharsAbove127.Contains(c))
|
||||
sb.Append(c);
|
||||
else if(c >= 152 && c <= 255 && ForbiddenCharsAbove152.Contains(c) == false)
|
||||
sb.Append(c);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{MangaId} {Name}";
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema;
|
||||
@ -7,9 +6,18 @@ namespace API.Schema;
|
||||
[PrimaryKey("AltTitleId")]
|
||||
public class MangaAltTitle(string language, string title)
|
||||
{
|
||||
[MaxLength(64)]
|
||||
public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle", 64);
|
||||
[MaxLength(8)]
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle");
|
||||
[StringLength(8)]
|
||||
[Required]
|
||||
public string Language { get; init; } = language;
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string Title { get; set; } = title;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{AltTitleId} {Language} {Title}";
|
||||
}
|
||||
}
|
@ -1,192 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class AsuraToon : MangaConnector
|
||||
{
|
||||
|
||||
public AsuraToon() : base("AsuraToon", ["en"], ["https://asuracomic.net"])
|
||||
{
|
||||
this.downloadClient = new ChromiumDownloadClient();
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
||||
{
|
||||
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
|
||||
string requestUrl = $"https://asuracomic.net/series?name={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;
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
||||
{
|
||||
return GetMangaFromUrl($"https://asuracomic.net/series/{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>?)[] ParsePublicationsFromHtml(HtmlDocument document)
|
||||
{
|
||||
HtmlNodeCollection mangaList = document.DocumentNode.SelectNodes("//a[starts-with(@href,'series')]");
|
||||
if (mangaList is null || mangaList.Count < 1)
|
||||
return [];
|
||||
|
||||
IEnumerable<string> urls = mangaList.Select(a => $"https://asuracomic.net/{a.GetAttributeValue("href", "")}");
|
||||
|
||||
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 { } x)
|
||||
ret.Add(x);
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
||||
{
|
||||
string? originalLanguage = null;
|
||||
Dictionary<string, string> altTitles = new(), links = new();
|
||||
|
||||
HtmlNodeCollection genreNodes = document.DocumentNode.SelectNodes("//h3[text()='Genres']/../div/button");
|
||||
string[] tags = genreNodes.Select(b => b.InnerText).ToArray();
|
||||
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
|
||||
|
||||
HtmlNode statusNode = document.DocumentNode.SelectSingleNode("//h3[text()='Status']/../h3[2]");
|
||||
MangaReleaseStatus releaseStatus = statusNode.InnerText.ToLower() switch
|
||||
{
|
||||
"ongoing" => MangaReleaseStatus.Continuing,
|
||||
"hiatus" => MangaReleaseStatus.OnHiatus,
|
||||
"completed" => MangaReleaseStatus.Completed,
|
||||
"dropped" => MangaReleaseStatus.Cancelled,
|
||||
"season end" => MangaReleaseStatus.Continuing,
|
||||
"coming soon" => MangaReleaseStatus.Unreleased,
|
||||
_ => MangaReleaseStatus.Unreleased
|
||||
};
|
||||
|
||||
HtmlNode coverNode =
|
||||
document.DocumentNode.SelectSingleNode("//img[@alt='poster']");
|
||||
string coverUrl = coverNode.GetAttributeValue("src", "");
|
||||
|
||||
HtmlNode titleNode =
|
||||
document.DocumentNode.SelectSingleNode("//title");
|
||||
string sortName = Regex.Match(titleNode.InnerText, @"(.*) - Asura Scans").Groups[1].Value;
|
||||
|
||||
HtmlNode descriptionNode =
|
||||
document.DocumentNode.SelectSingleNode("//h3[starts-with(text(),'Synopsis')]/../span");
|
||||
string description = descriptionNode?.InnerText??"";
|
||||
|
||||
HtmlNodeCollection authorNodes = document.DocumentNode.SelectNodes("//h3[text()='Author']/../h3[not(text()='Author' or text()='_')]");
|
||||
HtmlNodeCollection artistNodes = document.DocumentNode.SelectNodes("//h3[text()='Artist']/../h3[not(text()='Artist' or text()='_')]");
|
||||
IEnumerable<string> authorNames = authorNodes is null ? [] : authorNodes.Select(a => a.InnerText);
|
||||
IEnumerable<string> artistNames = artistNodes is null ? [] : artistNodes.Select(a => a.InnerText);
|
||||
List<string> authorStrings = authorNames.Concat(artistNames).ToList();
|
||||
List<Author> authors = authorStrings.Select(author => new Author(author)).ToList();
|
||||
|
||||
HtmlNode? firstChapterNode = document.DocumentNode.SelectSingleNode("//a[contains(@href, 'chapter/1')]/../following-sibling::h3");
|
||||
uint year = uint.Parse(firstChapterNode?.InnerText.Split(' ')[^1] ?? "2000");
|
||||
|
||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
||||
originalLanguage, releaseStatus, -1,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
[],
|
||||
[]);
|
||||
|
||||
return (manga, authors, mangaTags, [], []);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||
{
|
||||
string requestUrl = $"https://asuracomic.net/series/{manga.MangaId}";
|
||||
// Leaving this in for verification if the page exists
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||
return [];
|
||||
|
||||
//Return Chapters ordered by Chapter-Number
|
||||
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
|
||||
return chapters.Order().ToArray();
|
||||
}
|
||||
|
||||
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
|
||||
{
|
||||
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
|
||||
{
|
||||
return new List<Chapter>();
|
||||
}
|
||||
|
||||
List<Chapter> ret = new();
|
||||
|
||||
HtmlNodeCollection chapterURLNodes = result.htmlDocument.DocumentNode.SelectNodes("//a[contains(@href, '/chapter/')]");
|
||||
Regex infoRex = new(@"Chapter ([0-9]+)(.*)?");
|
||||
|
||||
foreach (HtmlNode chapterInfo in chapterURLNodes)
|
||||
{
|
||||
string chapterUrl = chapterInfo.GetAttributeValue("href", "");
|
||||
|
||||
Match match = infoRex.Match(chapterInfo.InnerText);
|
||||
if(!ChapterNumber.CanParse(match.Groups[1].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(match.Groups[1].Value);
|
||||
string? chapterName = match.Groups[2].Success && match.Groups[2].Length > 1 ? match.Groups[2].Value : null;
|
||||
string url = $"https://asuracomic.net/series/{chapterUrl}";
|
||||
try
|
||||
{
|
||||
ret.Add(new Chapter(manga, url, chapterNumber, null, chapterName));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
string requestUrl = chapter.Url;
|
||||
// Leaving this in to check if the page exists
|
||||
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 string[] ParseImageUrlsFromHtml(HtmlDocument document)
|
||||
{
|
||||
HtmlNodeCollection images = document.DocumentNode.SelectNodes("//img[contains(@alt, 'chapter page')]");
|
||||
|
||||
return images.Select(i => i.GetAttributeValue("src", "")).ToArray();
|
||||
}
|
||||
}
|
@ -1,205 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class Bato : MangaConnector
|
||||
{
|
||||
|
||||
public Bato() : base("Bato", ["en"], ["bato.to"])
|
||||
{
|
||||
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(m => m.Value.Length > 0)).ToLower();
|
||||
string requestUrl = $"https://bato.to/v3x-search?word={sanitizedTitle}&lang=en";
|
||||
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;
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
||||
{
|
||||
return GetMangaFromUrl($"https://bato.to/title/{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>?)[] ParsePublicationsFromHtml(HtmlDocument document)
|
||||
{
|
||||
HtmlNode mangaList = document.DocumentNode.SelectSingleNode("//div[@data-hk='0-0-2']");
|
||||
if (!mangaList.ChildNodes.Any(node => node.Name == "div"))
|
||||
return [];
|
||||
|
||||
List<string> urls = mangaList.ChildNodes
|
||||
.Select(node => $"https://bato.to{node.Descendants("div").First().FirstChild.GetAttributeValue("href", "")}").ToList();
|
||||
|
||||
HashSet<(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 { } x)
|
||||
ret.Add(x);
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
||||
{
|
||||
HtmlNode infoNode = document.DocumentNode.SelectSingleNode("/html/body/div/main/div[1]/div[2]");
|
||||
|
||||
string sortName = infoNode.Descendants("h3").First().InnerText;
|
||||
string description = document.DocumentNode
|
||||
.SelectSingleNode("//div[contains(concat(' ',normalize-space(@class),' '),'prose')]").InnerText;
|
||||
|
||||
string[] altTitlesList = infoNode.ChildNodes[1].ChildNodes[2].InnerText.Split('/');
|
||||
int i = 0;
|
||||
List<MangaAltTitle> altTitles = altTitlesList.Select(a => new MangaAltTitle(i++.ToString(), a)).ToList();
|
||||
|
||||
string coverUrl = document.DocumentNode.SelectNodes("//img")
|
||||
.First(child => child.GetAttributeValue("data-hk", "") == "0-1-0").GetAttributeValue("src", "").Replace("&", "&");
|
||||
|
||||
List<HtmlNode> genreNodes = document.DocumentNode.SelectSingleNode("//b[text()='Genres:']/..").SelectNodes("span").ToList();
|
||||
string[] tags = genreNodes.Select(node => node.FirstChild.InnerText).ToArray();
|
||||
List<MangaTag> mangaTags = tags.Select(s => new MangaTag(s)).ToList();
|
||||
|
||||
List<HtmlNode> authorsNodes = infoNode.ChildNodes[1].ChildNodes[3].Descendants("a").ToList();
|
||||
List<string> authorNames = authorsNodes.Select(node => node.InnerText.Replace("amp;", "")).ToList();
|
||||
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
|
||||
|
||||
HtmlNode? originalLanguageNode = document.DocumentNode.SelectSingleNode("//span[text()='Tr From']/..");
|
||||
string originalLanguage = originalLanguageNode is not null ? originalLanguageNode.LastChild.InnerText : "";
|
||||
|
||||
if (!uint.TryParse(
|
||||
document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..").LastChild.InnerText.Split('-')[0],
|
||||
out uint year))
|
||||
year = (uint)DateTime.Now.Year;
|
||||
|
||||
string status = document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..")
|
||||
.ChildNodes[2].InnerText;
|
||||
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
||||
switch (status.ToLower())
|
||||
{
|
||||
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
|
||||
case "completed": releaseStatus = MangaReleaseStatus.Completed; break;
|
||||
case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
|
||||
case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
||||
case "pending": releaseStatus = MangaReleaseStatus.Unreleased; break;
|
||||
}
|
||||
|
||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
||||
originalLanguage, releaseStatus, -1,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
[],
|
||||
altTitles);
|
||||
|
||||
return (manga, authors, mangaTags, [], altTitles);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||
{
|
||||
string requestUrl = $"https://bato.to/title/{manga.MangaId}";
|
||||
// Leaving this in for verification if the page exists
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||
return [];
|
||||
|
||||
//Return Chapters ordered by Chapter-Number
|
||||
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
|
||||
return chapters.Order().ToArray();
|
||||
}
|
||||
|
||||
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
|
||||
{
|
||||
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
|
||||
{
|
||||
return new List<Chapter>();
|
||||
}
|
||||
|
||||
List<Chapter> ret = new();
|
||||
|
||||
HtmlNode chapterList =
|
||||
result.htmlDocument.DocumentNode.SelectSingleNode("/html/body/div/main/div[3]/astro-island/div/div[2]/div/div/astro-slot");
|
||||
|
||||
Regex numberRex = new(@"\/title\/.+\/([0-9])+(?:-vol_([0-9]+))?-ch_([0-9\.]+)");
|
||||
|
||||
foreach (HtmlNode chapterInfo in chapterList.SelectNodes("div"))
|
||||
{
|
||||
HtmlNode infoNode = chapterInfo.FirstChild.FirstChild;
|
||||
string chapterUrl = infoNode.GetAttributeValue("href", "");
|
||||
|
||||
Match match = numberRex.Match(chapterUrl);
|
||||
string id = match.Groups[1].Value;
|
||||
int? volumeNumber = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : null;
|
||||
if(ChapterNumber.CanParse(match.Groups[3].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(match.Groups[3].Value);
|
||||
string url = $"https://bato.to{chapterUrl}?load=2";
|
||||
try
|
||||
{
|
||||
ret.Add(new Chapter(manga, url, chapterNumber, volumeNumber, null));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
string requestUrl = chapter.Url;
|
||||
// Leaving this in to check if the page exists
|
||||
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 string[] ParseImageUrlsFromHtml(HtmlDocument document)
|
||||
{
|
||||
HtmlNode images = document.DocumentNode.SelectNodes("//astro-island").First(node =>
|
||||
node.GetAttributeValue("component-url", "").Contains("/_astro/ImageList."));
|
||||
|
||||
string weirdString = images.OuterHtml;
|
||||
string weirdString2 = Regex.Match(weirdString, @"props=\""(.*)}\""").Groups[1].Value;
|
||||
string[] urls = Regex.Matches(weirdString2, @"(https:\/\/[A-z\-0-9\.\?\&\;\=\/]+)\\")
|
||||
.Select(match => match.Groups[1].Value.Replace("&", "&")).ToArray();
|
||||
|
||||
return urls;
|
||||
}
|
||||
}
|
247
API/Schema/MangaConnectors/ComickIo.cs
Normal file
247
API/Schema/MangaConnectors/ComickIo.cs
Normal file
@ -0,0 +1,247 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class ComickIo : MangaConnector
|
||||
{
|
||||
//https://api.comick.io/docs/
|
||||
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
|
||||
|
||||
public ComickIo() : base("ComickIo",
|
||||
["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"],
|
||||
["comick.io"],
|
||||
"https://comick.io/static/icons/unicorn-64.png")
|
||||
{
|
||||
this.downloadClient = new HttpDownloadClient();
|
||||
}
|
||||
|
||||
public override Manga[] SearchManga(string mangaSearchName)
|
||||
{
|
||||
Log.Info($"Searching Manga: {mangaSearchName}");
|
||||
|
||||
List<string> slugs = new();
|
||||
int page = 1;
|
||||
while(page < 50)
|
||||
{
|
||||
string requestUrl = $"https://api.comick.fun/v1.0/search/?type=comic&t=false&limit=100&showall=true&" +
|
||||
$"page={page}&q={mangaSearchName}";
|
||||
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
return [];
|
||||
}
|
||||
|
||||
using StreamReader sr = new (result.result);
|
||||
JArray data = JArray.Parse(sr.ReadToEnd());
|
||||
|
||||
if (data.Count < 1)
|
||||
break;
|
||||
|
||||
slugs.AddRange(data.Select(token => token.Value<string>("slug")!));
|
||||
page++;
|
||||
}
|
||||
Log.Debug($"Search {mangaSearchName} yielded {slugs.Count} slugs. Requesting mangas now...");
|
||||
|
||||
List<Manga> mangas = slugs.Select(GetMangaFromId).ToList()!;
|
||||
|
||||
Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
|
||||
return mangas.ToArray();
|
||||
}
|
||||
|
||||
private readonly Regex _getSlugFromTitleRex = new(@"https?:\/\/comick\.io\/comic\/(.+)(?:\/.*)*");
|
||||
public override Manga? GetMangaFromUrl(string url)
|
||||
{
|
||||
Match m = _getSlugFromTitleRex.Match(url);
|
||||
return m.Groups[1].Success ? GetMangaFromId(m.Groups[1].Value) : null;
|
||||
}
|
||||
|
||||
public override Manga? GetMangaFromId(string mangaIdOnSite)
|
||||
{
|
||||
string requestUrl = $"https://api.comick.fun/comic/{mangaIdOnSite}";
|
||||
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
return null;
|
||||
}
|
||||
using StreamReader sr = new (result.result);
|
||||
JToken data = JToken.Parse(sr.ReadToEnd());
|
||||
|
||||
return ParseMangaFromJToken(data);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string? language = null)
|
||||
{
|
||||
Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
|
||||
List<string> chapterHids = new();
|
||||
int page = 1;
|
||||
while(page < 50)
|
||||
{
|
||||
string requestUrl = $"https://api.comick.fun/comic/{manga.IdOnConnectorSite}/chapters?limit=100&page={page}&lang={language}";
|
||||
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
return [];
|
||||
}
|
||||
|
||||
using StreamReader sr = new (result.result);
|
||||
JToken data = JToken.Parse(sr.ReadToEnd());
|
||||
JArray? chaptersArray = data["chapters"] as JArray;
|
||||
|
||||
if (chaptersArray?.Count < 1)
|
||||
break;
|
||||
|
||||
chapterHids.AddRange(chaptersArray?.Select(token => token.Value<string>("hid")!)!);
|
||||
|
||||
page++;
|
||||
}
|
||||
Log.Debug($"Getting chapters for {manga.Name} yielded {chapterHids.Count} hids. Requesting chapters now...");
|
||||
|
||||
List<Chapter> chapters = chapterHids.Select(hid => ChapterFromHid(manga, hid)).ToList();
|
||||
|
||||
return chapters.ToArray();
|
||||
}
|
||||
|
||||
private readonly Regex _hidFromUrl = new(@"https?:\/\/comick\.io\/comic\/.+\/([^-]+).*");
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
Match m = _hidFromUrl.Match(chapter.Url);
|
||||
if (!m.Groups[1].Success)
|
||||
return [];
|
||||
|
||||
string hid = m.Groups[1].Value;
|
||||
|
||||
string requestUrl = $"https://api.comick.fun/chapter/{hid}/get_images";
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
return [];
|
||||
}
|
||||
|
||||
using StreamReader sr = new (result.result);
|
||||
JArray data = JArray.Parse(sr.ReadToEnd());
|
||||
|
||||
return data.Select(token =>
|
||||
{
|
||||
string url = $"https://meo.comick.pictures/{token.Value<string>("b2key")}";
|
||||
return url;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private Manga ParseMangaFromJToken(JToken json)
|
||||
{
|
||||
string? hid = json["comic"]?.Value<string>("hid");
|
||||
string? slug = json["comic"]?.Value<string>("slug");
|
||||
string? name = json["comic"]?.Value<string>("title");
|
||||
string? description = json["comic"]?.Value<string>("desc");
|
||||
string? originalLanguage = json["comic"]?.Value<string>("country");
|
||||
string url = $"https://comick.io/comic/{slug}";
|
||||
string? coverName = json["comic"]?["md_covers"]?.First?.Value<string>("b2key");
|
||||
string coverUrl = $"https://meo.comick.pictures/{coverName}";
|
||||
int? releaseStatusStr = json["comic"]?.Value<int>("status");
|
||||
MangaReleaseStatus status = releaseStatusStr switch
|
||||
{
|
||||
1 => MangaReleaseStatus.Continuing,
|
||||
2 => MangaReleaseStatus.Completed,
|
||||
3 => MangaReleaseStatus.Cancelled,
|
||||
4 => MangaReleaseStatus.OnHiatus,
|
||||
_ => MangaReleaseStatus.Unreleased
|
||||
};
|
||||
uint? year = json["comic"]?.Value<uint?>("year");
|
||||
JArray? altTitlesArray = json["comic"]?["md_titles"] as JArray;
|
||||
//Cant let language be null, so fill with whatever.
|
||||
byte whatever = 0;
|
||||
List<MangaAltTitle> altTitles = altTitlesArray?
|
||||
.Select(token => new MangaAltTitle(token.Value<string>("lang")??whatever++.ToString(), token.Value<string>("title")!))
|
||||
.ToList()!;
|
||||
|
||||
JArray? authorsArray = json["authors"] as JArray;
|
||||
JArray? artistsArray = json["artists"] as JArray;
|
||||
List<Author> authors = authorsArray?.Concat(artistsArray!)
|
||||
.Select(token => new Author(token.Value<string>("name")!))
|
||||
.DistinctBy(a => a.AuthorId)
|
||||
.ToList()!;
|
||||
|
||||
JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray;
|
||||
List<MangaTag> tags = genreArray?
|
||||
.Select(token => new MangaTag(token["md_genres"]?.Value<string>("name")!))
|
||||
.ToList()!;
|
||||
|
||||
JArray? linksArray = json["comic"]?["links"] as JArray;
|
||||
List<Link> links = linksArray?
|
||||
.ToObject<Dictionary<string,string>>()?
|
||||
.Select(kv =>
|
||||
{
|
||||
string fullUrl = kv.Key switch
|
||||
{
|
||||
"al" => $"https://anilist.co/manga/{kv.Value}",
|
||||
"ap" => $"https://www.anime-planet.com/manga/{kv.Value}",
|
||||
"bw" => $"https://bookwalker.jp/{kv.Value}",
|
||||
"mu" => $"https://www.mangaupdates.com/series.html?id={kv.Value}",
|
||||
"nu" => $"https://www.novelupdates.com/series/{kv.Value}",
|
||||
"mal" => $"https://myanimelist.net/manga/{kv.Value}",
|
||||
_ => kv.Value
|
||||
};
|
||||
string key = kv.Key switch
|
||||
{
|
||||
"al" => "AniList",
|
||||
"ap" => "Anime Planet",
|
||||
"bw" => "BookWalker",
|
||||
"mu" => "Manga Updates",
|
||||
"nu" => "Novel Updates",
|
||||
"kt" => "Kitsu.io",
|
||||
"amz" => "Amazon",
|
||||
"ebj" => "eBookJapan",
|
||||
"mal" => "MyAnimeList",
|
||||
"cdj" => "CDJapan",
|
||||
_ => kv.Key
|
||||
};
|
||||
return new Link(key, fullUrl);
|
||||
}).ToList()!;
|
||||
|
||||
if(hid is null)
|
||||
throw new Exception("hid is null");
|
||||
if(slug is null)
|
||||
throw new Exception("slug is null");
|
||||
if(name is null)
|
||||
throw new Exception("name is null");
|
||||
|
||||
return new Manga(hid, name, description??"", url, coverUrl, status, this,
|
||||
authors, tags, links, altTitles,
|
||||
year: year, originalLanguage: originalLanguage);
|
||||
}
|
||||
|
||||
private Chapter ChapterFromHid(Manga parentManga, string hid)
|
||||
{
|
||||
string requestUrl = $"https://api.comick.fun/chapter/{hid}";
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
throw new Exception("Request failed");
|
||||
}
|
||||
|
||||
using StreamReader sr = new (result.result);
|
||||
JToken data = JToken.Parse(sr.ReadToEnd());
|
||||
|
||||
string? canonical = data.Value<string>("canonical");
|
||||
string? chapterNum = data["chapter"]?.Value<string>("chap");
|
||||
string? volumeNumStr = data["chapter"]?.Value<string>("vol");
|
||||
int? volumeNum = volumeNumStr is null ? null : int.Parse(volumeNumStr);
|
||||
string? title = data["chapter"]?.Value<string>("title");
|
||||
|
||||
if(chapterNum is null)
|
||||
throw new Exception("chapterNum is null");
|
||||
|
||||
string url = $"https://comick.io{canonical}";
|
||||
return new Chapter(parentManga, url, chapterNum, volumeNum, hid, title);
|
||||
}
|
||||
}
|
55
API/Schema/MangaConnectors/Global.cs
Normal file
55
API/Schema/MangaConnectors/Global.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using API.Schema.Contexts;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class Global : MangaConnector
|
||||
{
|
||||
private PgsqlContext context { get; init; }
|
||||
public Global(PgsqlContext context) : base("Global", ["all"], [""], "")
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public override Manga[] SearchManga(string mangaSearchName)
|
||||
{
|
||||
//Get all enabled Connectors
|
||||
MangaConnector[] enabledConnectors = context.MangaConnectors.Where(c => c.Enabled && c.Name != "Global").ToArray();
|
||||
|
||||
//Create Task for each MangaConnector to search simulatneously
|
||||
Task<Manga[]>[] tasks =
|
||||
enabledConnectors.Select(c => new Task<Manga[]>(() => c.SearchManga(mangaSearchName))).ToArray();
|
||||
foreach (var task in tasks)
|
||||
task.Start();
|
||||
|
||||
//Wait for all tasks to finish
|
||||
do
|
||||
{
|
||||
Thread.Sleep(50);
|
||||
}while(tasks.Any(t => t.Status < TaskStatus.RanToCompletion));
|
||||
|
||||
//Concatenate all results into one
|
||||
Manga[] ret = tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray();
|
||||
return ret;
|
||||
}
|
||||
|
||||
public override Manga? GetMangaFromUrl(string url)
|
||||
{
|
||||
MangaConnector? mc = context.MangaConnectors.ToArray().FirstOrDefault(c => c.UrlMatchesConnector(url));
|
||||
return mc?.GetMangaFromUrl(url) ?? null;
|
||||
}
|
||||
|
||||
public override Manga? GetMangaFromId(string mangaIdOnSite)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string? language = null)
|
||||
{
|
||||
return manga.MangaConnector.GetChapters(manga, language);
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
return chapter.ParentManga.MangaConnector.GetChapterImageUrls(chapter);
|
||||
}
|
||||
}
|
@ -1,39 +1,74 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
[PrimaryKey("Name")]
|
||||
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris)
|
||||
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)
|
||||
{
|
||||
[MaxLength(32)]
|
||||
public string Name { get; init; } = name;
|
||||
public string[] SupportedLanguages { get; init; } = supportedLanguages;
|
||||
public string[] BaseUris { get; init; } = baseUris;
|
||||
|
||||
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "");
|
||||
|
||||
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url);
|
||||
|
||||
public abstract (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId);
|
||||
|
||||
public abstract Chapter[] GetChapters(Manga manga, string language="en");
|
||||
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
internal DownloadClient downloadClient { get; init; } = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
protected ILog Log { get; init; } = LogManager.GetLogger(name);
|
||||
|
||||
public Chapter[] GetNewChapters(Manga manga)
|
||||
{
|
||||
Chapter[] allChapters = GetChapters(manga);
|
||||
if (allChapters.Length < 1)
|
||||
return [];
|
||||
|
||||
return allChapters.Where(chapter => !chapter.IsDownloaded()).ToArray();
|
||||
}
|
||||
[StringLength(32)]
|
||||
[Required]
|
||||
public string Name { get; init; } = name;
|
||||
[StringLength(8)]
|
||||
[Required]
|
||||
public string[] SupportedLanguages { get; init; } = supportedLanguages;
|
||||
[StringLength(2048)]
|
||||
[Required]
|
||||
public string IconUrl { get; init; } = iconUrl;
|
||||
[StringLength(256)]
|
||||
[Required]
|
||||
public string[] BaseUris { get; init; } = baseUris;
|
||||
[Required]
|
||||
public bool Enabled { get; internal set; } = true;
|
||||
|
||||
public abstract Manga[] SearchManga(string mangaSearchName);
|
||||
|
||||
public abstract Manga? GetMangaFromUrl(string url);
|
||||
|
||||
public abstract Manga? GetMangaFromId(string mangaIdOnSite);
|
||||
|
||||
public abstract Chapter[] GetChapters(Manga manga, string? language = null);
|
||||
|
||||
internal abstract string[] GetChapterImageUrls(Chapter chapter);
|
||||
|
||||
public bool UrlMatchesConnector(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
|
||||
|
||||
internal string? SaveCoverImageToCache(Manga manga, int retries = 3)
|
||||
{
|
||||
if(retries < 0)
|
||||
return null;
|
||||
|
||||
Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
|
||||
//https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
|
||||
Match match = urlRex.Match(manga.CoverUrl);
|
||||
string filename = $"{match.Groups[1].Value}-{manga.MangaId}.{match.Groups[3].Value}";
|
||||
string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
|
||||
|
||||
if (File.Exists(saveImagePath))
|
||||
return saveImagePath;
|
||||
|
||||
RequestResult coverResult = downloadClient.MakeRequest(manga.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
|
||||
if ((int)coverResult.statusCode < 200 || (int)coverResult.statusCode >= 300)
|
||||
return SaveCoverImageToCache(manga, --retries);
|
||||
|
||||
using MemoryStream ms = new();
|
||||
coverResult.result.CopyTo(ms);
|
||||
Directory.CreateDirectory(TrangaSettings.coverImageCache);
|
||||
File.WriteAllBytes(saveImagePath, ms.ToArray());
|
||||
|
||||
return saveImagePath;
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
using System.Net;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
@ -11,271 +9,327 @@ public class MangaDex : MangaConnector
|
||||
//https://api.mangadex.org/docs/3-enumerations/#language-codes--localization
|
||||
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
|
||||
//https://gist.github.com/Josantonius/b455e315bc7f790d14b136d61d9ae469
|
||||
public MangaDex() : base("MangaDex", ["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"], ["mangadex.org"])
|
||||
public MangaDex() : base("MangaDex",
|
||||
["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"],
|
||||
["mangadex.org"],
|
||||
"https://mangadex.org/favicon.ico")
|
||||
{
|
||||
this.downloadClient = new HttpDownloadClient();
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
||||
private const int Limit = 100;
|
||||
public override Manga[] SearchManga(string mangaSearchName)
|
||||
{
|
||||
const int limit = 100; //How many values we want returned at once
|
||||
int offset = 0; //"Page"
|
||||
int total = int.MaxValue; //How many total results are there, is updated on first request
|
||||
HashSet<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> retManga = new();
|
||||
int loadedPublicationData = 0;
|
||||
List<JsonNode> results = new();
|
||||
Log.Info($"Searching Manga: {mangaSearchName}");
|
||||
List<Manga> mangas = new ();
|
||||
|
||||
//Request all search-results
|
||||
while (offset < total) //As long as we haven't requested all "Pages"
|
||||
int offset = 0;
|
||||
int total = int.MaxValue;
|
||||
while(offset < total)
|
||||
{
|
||||
//Request next Page
|
||||
RequestResult requestResult = downloadClient.MakeRequest(
|
||||
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}" +
|
||||
$"&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica" +
|
||||
$"&contentRating%5B%5D=pornographic" +
|
||||
$"&includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author" +
|
||||
$"&includes%5B%5D=artist&includes%5B%5D=tag", RequestType.MangaInfo);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||
break;
|
||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||
|
||||
offset += limit;
|
||||
if (result is null)
|
||||
break;
|
||||
|
||||
if(result.ContainsKey("total"))
|
||||
total = result["total"]!.GetValue<int>(); //Update the total number of Publications
|
||||
else continue;
|
||||
string requestUrl =
|
||||
$"https://api.mangadex.org/manga?limit={Limit}&offset={offset}&title={mangaSearchName}" +
|
||||
$"&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica" +
|
||||
$"&includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
|
||||
offset += Limit;
|
||||
|
||||
if (result.ContainsKey("data"))
|
||||
results.AddRange(result["data"]!.AsArray()!);//Manga-data-Array
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
return [];
|
||||
}
|
||||
|
||||
using StreamReader sr = new (result.result);
|
||||
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
||||
|
||||
if (jObject.Value<string>("result") != "ok")
|
||||
{
|
||||
JArray? errors = jObject["errors"] as JArray;
|
||||
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
|
||||
return [];
|
||||
}
|
||||
|
||||
total = jObject.Value<int>("total");
|
||||
|
||||
JArray? data = jObject.Value<JArray>("data");
|
||||
if (data is null)
|
||||
{
|
||||
Log.Error("Data was null");
|
||||
return [];
|
||||
}
|
||||
|
||||
mangas.AddRange(data.Select(ParseMangaFromJToken));
|
||||
}
|
||||
|
||||
foreach (JsonNode mangaNode in results)
|
||||
{
|
||||
if(MangaFromJsonObject(mangaNode.AsObject()) is { } manga)
|
||||
retManga.Add(manga); //Add Publication (Manga) to result
|
||||
}
|
||||
return retManga.ToArray();
|
||||
Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
|
||||
return mangas.ToArray();
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
||||
private static readonly Regex GetMangaIdFromUrl = new(@"https?:\/\/mangadex\.org\/title\/([a-z0-9-]+)\/?.*");
|
||||
public override Manga? GetMangaFromUrl(string url)
|
||||
{
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest($"https://api.mangadex.org/manga/{publicationId}?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag", RequestType.MangaInfo);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||
Log.Info($"Getting Manga: {url}");
|
||||
if (!UrlMatchesConnector(url))
|
||||
{
|
||||
Log.Debug($"Url is not for Connector. {url}");
|
||||
return null;
|
||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||
if(result is not null)
|
||||
return MangaFromJsonObject(result["data"]!.AsObject());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Match match = GetMangaIdFromUrl.Match(url);
|
||||
if (!match.Success || !match.Groups[1].Success)
|
||||
{
|
||||
Log.Debug($"Url is not for Connector (Could not retrieve id). {url}");
|
||||
return null;
|
||||
}
|
||||
string id = match.Groups[1].Value;
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
|
||||
{
|
||||
Regex idRex = new (@"https:\/\/mangadex.org\/title\/([A-z0-9-]*)\/.*");
|
||||
string id = idRex.Match(url).Groups[1].Value;
|
||||
return GetMangaFromId(id);
|
||||
}
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? MangaFromJsonObject(JsonObject manga)
|
||||
public override Manga? GetMangaFromId(string mangaIdOnSite)
|
||||
{
|
||||
if (!manga.TryGetPropertyValue("id", out JsonNode? idNode))
|
||||
return null;
|
||||
string publicationId = idNode!.GetValue<string>();
|
||||
|
||||
if (!manga.TryGetPropertyValue("attributes", out JsonNode? attributesNode))
|
||||
return null;
|
||||
JsonObject attributes = attributesNode!.AsObject();
|
||||
|
||||
if (!attributes.TryGetPropertyValue("title", out JsonNode? titleNode))
|
||||
return null;
|
||||
string sortName = titleNode!.AsObject().ContainsKey("en") switch
|
||||
{
|
||||
true => titleNode.AsObject()["en"]!.GetValue<string>(),
|
||||
false => titleNode.AsObject().First().Value!.GetValue<string>()
|
||||
};
|
||||
Log.Info($"Getting Manga: {mangaIdOnSite}");
|
||||
string requestUrl =
|
||||
$"https://api.mangadex.org/manga/{mangaIdOnSite}" +
|
||||
$"?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
|
||||
|
||||
Dictionary<string, string> altTitlesDict = new();
|
||||
if (attributes.TryGetPropertyValue("altTitles", out JsonNode? altTitlesNode))
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
foreach (JsonNode? altTitleNode in altTitlesNode!.AsArray())
|
||||
{
|
||||
JsonObject altTitleNodeObject = altTitleNode!.AsObject();
|
||||
altTitlesDict.TryAdd(altTitleNodeObject.First().Key, altTitleNodeObject.First().Value!.GetValue<string>());
|
||||
}
|
||||
}
|
||||
List<MangaAltTitle> altTitles = altTitlesDict.Select(t => new MangaAltTitle(t.Key, t.Value)).ToList();
|
||||
|
||||
if (!attributes.TryGetPropertyValue("description", out JsonNode? descriptionNode))
|
||||
Log.Error("Request failed");
|
||||
return null;
|
||||
string description = descriptionNode!.AsObject().ContainsKey("en") switch
|
||||
{
|
||||
true => descriptionNode.AsObject()["en"]!.GetValue<string>(),
|
||||
false => descriptionNode.AsObject().FirstOrDefault().Value?.GetValue<string>() ?? ""
|
||||
};
|
||||
|
||||
Dictionary<string, string> linksDict = new();
|
||||
if (attributes.TryGetPropertyValue("links", out JsonNode? linksNode) && linksNode is not null)
|
||||
foreach (KeyValuePair<string, JsonNode?> linkKv in linksNode!.AsObject())
|
||||
linksDict.TryAdd(linkKv.Key, linkKv.Value.GetValue<string>());
|
||||
List<Link> links = linksDict.Select(x => new Link(x.Key, x.Value)).ToList();
|
||||
|
||||
string? originalLanguage =
|
||||
attributes.TryGetPropertyValue("originalLanguage", out JsonNode? originalLanguageNode) switch
|
||||
{
|
||||
true => originalLanguageNode?.GetValue<string>(),
|
||||
false => null
|
||||
};
|
||||
|
||||
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
||||
if (attributes.TryGetPropertyValue("status", out JsonNode? statusNode))
|
||||
{
|
||||
releaseStatus = statusNode?.GetValue<string>().ToLower() switch
|
||||
{
|
||||
"ongoing" => MangaReleaseStatus.Continuing,
|
||||
"completed" => MangaReleaseStatus.Completed,
|
||||
"hiatus" => MangaReleaseStatus.OnHiatus,
|
||||
"cancelled" => MangaReleaseStatus.Cancelled,
|
||||
_ => MangaReleaseStatus.Unreleased
|
||||
};
|
||||
}
|
||||
|
||||
uint year = attributes.TryGetPropertyValue("year", out JsonNode? yearNode) switch
|
||||
{
|
||||
true => yearNode?.GetValue<uint>()??0,
|
||||
false => 0
|
||||
};
|
||||
|
||||
HashSet<string> tags = new(128);
|
||||
if (attributes.TryGetPropertyValue("tags", out JsonNode? tagsNode))
|
||||
foreach (JsonNode? tagNode in tagsNode!.AsArray())
|
||||
tags.Add(tagNode!["attributes"]!["name"]!["en"]!.GetValue<string>());
|
||||
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
|
||||
|
||||
if (!manga.TryGetPropertyValue("relationships", out JsonNode? relationshipsNode))
|
||||
return null;
|
||||
|
||||
JsonNode? coverNode = relationshipsNode!.AsArray()
|
||||
.FirstOrDefault(rel => rel!["type"]!.GetValue<string>().Equals("cover_art"));
|
||||
if (coverNode is null)
|
||||
return null;
|
||||
string fileName = coverNode["attributes"]!["fileName"]!.GetValue<string>();
|
||||
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
|
||||
|
||||
List<string> authorNames = new();
|
||||
JsonNode?[] authorNodes = relationshipsNode.AsArray()
|
||||
.Where(rel => rel!["type"]!.GetValue<string>().Equals("author") || rel!["type"]!.GetValue<string>().Equals("artist")).ToArray();
|
||||
foreach (JsonNode? authorNode in authorNodes)
|
||||
{
|
||||
string authorName = authorNode!["attributes"]!["name"]!.GetValue<string>();
|
||||
if(!authorNames.Contains(authorName))
|
||||
authorNames.Add(authorName);
|
||||
}
|
||||
List<Author> authors = authorNames.Select(a => new Author(a)).ToList();
|
||||
using StreamReader sr = new (result.result);
|
||||
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
||||
|
||||
Manga pub = new (publicationId, sortName, description, $"https://mangadex.org/title/{publicationId}", coverUrl, null, year,
|
||||
originalLanguage, releaseStatus, -1,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
links,
|
||||
altTitles);
|
||||
|
||||
return (pub, authors, mangaTags, links, altTitles);
|
||||
if (jObject.Value<string>("result") != "ok")
|
||||
{
|
||||
JArray? errors = jObject["errors"] as JArray;
|
||||
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
|
||||
return null;
|
||||
}
|
||||
|
||||
JObject? data = jObject["data"] as JObject;
|
||||
if (data is null)
|
||||
{
|
||||
Log.Error("Data was null");
|
||||
return null;
|
||||
}
|
||||
|
||||
Manga manga = ParseMangaFromJToken(data);
|
||||
return manga;
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||
public override Chapter[] GetChapters(Manga manga, string? language = null)
|
||||
{
|
||||
const int limit = 100; //How many values we want returned at once
|
||||
int offset = 0; //"Page"
|
||||
int total = int.MaxValue; //How many total results are there, is updated on first request
|
||||
List<Chapter> chapters = new();
|
||||
//As long as we haven't requested all "Pages"
|
||||
while (offset < total)
|
||||
Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
|
||||
List<Chapter> chapters = new ();
|
||||
|
||||
int offset = 0;
|
||||
int total = int.MaxValue;
|
||||
while(offset < total)
|
||||
{
|
||||
//Request next "Page"
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(
|
||||
$"https://api.mangadex.org/manga/{manga.ConnectorId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&contentRating%5B%5D=pornographic", RequestType.MangaDexFeed);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||
break;
|
||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||
|
||||
offset += limit;
|
||||
if (result is null)
|
||||
break;
|
||||
|
||||
total = result["total"]!.GetValue<int>();
|
||||
JsonArray chaptersInResult = result["data"]!.AsArray();
|
||||
//Loop through all Chapters in result and extract information from JSON
|
||||
foreach (JsonNode? jsonNode in chaptersInResult)
|
||||
{
|
||||
JsonObject chapter = (JsonObject)jsonNode!;
|
||||
JsonObject attributes = chapter["attributes"]!.AsObject();
|
||||
|
||||
string chapterId = chapter["id"]!.GetValue<string>();
|
||||
string url = $"https://mangadex.org/chapter/{chapterId}";
|
||||
|
||||
string? title = attributes.ContainsKey("title") && attributes["title"] is not null
|
||||
? attributes["title"]!.GetValue<string>()
|
||||
: null;
|
||||
|
||||
int? volume = attributes.ContainsKey("volume") && attributes["volume"] is not null
|
||||
? int.Parse(attributes["volume"]!.GetValue<string>())
|
||||
: null;
|
||||
|
||||
string? chapterNumStr = attributes.ContainsKey("chapter") && attributes["chapter"] is not null
|
||||
? attributes["chapter"]!.GetValue<string>()
|
||||
: null;
|
||||
|
||||
if(chapterNumStr is null || ChapterNumber.CanParse(chapterNumStr))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(chapterNumStr);
|
||||
|
||||
|
||||
if (attributes.ContainsKey("pages") && attributes["pages"] is not null &&
|
||||
attributes["pages"]!.GetValue<int>() < 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Chapter newChapter = new Chapter(manga, url, chapterNumber, volume, title);
|
||||
if(!chapters.Contains(newChapter))
|
||||
chapters.Add(newChapter);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
string requestUrl =
|
||||
$"https://api.mangadex.org/manga/{manga.IdOnConnectorSite}/feed?limit={Limit}&offset={offset}&" +
|
||||
$"translatedLanguage%5B%5D={language}&" +
|
||||
$"contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&includeFutureUpdates=0&includes%5B%5D=";
|
||||
offset += Limit;
|
||||
|
||||
//Return Chapters ordered by Chapter-Number
|
||||
return chapters.Order().ToArray();
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
return [];
|
||||
}
|
||||
|
||||
using StreamReader sr = new (result.result);
|
||||
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
||||
|
||||
if (jObject.Value<string>("result") != "ok")
|
||||
{
|
||||
JArray? errors = jObject["errors"] as JArray;
|
||||
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
|
||||
return [];
|
||||
}
|
||||
|
||||
total = jObject.Value<int>("total");
|
||||
|
||||
JArray? data = jObject.Value<JArray>("data");
|
||||
if (data is null)
|
||||
{
|
||||
Log.Error("Data was null");
|
||||
return [];
|
||||
}
|
||||
|
||||
chapters.AddRange(data.Select(d => ParseChapterFromJToken(manga, d)));
|
||||
}
|
||||
|
||||
Log.Info($"Request for chapters for {manga.Name} yielded {chapters.Count} results.");
|
||||
return chapters.ToArray();
|
||||
}
|
||||
|
||||
private static readonly Regex GetChapterIdFromUrl = new(@"https?:\/\/mangadex\.org\/chapter\/([a-z0-9-]+)\/?.*");
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{//Request URLs for Chapter-Images
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.ChapterId}?forcePort443=false", RequestType.MangaDexImage);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||
{
|
||||
Log.Info($"Getting Chapter Image-Urls: {chapter.Url}");
|
||||
if (!UrlMatchesConnector(chapter.Url))
|
||||
{
|
||||
Log.Debug($"Url is not for Connector. {chapter.Url}");
|
||||
return [];
|
||||
}
|
||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
||||
if (result is null)
|
||||
|
||||
Match match = GetChapterIdFromUrl.Match(chapter.Url);
|
||||
if (!match.Success || !match.Groups[1].Success)
|
||||
{
|
||||
Log.Debug($"Url is not for Connector (Could not retrieve id). {chapter.Url}");
|
||||
return [];
|
||||
}
|
||||
string baseUrl = result["baseUrl"]!.GetValue<string>();
|
||||
string hash = result["chapter"]!["hash"]!.GetValue<string>();
|
||||
JsonArray imageFileNames = result["chapter"]!["data"]!.AsArray();
|
||||
//Loop through all imageNames and construct urls (imageUrl)
|
||||
List<string> imageUrls = new();
|
||||
foreach (JsonNode? image in imageFileNames)
|
||||
imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}");
|
||||
return imageUrls.ToArray();
|
||||
|
||||
string id = match.Groups[1].Value;
|
||||
string requestUrl = $"https://api.mangadex.org/at-home/server/{id}";
|
||||
|
||||
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||
{
|
||||
Log.Error("Request failed");
|
||||
return [];
|
||||
}
|
||||
|
||||
using StreamReader sr = new (result.result);
|
||||
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
||||
|
||||
if (jObject.Value<string>("result") != "ok")
|
||||
{
|
||||
JArray? errors = jObject["errors"] as JArray;
|
||||
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
|
||||
return [];
|
||||
}
|
||||
|
||||
string? baseUrl = jObject.Value<string>("baseUrl");
|
||||
JToken? chapterToken = jObject["chapter"];
|
||||
string? hash = chapterToken?.Value<string>("hash");
|
||||
JArray? data = chapterToken?["data"] as JArray;
|
||||
|
||||
if (baseUrl is null || hash is null || data is null)
|
||||
{
|
||||
Log.Error("Data was null");
|
||||
return [];
|
||||
}
|
||||
|
||||
IEnumerable<string> urls = data.Select(t => $"{baseUrl}/data/{hash}/{t.Value<string>()}");
|
||||
|
||||
return urls.ToArray();
|
||||
}
|
||||
|
||||
private Manga ParseMangaFromJToken(JToken jToken)
|
||||
{
|
||||
string? id = jToken.Value<string>("id");
|
||||
|
||||
JObject? attributes = jToken["attributes"] as JObject;
|
||||
string? name = attributes?["title"]?.Value<string>("en") ?? attributes?["title"]?.First?.First?.Value<string>();
|
||||
string? description = attributes?["description"]?.Value<string>("en")??attributes?["description"]?.First?.First?.Value<string>();
|
||||
string? status = attributes?["status"]?.Value<string>();
|
||||
uint? year = attributes?["year"]?.Value<uint?>();
|
||||
string? originalLanguage = attributes?["originalLanguage"]?.Value<string>();
|
||||
JArray? altTitlesJArray = attributes?["altTitles"] as JArray;
|
||||
JArray? tagsJArray = attributes?["tags"] as JArray;
|
||||
|
||||
JArray? relationships = jToken["relationships"] as JArray;
|
||||
string? coverFileName =
|
||||
relationships?.FirstOrDefault(r => r["type"]?.Value<string>() == "cover_art")?["attributes"]?.Value<string>("fileName");
|
||||
|
||||
if (id is null || attributes is null || name is null || description is null || status is null ||
|
||||
altTitlesJArray is null || tagsJArray is null || relationships is null || coverFileName is null)
|
||||
throw new Exception("jToken was not in expected format");
|
||||
|
||||
List<Link> links = attributes["links"]?
|
||||
.ToObject<Dictionary<string,string>>()?
|
||||
.Select(kv =>
|
||||
{
|
||||
//https://api.mangadex.org/docs/3-enumerations/#manga-links-data
|
||||
string url = kv.Key switch
|
||||
{
|
||||
"al" => $"https://anilist.co/manga/{kv.Value}",
|
||||
"ap" => $"https://www.anime-planet.com/manga/{kv.Value}",
|
||||
"bw" => $"https://bookwalker.jp/{kv.Value}",
|
||||
"mu" => $"https://www.mangaupdates.com/series.html?id={kv.Value}",
|
||||
"nu" => $"https://www.novelupdates.com/series/{kv.Value}",
|
||||
"mal" => $"https://myanimelist.net/manga/{kv.Value}",
|
||||
_ => kv.Value
|
||||
};
|
||||
string key = kv.Key switch
|
||||
{
|
||||
"al" => "AniList",
|
||||
"ap" => "Anime Planet",
|
||||
"bw" => "BookWalker",
|
||||
"mu" => "Manga Updates",
|
||||
"nu" => "Novel Updates",
|
||||
"kt" => "Kitsu.io",
|
||||
"amz" => "Amazon",
|
||||
"ebj" => "eBookJapan",
|
||||
"mal" => "MyAnimeList",
|
||||
"cdj" => "CDJapan",
|
||||
_ => kv.Key
|
||||
};
|
||||
return new Link(key, url);
|
||||
}).ToList()!;
|
||||
|
||||
List<MangaAltTitle> altTitles = altTitlesJArray
|
||||
.Select(t =>
|
||||
{
|
||||
JObject? j = t as JObject;
|
||||
JProperty? p = j?.Properties().First();
|
||||
if (p is null)
|
||||
return null;
|
||||
return new MangaAltTitle(p.Name, p.Value.ToString());
|
||||
}).Where(x => x is not null).ToList()!;
|
||||
|
||||
List<MangaTag> tags = tagsJArray
|
||||
.Where(t => t.Value<string>("type") == "tag")
|
||||
.Select(t => t["attributes"]?["name"]?.Value<string>("en")??t["attributes"]?["name"]?.First?.First?.Value<string>())
|
||||
.Select(str => str is not null ? new MangaTag(str) : null)
|
||||
.Where(x => x is not null).ToList()!;
|
||||
|
||||
List<Author> authors = relationships
|
||||
.Where(r => r["type"]?.Value<string>() == "author")
|
||||
.Select(t => t["attributes"]?.Value<string>("name"))
|
||||
.Select(str => str is not null ? new Author(str) : null)
|
||||
.Where(x => x is not null).ToList()!;
|
||||
|
||||
|
||||
MangaReleaseStatus releaseStatus = status switch
|
||||
{
|
||||
"completed" => MangaReleaseStatus.Completed,
|
||||
"ongoing" => MangaReleaseStatus.Continuing,
|
||||
"cancelled" => MangaReleaseStatus.Cancelled,
|
||||
"hiatus" => MangaReleaseStatus.OnHiatus,
|
||||
_ => MangaReleaseStatus.Unreleased
|
||||
};
|
||||
string websiteUrl = $"https://mangadex.org/title/{id}";
|
||||
string coverUrl = $"https://uploads.mangadex.org/covers/{id}/{coverFileName}";
|
||||
|
||||
return new Manga(id, name, description, websiteUrl, coverUrl, releaseStatus, this,
|
||||
authors, tags, links,altTitles,
|
||||
null, 0f, year, originalLanguage);
|
||||
}
|
||||
|
||||
private Chapter ParseChapterFromJToken(Manga parentManga, JToken jToken)
|
||||
{
|
||||
string? id = jToken.Value<string>("id");
|
||||
JToken? attributes = jToken["attributes"];
|
||||
string? chapter = attributes?.Value<string>("chapter");
|
||||
string? volumeStr = attributes?.Value<string>("volume");
|
||||
int? volume = null;
|
||||
string? title = attributes?.Value<string>("title");
|
||||
|
||||
if(id is null || chapter is null)
|
||||
throw new Exception("jToken was not in expected format");
|
||||
if(volumeStr is not null)
|
||||
volume = int.Parse(volumeStr);
|
||||
|
||||
string url = $"https://mangadex.org/chapter/{id}";
|
||||
return new Chapter(parentManga, url, chapter, volume, id, title);
|
||||
}
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class MangaHere : MangaConnector
|
||||
{
|
||||
public MangaHere() : base("MangaHere", ["en"], ["www.mangahere.cc"])
|
||||
{
|
||||
this.downloadClient = new ChromiumDownloadClient();
|
||||
}
|
||||
|
||||
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://www.mangahere.cc/search?title={sanitizedTitle}";
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || 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)
|
||||
{
|
||||
if (document.DocumentNode.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),' container ')]").Any(node => node.ChildNodes.Any(cNode => cNode.HasClass("search-keywords"))))
|
||||
return [];
|
||||
|
||||
List<string> urls = document.DocumentNode
|
||||
.SelectNodes("//a[contains(@href, '/manga/') and not(contains(@href, '.html'))]")
|
||||
.Select(thumb => $"https://www.mangahere.cc{thumb.GetAttributeValue("href", "")}").Distinct().ToList();
|
||||
|
||||
HashSet<(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 { } x)
|
||||
ret.Add(x);
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
||||
{
|
||||
return GetMangaFromUrl($"https://www.mangahere.cc/manga/{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 || requestResult.htmlDocument is null)
|
||||
return null;
|
||||
|
||||
Regex idRex = new (@"https:\/\/www\.mangahere\.[a-z]{0,63}\/manga\/([0-9A-z\-]+).*");
|
||||
string id = idRex.Match(url).Groups[1].Value;
|
||||
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, id, url);
|
||||
}
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
||||
{
|
||||
string originalLanguage = "", status = "";
|
||||
Dictionary<string, string> altTitles = new(), links = new();
|
||||
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
||||
|
||||
//We dont get posters, because same origin bs HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//img[contains(concat(' ',normalize-space(@class),' '),' detail-info-cover-img ')]");
|
||||
string coverUrl = "http://static.mangahere.cc/v20230914/mangahere/images/nopicture.jpg";
|
||||
|
||||
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//span[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-title-font ')]");
|
||||
string sortName = titleNode.InnerText;
|
||||
|
||||
List<string> authorNames = document.DocumentNode
|
||||
.SelectNodes("//p[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-say ')]/a")
|
||||
.Select(node => node.InnerText)
|
||||
.ToList();
|
||||
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
|
||||
|
||||
HashSet<string> tags = document.DocumentNode
|
||||
.SelectNodes("//p[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-tag-list ')]/a")
|
||||
.Select(node => node.InnerText)
|
||||
.ToHashSet();
|
||||
List<MangaTag> mangaTags = tags.Select(n => new MangaTag(n)).ToList();
|
||||
|
||||
status = document.DocumentNode.SelectSingleNode("//span[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-title-tip ')]").InnerText;
|
||||
switch (status.ToLower())
|
||||
{
|
||||
case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
||||
case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
|
||||
case "discontinued": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
||||
case "complete": releaseStatus = MangaReleaseStatus.Completed; break;
|
||||
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
|
||||
}
|
||||
|
||||
HtmlNode descriptionNode = document.DocumentNode
|
||||
.SelectSingleNode("//p[contains(concat(' ',normalize-space(@class),' '),' fullcontent ')]");
|
||||
string description = descriptionNode.InnerText;
|
||||
|
||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, 0,
|
||||
originalLanguage, releaseStatus, -1,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
[],
|
||||
[]);
|
||||
|
||||
return (manga, authors, mangaTags, [], []);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||
{
|
||||
string requestUrl = $"https://www.mangahere.cc/manga/{manga.MangaId}";
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
|
||||
return Array.Empty<Chapter>();
|
||||
|
||||
List<string> urls = requestResult.htmlDocument.DocumentNode.SelectNodes("//div[@id='list-1']/ul//li//a[contains(@href, '/manga/')]")
|
||||
.Select(node => node.GetAttributeValue("href", "")).ToList();
|
||||
Regex chapterRex = new(@".*\/manga\/[a-zA-Z0-9\-\._\~\!\$\&\'\(\)\*\+\,\;\=\:\@]+\/v([0-9(TBD)]+)\/c([0-9\.]+)\/.*");
|
||||
|
||||
List<Chapter> chapters = new();
|
||||
foreach (string url in urls)
|
||||
{
|
||||
Match rexMatch = chapterRex.Match(url);
|
||||
|
||||
int? volumeNumber = rexMatch.Groups[1].Value == "TBD" ? null : int.Parse(rexMatch.Groups[1].Value);
|
||||
if(!ChapterNumber.CanParse(rexMatch.Groups[2].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(rexMatch.Groups[2].Value);
|
||||
string fullUrl = $"https://www.mangahere.cc{url}";
|
||||
|
||||
try
|
||||
{
|
||||
chapters.Add(new Chapter(manga, fullUrl, chapterNumber, volumeNumber, null));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
//Return Chapters ordered by Chapter-Number
|
||||
return chapters.Order().ToArray();
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
List<string> imageUrls = new();
|
||||
|
||||
int downloaded = 1;
|
||||
int images = 1;
|
||||
string url = string.Join('/', chapter.Url.Split('/')[..^1]);
|
||||
do
|
||||
{
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest($"{url}/{downloaded}.html", RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
imageUrls.AddRange(ParseImageUrlsFromHtml(requestResult.htmlDocument));
|
||||
|
||||
images = requestResult.htmlDocument.DocumentNode
|
||||
.SelectNodes("//a[contains(@href, '/manga/')]")
|
||||
.MaxBy(node => node.GetAttributeValue("data-page", 0))!.GetAttributeValue("data-page", 0);
|
||||
} while (downloaded++ <= images);
|
||||
|
||||
return imageUrls.ToArray();
|
||||
}
|
||||
|
||||
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
|
||||
{
|
||||
return document.DocumentNode
|
||||
.SelectNodes("//img[contains(concat(' ',normalize-space(@class),' '),' reader-main-img ')]")
|
||||
.Select(node =>
|
||||
{
|
||||
string url = node.GetAttributeValue("src", "");
|
||||
return url.StartsWith("//") ? $"https:{url}" : url;
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
@ -1,234 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class MangaKatana : MangaConnector
|
||||
{
|
||||
public MangaKatana() : base("MangaKatana", ["en"], ["mangakatana.com"])
|
||||
{
|
||||
this.downloadClient = new HttpDownloadClient();
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
||||
{
|
||||
string sanitizedTitle = string.Join("%20", Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
|
||||
string requestUrl = $"https://mangakatana.com/?search={sanitizedTitle}&search_by=book_name";
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||
return [];
|
||||
|
||||
// ReSharper disable once MergeIntoPattern
|
||||
// If a single result is found, the user will be redirected to the results directly instead of a result page
|
||||
if(requestResult.hasBeenRedirected
|
||||
&& requestResult.redirectedToUrl is not null
|
||||
&& requestResult.redirectedToUrl.Contains("mangakatana.com/manga"))
|
||||
{
|
||||
return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1], requestResult.redirectedToUrl) };
|
||||
}
|
||||
|
||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] publications = ParsePublicationsFromHtml(requestResult.result);
|
||||
return publications;
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
||||
{
|
||||
return GetMangaFromUrl($"https://mangakatana.com/manga/{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;
|
||||
return ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1], url);
|
||||
}
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(Stream html)
|
||||
{
|
||||
StreamReader reader = new(html);
|
||||
string htmlString = reader.ReadToEnd();
|
||||
HtmlDocument document = new();
|
||||
document.LoadHtml(htmlString);
|
||||
IEnumerable<HtmlNode> searchResults = document.DocumentNode.SelectNodes("//*[@id='book_list']/div");
|
||||
if (searchResults is null || !searchResults.Any())
|
||||
return [];
|
||||
List<string> urls = new();
|
||||
foreach (HtmlNode mangaResult in searchResults)
|
||||
{
|
||||
urls.Add(mangaResult.Descendants("a").First().GetAttributes()
|
||||
.First(a => a.Name == "href").Value);
|
||||
}
|
||||
|
||||
HashSet<(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 { } x)
|
||||
ret.Add(x);
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(Stream html, string publicationId, string websiteUrl)
|
||||
{
|
||||
StreamReader reader = new(html);
|
||||
string htmlString = reader.ReadToEnd();
|
||||
HtmlDocument document = new();
|
||||
document.LoadHtml(htmlString);
|
||||
Dictionary<string, string> altTitlesDict = new();
|
||||
Dictionary<string, string>? links = null;
|
||||
HashSet<string> tags = new();
|
||||
string[] authorNames = [];
|
||||
string originalLanguage = "";
|
||||
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
||||
|
||||
HtmlNode infoNode = document.DocumentNode.SelectSingleNode("//*[@id='single_book']");
|
||||
string sortName = infoNode.Descendants("h1").First(n => n.HasClass("heading")).InnerText;
|
||||
HtmlNode infoTable = infoNode.SelectSingleNode("//*[@id='single_book']/div[2]/div/ul");
|
||||
|
||||
foreach (HtmlNode row in infoTable.Descendants("li"))
|
||||
{
|
||||
string key = row.SelectNodes("div").First().InnerText.ToLower();
|
||||
string value = row.SelectNodes("div").Last().InnerText;
|
||||
string keySanitized = string.Concat(Regex.Matches(key, "[a-z]"));
|
||||
|
||||
switch (keySanitized)
|
||||
{
|
||||
case "altnames":
|
||||
string[] alts = value.Split(" ; ");
|
||||
for (int i = 0; i < alts.Length; i++)
|
||||
altTitlesDict.Add(i.ToString(), alts[i]);
|
||||
break;
|
||||
case "authorsartists":
|
||||
authorNames = value.Split(',');
|
||||
break;
|
||||
case "status":
|
||||
switch (value.ToLower())
|
||||
{
|
||||
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
|
||||
case "completed": releaseStatus = MangaReleaseStatus.Completed; break;
|
||||
}
|
||||
break;
|
||||
case "genres":
|
||||
tags = row.SelectNodes("div").Last().Descendants("a").Select(a => a.InnerText).ToHashSet();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string coverUrl = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[1]/div").Descendants("img").First()
|
||||
.GetAttributes().First(a => a.Name == "src").Value;
|
||||
|
||||
string description = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[3]/p").InnerText;
|
||||
while (description.StartsWith('\n'))
|
||||
description = description.Substring(1);
|
||||
|
||||
uint year = (uint)DateTime.Now.Year;
|
||||
string yearString = infoTable.Descendants("div").First(d => d.HasClass("updateAt"))
|
||||
.InnerText.Split('-')[^1];
|
||||
|
||||
if(yearString.Contains("ago") == false)
|
||||
{
|
||||
year = uint.Parse(yearString);
|
||||
}
|
||||
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
|
||||
List<MangaTag> mangaTags = tags.Select(n => new MangaTag(n)).ToList();
|
||||
List<MangaAltTitle> altTitles = altTitlesDict.Select(x => new MangaAltTitle(x.Key, x.Value)).ToList();
|
||||
|
||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
||||
originalLanguage, releaseStatus, -1,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
[],
|
||||
altTitles);
|
||||
|
||||
return (manga, authors, mangaTags, [], altTitles);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||
{
|
||||
string requestUrl = $"https://mangakatana.com/manga/{manga.MangaId}";
|
||||
// Leaving this in for verification if the page exists
|
||||
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
|
||||
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
|
||||
return chapters.Order().ToArray();
|
||||
}
|
||||
|
||||
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
|
||||
{
|
||||
// Using HtmlWeb will include the chapters since they are loaded with js
|
||||
HtmlWeb web = new();
|
||||
HtmlDocument document = web.Load(mangaUrl);
|
||||
|
||||
List<Chapter> ret = new();
|
||||
|
||||
HtmlNode chapterList = document.DocumentNode.SelectSingleNode("//div[contains(@class, 'chapters')]/table/tbody");
|
||||
|
||||
Regex volumeRex = new(@"[0-9a-z\-\.]+\/[0-9a-z\-]*v([0-9\.]+)");
|
||||
Regex chapterNumRex = new(@"[0-9a-z\-\.]+\/[0-9a-z\-]*c([0-9\.]+)");
|
||||
Regex chapterNameRex = new(@"Chapter [0-9\.]+:? (.*)");
|
||||
|
||||
foreach (HtmlNode chapterInfo in chapterList.Descendants("tr"))
|
||||
{
|
||||
string fullString = chapterInfo.Descendants("a").First().InnerText;
|
||||
string url = chapterInfo.Descendants("a").First()
|
||||
.GetAttributeValue("href", "");
|
||||
|
||||
int? volumeNumber = volumeRex.IsMatch(url) ? int.Parse(volumeRex.Match(url).Groups[1].Value) : null;
|
||||
if(!ChapterNumber.CanParse(chapterNumRex.Match(url).Groups[1].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(chapterNumRex.Match(url).Groups[1].Value);
|
||||
string chapterName = chapterNameRex.Match(fullString).Groups[1].Value;
|
||||
try
|
||||
{
|
||||
ret.Add(new Chapter(manga, url, chapterNumber, volumeNumber, chapterName));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
string requestUrl = chapter.Url;
|
||||
// Leaving this in to check if the page exists
|
||||
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 string[] ParseImageUrlsFromHtml(HtmlDocument document)
|
||||
{
|
||||
// Images are loaded dynamically, but the urls are present in a piece of js code on the page
|
||||
string js = document.DocumentNode.SelectSingleNode("//script[contains(., 'data-src')]").InnerText
|
||||
.Replace("\r", "")
|
||||
.Replace("\n", "")
|
||||
.Replace("\t", "");
|
||||
|
||||
// ReSharper disable once StringLiteralTypo
|
||||
string regexPat = @"(var thzq=\[')(.*)(,];function)";
|
||||
var group = Regex.Matches(js, regexPat).First().Groups[2].Value.Replace("'", "");
|
||||
var urls = group.Split(',');
|
||||
|
||||
return urls;
|
||||
}
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class MangaLife : MangaConnector
|
||||
{
|
||||
public MangaLife() : base("Manga4Life", ["en"], ["manga4life.com"])
|
||||
{
|
||||
this.downloadClient = new ChromiumDownloadClient();
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
||||
{
|
||||
string sanitizedTitle = WebUtility.UrlEncode(publicationTitle);
|
||||
string requestUrl = $"https://manga4life.com/search/?name={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;
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
||||
{
|
||||
return GetMangaFromUrl($"https://manga4life.com/manga/{publicationId}");
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
|
||||
{
|
||||
Regex publicationIdRex = new(@"https:\/\/(www\.)?manga4life.com\/manga\/(.*)(\/.*)*");
|
||||
string publicationId = publicationIdRex.Match(url).Groups[2].Value;
|
||||
|
||||
RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
||||
if(requestResult.htmlDocument is not null)
|
||||
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
|
||||
return null;
|
||||
}
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
|
||||
{
|
||||
HtmlNode resultsNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']/div[last()]/div[1]/div");
|
||||
if (resultsNode.Descendants("div").Count() == 1 && resultsNode.Descendants("div").First().HasClass("NoResults"))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
|
||||
|
||||
foreach (HtmlNode resultNode in resultsNode.SelectNodes("div"))
|
||||
{
|
||||
string url = resultNode.Descendants().First(d => d.HasClass("SeriesName")).GetAttributeValue("href", "");
|
||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? manga = GetMangaFromUrl($"https://manga4life.com{url}");
|
||||
if (manga is { } x)
|
||||
ret.Add(x);
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
||||
{
|
||||
string originalLanguage = "", status = "";
|
||||
Dictionary<string, string> altTitles = new(), links = new();
|
||||
HashSet<string> tags = new();
|
||||
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
||||
|
||||
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//img");
|
||||
string coverUrl = posterNode.GetAttributeValue("src", "");
|
||||
|
||||
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//div[@class='BoxBody']//div[@class='row']//h1");
|
||||
string sortName = titleNode.InnerText;
|
||||
|
||||
HtmlNode[] authorsNodes = document.DocumentNode
|
||||
.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Author(s):']/..").Descendants("a")
|
||||
.ToArray();
|
||||
List<string> authorNames = new();
|
||||
foreach (HtmlNode authorNode in authorsNodes)
|
||||
authorNames.Add(authorNode.InnerText);
|
||||
List<Author> authors = authorNames.Select(a => new Author(a)).ToList();
|
||||
|
||||
HtmlNode[] genreNodes = document.DocumentNode
|
||||
.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Genre(s):']/..").Descendants("a")
|
||||
.ToArray();
|
||||
foreach (HtmlNode genreNode in genreNodes)
|
||||
tags.Add(genreNode.InnerText);
|
||||
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
|
||||
|
||||
HtmlNode yearNode = document.DocumentNode
|
||||
.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Released:']/..").Descendants("a")
|
||||
.First();
|
||||
uint year = uint.Parse(yearNode.InnerText);
|
||||
|
||||
HtmlNode[] statusNodes = document.DocumentNode
|
||||
.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Status:']/..").Descendants("a")
|
||||
.ToArray();
|
||||
foreach (HtmlNode statusNode in statusNodes)
|
||||
if (statusNode.InnerText.Contains("publish", StringComparison.CurrentCultureIgnoreCase))
|
||||
status = statusNode.InnerText.Split(' ')[0];
|
||||
switch (status.ToLower())
|
||||
{
|
||||
case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
||||
case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
|
||||
case "discontinued": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
||||
case "complete": releaseStatus = MangaReleaseStatus.Completed; break;
|
||||
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
|
||||
}
|
||||
|
||||
HtmlNode descriptionNode = document.DocumentNode
|
||||
.SelectNodes("//div[@class='BoxBody']//div[@class='row']//span[text()='Description:']/..")
|
||||
.Descendants("div").First();
|
||||
string description = descriptionNode.InnerText;
|
||||
|
||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
||||
originalLanguage, releaseStatus, -1,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
[],
|
||||
[]);
|
||||
|
||||
return (manga, authors, mangaTags, [], []);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||
{
|
||||
RequestResult result = downloadClient.MakeRequest($"https://manga4life.com/manga/{manga.MangaId}", RequestType.Default, clickButton:"[class*='ShowAllChapters']");
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
|
||||
{
|
||||
return Array.Empty<Chapter>();
|
||||
}
|
||||
|
||||
HtmlNodeCollection chapterNodes = result.htmlDocument.DocumentNode.SelectNodes(
|
||||
"//a[contains(concat(' ',normalize-space(@class),' '),' ChapterLink ')]");
|
||||
string[] urls = chapterNodes.Select(node => node.GetAttributeValue("href", "")).ToArray();
|
||||
Regex urlRex = new (@"-chapter-([0-9\\.]+)(-index-([0-9\\.]+))?");
|
||||
|
||||
List<Chapter> chapters = new();
|
||||
foreach (string url in urls)
|
||||
{
|
||||
Match rexMatch = urlRex.Match(url);
|
||||
|
||||
int? volumeNumber = rexMatch.Groups[3].Success && rexMatch.Groups[3].Value.Length > 0
|
||||
? int.Parse(rexMatch.Groups[3].Value)
|
||||
: null;
|
||||
|
||||
if(!ChapterNumber.CanParse(rexMatch.Groups[1].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(rexMatch.Groups[1].Value);
|
||||
string fullUrl = $"https://manga4life.com{url}";
|
||||
fullUrl = fullUrl.Replace(Regex.Match(url,"(-page-[0-9])").Value,"");
|
||||
try
|
||||
{
|
||||
chapters.Add(new Chapter(manga, fullUrl, chapterNumber, volumeNumber, null));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
//Return Chapters ordered by Chapter-Number
|
||||
return chapters.Order().ToArray();
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
RequestResult requestResult = this.downloadClient.MakeRequest(chapter.Url, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
HtmlDocument document = requestResult.htmlDocument;
|
||||
|
||||
HtmlNode gallery = document.DocumentNode.Descendants("div").First(div => div.HasClass("ImageGallery"));
|
||||
HtmlNode[] images = gallery.Descendants("img").Where(img => img.HasClass("img-fluid")).ToArray();
|
||||
List<string> urls = new();
|
||||
foreach(HtmlNode galleryImage in images)
|
||||
urls.Add(galleryImage.GetAttributeValue("src", ""));
|
||||
return urls.ToArray();
|
||||
}
|
||||
}
|
@ -1,226 +0,0 @@
|
||||
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"], ["manganato.com"])
|
||||
{
|
||||
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.com/search/story/{sanitizedTitle}";
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||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("search-story-item")).ToList();
|
||||
List<string> urls = new();
|
||||
foreach (HtmlNode mangaResult in searchResults)
|
||||
{
|
||||
urls.Add(mangaResult.Descendants("a").First(n => n.HasClass("item-title")).GetAttributes()
|
||||
.First(a => a.Name == "href").Value);
|
||||
}
|
||||
|
||||
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 { } x)
|
||||
ret.Add(x);
|
||||
}
|
||||
|
||||
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> altTitlesDict = new();
|
||||
Dictionary<string, string>? links = null;
|
||||
HashSet<string> tags = new();
|
||||
string[] authorNames = [];
|
||||
string originalLanguage = "";
|
||||
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
||||
|
||||
HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("story-info-right"));
|
||||
|
||||
string sortName = infoNode.Descendants("h1").First().InnerText;
|
||||
|
||||
HtmlNode infoTable = infoNode.Descendants().First(d => d.Name == "table");
|
||||
|
||||
foreach (HtmlNode row in infoTable.Descendants("tr"))
|
||||
{
|
||||
string key = row.SelectNodes("td").First().InnerText.ToLower();
|
||||
string value = row.SelectNodes("td").Last().InnerText;
|
||||
string keySanitized = string.Concat(Regex.Matches(key, "[a-z]"));
|
||||
|
||||
switch (keySanitized)
|
||||
{
|
||||
case "alternative":
|
||||
string[] alts = value.Split(" ; ");
|
||||
for(int i = 0; i < alts.Length; i++)
|
||||
altTitlesDict.Add(i.ToString(), alts[i]);
|
||||
break;
|
||||
case "authors":
|
||||
authorNames = value.Split('-');
|
||||
for (int i = 0; i < authorNames.Length; i++)
|
||||
authorNames[i] = authorNames[i].Replace("\r\n", "");
|
||||
break;
|
||||
case "status":
|
||||
switch (value.ToLower())
|
||||
{
|
||||
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
|
||||
case "completed": releaseStatus = MangaReleaseStatus.Completed; break;
|
||||
}
|
||||
break;
|
||||
case "genres":
|
||||
string[] genres = value.Split(" - ");
|
||||
for (int i = 0; i < genres.Length; i++)
|
||||
genres[i] = genres[i].Replace("\r\n", "");
|
||||
tags = genres.ToHashSet();
|
||||
break;
|
||||
}
|
||||
}
|
||||
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
|
||||
List<MangaTag> mangaTags = tags.Select(n => new MangaTag(n)).ToList();
|
||||
List<MangaAltTitle> mangaAltTitles = altTitlesDict.Select(a => new MangaAltTitle(a.Key, a.Value)).ToList();
|
||||
|
||||
string coverUrl = document.DocumentNode.Descendants("span").First(s => s.HasClass("info-image")).Descendants("img").First()
|
||||
.GetAttributes().First(a => a.Name == "src").Value;
|
||||
|
||||
string description = document.DocumentNode.Descendants("div").First(d => d.HasClass("panel-story-info-description"))
|
||||
.InnerText.Replace("Description :", "");
|
||||
while (description.StartsWith('\n'))
|
||||
description = description.Substring(1);
|
||||
|
||||
string pattern = "MMM dd,yyyy HH:mm";
|
||||
|
||||
HtmlNode? oldestChapter = document.DocumentNode
|
||||
.SelectNodes("//span[contains(concat(' ',normalize-space(@class),' '),' chapter-time ')]").MaxBy(
|
||||
node => DateTime.ParseExact(node.GetAttributeValue("title", "Dec 31 2400, 23:59"), pattern,
|
||||
CultureInfo.InvariantCulture).Millisecond);
|
||||
|
||||
|
||||
uint year = (uint)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, coverUrl, null, year,
|
||||
originalLanguage, releaseStatus, -1,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
[],
|
||||
mangaAltTitles);
|
||||
|
||||
return (manga, authors, mangaTags, [], mangaAltTitles);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||
{
|
||||
string requestUrl = $"https://chapmanganato.com/{manga.MangaId}";
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
||||
return [];
|
||||
|
||||
//Return Chapters ordered by Chapter-Number
|
||||
if (requestResult.htmlDocument is null)
|
||||
return [];
|
||||
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
|
||||
return chapters.Order().ToArray();
|
||||
}
|
||||
|
||||
|
||||
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
|
||||
{
|
||||
List<Chapter> ret = new();
|
||||
|
||||
HtmlNode chapterList = document.DocumentNode.Descendants("ul").First(l => l.HasClass("row-content-chapter"));
|
||||
|
||||
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("li"))
|
||||
{
|
||||
string fullString = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name")).InnerText;
|
||||
|
||||
string url = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name"))
|
||||
.GetAttributeValue("href", "");
|
||||
|
||||
int? volumeNumber = volRex.IsMatch(fullString)
|
||||
? int.Parse(volRex.Match(fullString).Groups[1].Value)
|
||||
: null;
|
||||
if(!ChapterNumber.CanParse(chapterRex.Match(url).Groups[1].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(chapterRex.Match(url).Groups[1].Value);
|
||||
string chapterName = nameRex.Match(fullString).Groups[3].Value;
|
||||
try
|
||||
{
|
||||
ret.Add(new Chapter(manga, url, chapterNumber, volumeNumber, chapterName));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
ret.Reverse();
|
||||
return ret;
|
||||
}
|
||||
|
||||
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 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();
|
||||
}
|
||||
}
|
@ -1,225 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class Mangaworld : MangaConnector
|
||||
{
|
||||
public Mangaworld() : base("Mangaworld", ["it"], ["www.mangaworld.ac"])
|
||||
{
|
||||
this.downloadClient = new ChromiumDownloadClient();
|
||||
}
|
||||
|
||||
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://www.mangaworld.ac/archive?keyword={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)
|
||||
{
|
||||
if (!document.DocumentNode.SelectSingleNode("//div[@class='comics-grid']").ChildNodes
|
||||
.Any(node => node.HasClass("entry")))
|
||||
return [];
|
||||
|
||||
List<string> urls = document.DocumentNode
|
||||
.SelectNodes(
|
||||
"//div[@class='comics-grid']//div[@class='entry']//a[contains(concat(' ',normalize-space(@class),' '),'thumb')]")
|
||||
.Select(thumb => thumb.GetAttributeValue("href", "")).ToList();
|
||||
|
||||
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 { } x)
|
||||
ret.Add(x);
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
||||
{
|
||||
return GetMangaFromUrl($"https://www.mangaworld.ac/manga/{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;
|
||||
|
||||
Regex idRex = new (@"https:\/\/www\.mangaworld\.[a-z]{0,63}\/manga\/([0-9]+\/[0-9A-z\-]+).*");
|
||||
string id = idRex.Match(url).Groups[1].Value;
|
||||
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, id, url);
|
||||
}
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
||||
{
|
||||
Dictionary<string, string> altTitlesDict = new();
|
||||
string originalLanguage = "";
|
||||
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
||||
|
||||
HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("info"));
|
||||
|
||||
string sortName = infoNode.Descendants("h1").First().InnerText;
|
||||
|
||||
HtmlNode metadata = infoNode.Descendants().First(d => d.HasClass("meta-data"));
|
||||
|
||||
HtmlNode altTitlesNode = metadata.SelectSingleNode("//span[text()='Titoli alternativi: ' or text()='Titolo alternativo: ']/..").ChildNodes[1];
|
||||
string[] alts = altTitlesNode.InnerText.Split(", ");
|
||||
for(int i = 0; i < alts.Length; i++)
|
||||
altTitlesDict.Add(i.ToString(), alts[i]);
|
||||
List<MangaAltTitle> altTitles = altTitlesDict.Select(a => new MangaAltTitle(a.Key, a.Value)).ToList();
|
||||
|
||||
HtmlNode genresNode =
|
||||
metadata.SelectSingleNode("//span[text()='Generi: ' or text()='Genero: ']/..");
|
||||
HashSet<string> tags = genresNode.SelectNodes("a").Select(node => node.InnerText).ToHashSet();
|
||||
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
|
||||
|
||||
HtmlNode authorsNode =
|
||||
metadata.SelectSingleNode("//span[text()='Autore: ' or text()='Autori: ']/..");
|
||||
string[] authorNames = authorsNode.SelectNodes("a").Select(node => node.InnerText).ToArray();
|
||||
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
|
||||
|
||||
string status = metadata.SelectSingleNode("//span[text()='Stato: ']/..").SelectNodes("a").First().InnerText;
|
||||
// ReSharper disable 5 times StringLiteralTypo
|
||||
switch (status.ToLower())
|
||||
{
|
||||
case "cancellato": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
||||
case "in pausa": releaseStatus = MangaReleaseStatus.OnHiatus; break;
|
||||
case "droppato": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
||||
case "finito": releaseStatus = MangaReleaseStatus.Completed; break;
|
||||
case "in corso": releaseStatus = MangaReleaseStatus.Continuing; break;
|
||||
}
|
||||
|
||||
string coverUrl = document.DocumentNode.SelectSingleNode("//img[@class='rounded']").GetAttributeValue("src", "");
|
||||
|
||||
string description = document.DocumentNode.SelectSingleNode("//div[@id='noidungm']").InnerText;
|
||||
|
||||
string yearString = metadata.SelectSingleNode("//span[text()='Anno di uscita: ']/..").SelectNodes("a").First().InnerText;
|
||||
uint year = uint.Parse(yearString);
|
||||
|
||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
||||
originalLanguage, releaseStatus, -1,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
[],
|
||||
altTitles);
|
||||
|
||||
return (manga, authors, mangaTags, [], altTitles);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||
{
|
||||
string requestUrl = $"https://www.mangaworld.ac/manga/{manga.MangaId}";
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
|
||||
return [];
|
||||
|
||||
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
|
||||
return chapters.Order().ToArray();
|
||||
}
|
||||
|
||||
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
|
||||
{
|
||||
List<Chapter> ret = new();
|
||||
|
||||
HtmlNode chaptersWrapper =
|
||||
document.DocumentNode.SelectSingleNode(
|
||||
"//div[contains(concat(' ',normalize-space(@class),' '),'chapters-wrapper')]");
|
||||
|
||||
Regex volumeRex = new(@"[Vv]olume ([0-9]+).*");
|
||||
Regex chapterRex = new(@"[Cc]apitolo ([0-9]+(?:\.[0-9]+)?).*");
|
||||
Regex idRex = new(@".*\/read\/([a-z0-9]+)(?:[?\/].*)?");
|
||||
if (chaptersWrapper.Descendants("div").Any(descendant => descendant.HasClass("volume-element")))
|
||||
{
|
||||
foreach (HtmlNode volNode in document.DocumentNode.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),'volume-element')]"))
|
||||
{
|
||||
string volumeStr = volumeRex.Match(volNode.SelectNodes("div").First(node => node.HasClass("volume")).SelectSingleNode("p").InnerText).Groups[1].Value;
|
||||
int volume = int.Parse(volumeStr);
|
||||
foreach (HtmlNode chNode in volNode.SelectNodes("div").First(node => node.HasClass("volume-chapters")).SelectNodes("div"))
|
||||
{
|
||||
|
||||
string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
|
||||
if(!ChapterNumber.CanParse(numberStr))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(numberStr);
|
||||
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
|
||||
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
|
||||
try
|
||||
{
|
||||
ret.Add(new Chapter(manga, url, chapterNumber, volume, null));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (HtmlNode chNode in chaptersWrapper.SelectNodes("div").Where(node => node.HasClass("chapter")))
|
||||
{
|
||||
string numberStr = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
|
||||
if(!ChapterNumber.CanParse(numberStr))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(numberStr);
|
||||
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
|
||||
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
|
||||
try
|
||||
{
|
||||
ret.Add(new Chapter(manga, url, chapterNumber, null, null));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ret.Reverse();
|
||||
return ret;
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
string requestUrl = $"{chapter.Url}?style=list";
|
||||
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 string[] ParseImageUrlsFromHtml(HtmlDocument document)
|
||||
{
|
||||
List<string> ret = new();
|
||||
|
||||
HtmlNode imageContainer =
|
||||
document.DocumentNode.SelectSingleNode("//div[@id='page']");
|
||||
foreach(HtmlNode imageNode in imageContainer.Descendants("img"))
|
||||
ret.Add(imageNode.GetAttributeValue("src", ""));
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class ManhuaPlus : MangaConnector
|
||||
{
|
||||
public ManhuaPlus() : base("ManhuaPlus", ["en"], ["manhuaplus.org"])
|
||||
{
|
||||
this.downloadClient = new ChromiumDownloadClient();
|
||||
}
|
||||
|
||||
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://manhuaplus.org/search?keyword={sanitizedTitle}";
|
||||
RequestResult requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || 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)
|
||||
{
|
||||
if (document.DocumentNode.SelectSingleNode("//h1/../..").ChildNodes//I already want to not.
|
||||
.Any(node => node.InnerText.Contains("No manga found")))
|
||||
return [];
|
||||
|
||||
List<string> urls = document.DocumentNode
|
||||
.SelectNodes("//h1/../..//a[contains(@href, 'https://manhuaplus.org/manga/') and contains(concat(' ',normalize-space(@class),' '),' clamp ') and not(contains(@href, '/chapter'))]")
|
||||
.Select(mangaNode => mangaNode.GetAttributeValue("href", "")).ToList();
|
||||
|
||||
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 { } x)
|
||||
ret.Add(x);
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
||||
{
|
||||
return GetMangaFromUrl($"https://manhuaplus.org/manga/{publicationId}");
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
|
||||
{
|
||||
Regex publicationIdRex = new(@"https:\/\/manhuaplus.org\/manga\/(.*)(\/.*)*");
|
||||
string publicationId = publicationIdRex.Match(url).Groups[1].Value;
|
||||
|
||||
RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
||||
if((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 && requestResult.htmlDocument is not null && requestResult.redirectedToUrl != "https://manhuaplus.org/home") //When manga doesnt exists it redirects to home
|
||||
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
|
||||
return null;
|
||||
}
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
||||
{
|
||||
string originalLanguage = "", status = "";
|
||||
Dictionary<string, string> altTitles = new(), links = new();
|
||||
HashSet<string> tags = new();
|
||||
MangaReleaseStatus releaseStatus = MangaReleaseStatus.Unreleased;
|
||||
|
||||
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("/html/body/main/div/div/div[2]/div[1]/figure/a/img");//BRUH
|
||||
Regex posterRex = new(@".*(\/uploads/covers/[a-zA-Z0-9\-\._\~\!\$\&\'\(\)\*\+\,\;\=\:\@]+).*");
|
||||
string coverUrl = $"https://manhuaplus.org/{posterRex.Match(posterNode.GetAttributeValue("src", "")).Groups[1].Value}";
|
||||
|
||||
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//h1");
|
||||
string sortName = titleNode.InnerText.Replace("\n", "");
|
||||
|
||||
List<string> authorNames = new();
|
||||
try
|
||||
{
|
||||
HtmlNode[] authorsNodes = document.DocumentNode
|
||||
.SelectNodes("//a[contains(@href, 'https://manhuaplus.org/authors/')]")
|
||||
.ToArray();
|
||||
foreach (HtmlNode authorNode in authorsNodes)
|
||||
authorNames.Add(authorNode.InnerText);
|
||||
}
|
||||
catch (ArgumentNullException e)
|
||||
{
|
||||
}
|
||||
List<Author> authors = authorNames.Select(a => new Author(a)).ToList();
|
||||
|
||||
try
|
||||
{
|
||||
HtmlNode[] genreNodes = document.DocumentNode
|
||||
.SelectNodes("//a[contains(@href, 'https://manhuaplus.org/genres/')]").ToArray();
|
||||
foreach (HtmlNode genreNode in genreNodes)
|
||||
tags.Add(genreNode.InnerText.Replace("\n", ""));
|
||||
}
|
||||
catch (ArgumentNullException e)
|
||||
{
|
||||
}
|
||||
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
|
||||
|
||||
Regex yearRex = new(@"(?:[0-9]{1,2}\/){2}([0-9]{2,4}) [0-9]{1,2}:[0-9]{1,2}");
|
||||
HtmlNode yearNode = document.DocumentNode.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-clock ')]/../span");
|
||||
Match match = yearRex.Match(yearNode.InnerText);
|
||||
uint year = match.Success && match.Groups[1].Success ? uint.Parse(match.Groups[1].Value) : 0;
|
||||
|
||||
status = document.DocumentNode.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-rss ')]/../span").InnerText.Replace("\n", "");
|
||||
switch (status.ToLower())
|
||||
{
|
||||
case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
||||
case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
|
||||
case "discontinued": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
||||
case "complete": releaseStatus = MangaReleaseStatus.Completed; break;
|
||||
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
|
||||
}
|
||||
|
||||
HtmlNode descriptionNode = document.DocumentNode
|
||||
.SelectSingleNode("//div[@id='syn-target']");
|
||||
string description = descriptionNode.InnerText;
|
||||
|
||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
||||
originalLanguage, releaseStatus, -1,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
[],
|
||||
[]);
|
||||
|
||||
return (manga, authors, mangaTags, [], []);
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language="en")
|
||||
{
|
||||
RequestResult result = downloadClient.MakeRequest($"https://manhuaplus.org/manga/{manga.MangaId}", RequestType.Default);
|
||||
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
|
||||
{
|
||||
return Array.Empty<Chapter>();
|
||||
}
|
||||
|
||||
HtmlNodeCollection chapterNodes = result.htmlDocument.DocumentNode.SelectNodes("//li[contains(concat(' ',normalize-space(@class),' '),' chapter ')]//a");
|
||||
string[] urls = chapterNodes.Select(node => node.GetAttributeValue("href", "")).ToArray();
|
||||
Regex urlRex = new (@".*\/chapter-([0-9\-]+).*");
|
||||
|
||||
List<Chapter> chapters = new();
|
||||
foreach (string url in urls)
|
||||
{
|
||||
Match rexMatch = urlRex.Match(url);
|
||||
|
||||
if(!ChapterNumber.CanParse(rexMatch.Groups[1].Value))
|
||||
continue;
|
||||
ChapterNumber chapterNumber = new(rexMatch.Groups[1].Value);
|
||||
string fullUrl = url;
|
||||
try
|
||||
{
|
||||
chapters.Add(new Chapter(manga, fullUrl, chapterNumber, null, null));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
//Return Chapters ordered by Chapter-Number
|
||||
return chapters.Order().ToArray();
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
RequestResult requestResult = this.downloadClient.MakeRequest(chapter.Url, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
HtmlDocument document = requestResult.htmlDocument;
|
||||
|
||||
HtmlNode[] images = document.DocumentNode.SelectNodes("//a[contains(concat(' ',normalize-space(@class),' '),' readImg ')]/img").ToArray();
|
||||
List<string> urls = images.Select(node => node.GetAttributeValue("src", "")).ToList();
|
||||
return urls.ToArray();
|
||||
}
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using API.MangaDownloadClients;
|
||||
using HtmlAgilityPack;
|
||||
using Soenneker.Utils.String.NeedlemanWunsch;
|
||||
|
||||
namespace API.Schema.MangaConnectors;
|
||||
|
||||
public class Weebcentral : MangaConnector
|
||||
{
|
||||
private readonly string _baseUrl = "https://weebcentral.com";
|
||||
|
||||
private readonly string[] _filterWords =
|
||||
{ "a", "the", "of", "as", "to", "no", "for", "on", "with", "be", "and", "in", "wa", "at", "be", "ni" };
|
||||
|
||||
public Weebcentral() : base("Weebcentral", ["en"], ["https://weebcentral.com"])
|
||||
{
|
||||
downloadClient = new ChromiumDownloadClient();
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
||||
{
|
||||
const int limit = 32; //How many values we want returned at once
|
||||
var offset = 0; //"Page"
|
||||
var requestUrl =
|
||||
$"{_baseUrl}/search/data?limit={limit}&offset={offset}&text={publicationTitle}&sort=Best+Match&order=Ascending&official=Any&display_mode=Minimal%20Display";
|
||||
var requestResult =
|
||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
|
||||
requestResult.htmlDocument == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
|
||||
|
||||
return publications;
|
||||
}
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] ParsePublicationsFromHtml(HtmlDocument document)
|
||||
{
|
||||
if (document.DocumentNode.SelectNodes("//article") == null)
|
||||
return [];
|
||||
|
||||
var urls = document.DocumentNode.SelectNodes("/html/body/article/a[@class='link link-hover']")
|
||||
.Select(elem => elem.GetAttributeValue("href", "")).ToList();
|
||||
|
||||
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> ret = new();
|
||||
foreach (var url in urls)
|
||||
{
|
||||
var manga = GetMangaFromUrl(url);
|
||||
if (manga is { } x)
|
||||
ret.Add(x);
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
|
||||
{
|
||||
Regex publicationIdRex = new(@"https:\/\/weebcentral\.com\/series\/(\w*)\/(.*)");
|
||||
var publicationId = publicationIdRex.Match(url).Groups[1].Value;
|
||||
|
||||
var requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
|
||||
if ((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 &&
|
||||
requestResult.htmlDocument is not null)
|
||||
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
|
||||
return null;
|
||||
}
|
||||
|
||||
private (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?) ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
|
||||
{
|
||||
var posterNode =
|
||||
document.DocumentNode.SelectSingleNode("//section[@class='flex items-center justify-center']/picture/img");
|
||||
var coverUrl = posterNode?.GetAttributeValue("src", "") ?? "";
|
||||
|
||||
var titleNode = document.DocumentNode.SelectSingleNode("//section/h1");
|
||||
var sortName = titleNode?.InnerText ?? "Undefined";
|
||||
|
||||
HtmlNode[] authorsNodes =
|
||||
document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Author(s): ']/span")?.ToArray() ?? [];
|
||||
var authorNames = authorsNodes.Select(n => n.InnerText).ToList();
|
||||
List<Author> authors = authorNames.Select(n => new Author(n)).ToList();
|
||||
|
||||
HtmlNode[] genreNodes =
|
||||
document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Tags(s): ']/span")?.ToArray() ?? [];
|
||||
HashSet<string> tags = genreNodes.Select(n => n.InnerText).ToHashSet();
|
||||
List<MangaTag> mangaTags = tags.Select(t => new MangaTag(t)).ToList();
|
||||
|
||||
var statusNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Status: ']/a");
|
||||
var status = statusNode?.InnerText ?? "";
|
||||
var releaseStatus = MangaReleaseStatus.Unreleased;
|
||||
switch (status.ToLower())
|
||||
{
|
||||
case "cancelled": releaseStatus = MangaReleaseStatus.Cancelled; break;
|
||||
case "hiatus": releaseStatus = MangaReleaseStatus.OnHiatus; break;
|
||||
case "complete": releaseStatus = MangaReleaseStatus.Completed; break;
|
||||
case "ongoing": releaseStatus = MangaReleaseStatus.Continuing; break;
|
||||
}
|
||||
|
||||
var yearNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Released: ']/span");
|
||||
var year = uint.Parse(yearNode?.InnerText ?? "0");
|
||||
|
||||
var descriptionNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Description']/p");
|
||||
var description = descriptionNode?.InnerText ?? "Undefined";
|
||||
|
||||
HtmlNode[] altTitleNodes = document.DocumentNode
|
||||
.SelectNodes("//ul/li[strong/text() = 'Associated Name(s)']/ul/li")?.ToArray() ?? [];
|
||||
Dictionary<string, string> altTitlesDict = new(), links = new();
|
||||
for (var i = 0; i < altTitleNodes.Length; i++)
|
||||
altTitlesDict.Add(i.ToString(), altTitleNodes[i].InnerText);
|
||||
List<MangaAltTitle> altTitles = altTitlesDict.Select(a => new MangaAltTitle(a.Key, a.Value)).ToList();
|
||||
|
||||
var originalLanguage = "";
|
||||
|
||||
Manga manga = new (publicationId, sortName, description, websiteUrl, coverUrl, null, year,
|
||||
originalLanguage, releaseStatus, -1,
|
||||
this,
|
||||
authors,
|
||||
mangaTags,
|
||||
[],
|
||||
altTitles);
|
||||
|
||||
return (manga, authors, mangaTags, [], altTitles);
|
||||
}
|
||||
|
||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
||||
{
|
||||
return GetMangaFromUrl($"https://weebcentral.com/series/{publicationId}");
|
||||
}
|
||||
|
||||
private string ToFilteredString(string input)
|
||||
{
|
||||
return string.Join(' ', input.ToLower().Split(' ').Where(word => _filterWords.Contains(word) == false));
|
||||
}
|
||||
|
||||
private SearchResult[] FilteredResults(string publicationTitle, SearchResult[] unfilteredSearchResults)
|
||||
{
|
||||
Dictionary<SearchResult, int> similarity = new();
|
||||
foreach (var sr in unfilteredSearchResults)
|
||||
{
|
||||
List<int> scores = new();
|
||||
var filteredPublicationString = ToFilteredString(publicationTitle);
|
||||
var filteredSString = ToFilteredString(sr.s);
|
||||
scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(filteredSString, filteredPublicationString));
|
||||
foreach (var srA in sr.a)
|
||||
{
|
||||
var filteredAString = ToFilteredString(srA);
|
||||
scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(filteredAString, filteredPublicationString));
|
||||
}
|
||||
|
||||
similarity.Add(sr, scores.Sum() / scores.Count);
|
||||
}
|
||||
|
||||
var ret = similarity.OrderBy(s => s.Value).Take(10).Select(s => s.Key).ToList();
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
public override Chapter[] GetChapters(Manga manga, string language = "en")
|
||||
{
|
||||
var requestUrl = $"{_baseUrl}/series/{manga.MangaId}/full-chapter-list";
|
||||
var 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>();
|
||||
var chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
|
||||
return chapters.Order().ToArray();
|
||||
}
|
||||
|
||||
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
|
||||
{
|
||||
var chaptersWrapper = document.DocumentNode.SelectSingleNode("/html/body");
|
||||
|
||||
Regex chapterRex = new(@".* (\d+)");
|
||||
Regex idRex = new(@"https:\/\/weebcentral\.com\/chapters\/(\w*)");
|
||||
|
||||
var ret = chaptersWrapper.Descendants("a").Select(elem =>
|
||||
{
|
||||
var url = elem.GetAttributeValue("href", "") ?? "Undefined";
|
||||
|
||||
if (!url.StartsWith("https://") && !url.StartsWith("http://"))
|
||||
return new Chapter(manga, "undefined", new ChapterNumber(-1), null, null);
|
||||
|
||||
var idMatch = idRex.Match(url);
|
||||
var id = idMatch.Success ? idMatch.Groups[1].Value : null;
|
||||
|
||||
var chapterNode = elem.SelectSingleNode("span[@class='grow flex items-center gap-2']/span")?.InnerText ??
|
||||
"Undefined";
|
||||
|
||||
var chapterNumberMatch = chapterRex.Match(chapterNode);
|
||||
|
||||
if(!chapterNumberMatch.Success || !ChapterNumber.CanParse(chapterNumberMatch.Groups[1].Value))
|
||||
return new Chapter(manga, "undefined", new ChapterNumber(-1), null, null);
|
||||
ChapterNumber chapterNumber = new(chapterNumberMatch.Groups[1].Value);
|
||||
|
||||
return new Chapter(manga, url, chapterNumber, null, null);
|
||||
}).Where(elem => elem.ChapterNumber < ChapterNumber.Zero && elem.Url != "undefined").ToList();
|
||||
|
||||
ret.Reverse();
|
||||
return ret;
|
||||
}
|
||||
|
||||
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||
{
|
||||
var requestResult = downloadClient.MakeRequest(chapter.Url, RequestType.Default);
|
||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||requestResult.htmlDocument is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var document = requestResult.htmlDocument;
|
||||
|
||||
var imageNodes =
|
||||
document.DocumentNode.SelectNodes($"//section[@hx-get='{chapter.Url}/images']/img")?.ToArray() ?? [];
|
||||
var urls = imageNodes.Select(imgNode => imgNode.GetAttributeValue("src", "")).ToArray();
|
||||
return urls;
|
||||
}
|
||||
|
||||
private struct SearchResult
|
||||
{
|
||||
public string i { get; set; }
|
||||
public string s { get; set; }
|
||||
public string[] a { get; set; }
|
||||
}
|
||||
}
|
@ -1,9 +1,17 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Schema;
|
||||
|
||||
[PrimaryKey("Tag")]
|
||||
public class MangaTag(string tag)
|
||||
{
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string Tag { get; init; } = tag;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Tag}";
|
||||
}
|
||||
}
|
@ -4,18 +4,49 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace API.Schema;
|
||||
|
||||
[PrimaryKey("NotificationId")]
|
||||
public class Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
|
||||
public class Notification
|
||||
{
|
||||
[MaxLength(64)]
|
||||
public string NotificationId { get; init; } = TokenGen.CreateToken("Notification", 64);
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string NotificationId { get; init; }
|
||||
|
||||
public NotificationUrgency Urgency { get; init; } = urgency;
|
||||
[Required]
|
||||
public NotificationUrgency Urgency { get; init; }
|
||||
|
||||
public string Title { get; init; } = title;
|
||||
[StringLength(128)]
|
||||
[Required]
|
||||
public string Title { get; init; }
|
||||
|
||||
public string Message { get; init; } = message;
|
||||
[StringLength(512)]
|
||||
[Required]
|
||||
public string Message { get; init; }
|
||||
|
||||
public DateTime Date { get; init; } = date ?? DateTime.UtcNow;
|
||||
|
||||
public Notification() : this("") { }
|
||||
[Required]
|
||||
public DateTime Date { get; init; }
|
||||
|
||||
public Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
|
||||
{
|
||||
this.NotificationId = TokenGen.CreateToken("Notification");
|
||||
this.Title = title;
|
||||
this.Message = message;
|
||||
this.Urgency = urgency;
|
||||
this.Date = date ?? DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF ONLY!!!
|
||||
/// </summary>
|
||||
public Notification(string notificationId, string title, string message, NotificationUrgency urgency, DateTime date)
|
||||
{
|
||||
this.NotificationId = notificationId;
|
||||
this.Title = title;
|
||||
this.Message = message;
|
||||
this.Urgency = urgency;
|
||||
this.Date = date;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NotificationId} {Urgency} {Title}";
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.NotificationConnectors;
|
||||
|
||||
public class Gotify(string endpoint, string appToken)
|
||||
: NotificationConnector(TokenGen.CreateToken(typeof(Gotify), 64), NotificationConnectorType.Gotify)
|
||||
{
|
||||
public string Endpoint { get; init; } = endpoint;
|
||||
public string AppToken { get; init; } = appToken;
|
||||
|
||||
public override void SendNotification(string title, string notificationText)
|
||||
{
|
||||
MessageData message = new(title, notificationText);
|
||||
HttpRequestMessage request = new(HttpMethod.Post, $"{endpoint}/message");
|
||||
request.Headers.Add("X-Gotify-Key", this.AppToken);
|
||||
request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json");
|
||||
HttpResponseMessage response = _client.Send(request);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
StreamReader sr = new (response.Content.ReadAsStream());
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageData
|
||||
{
|
||||
// ReSharper disable four times UnusedAutoPropertyAccessor.Local
|
||||
public string message { get; }
|
||||
public long priority { get; }
|
||||
public string title { get; }
|
||||
public Dictionary<string, object> extras { get; }
|
||||
|
||||
public MessageData(string title, string message)
|
||||
{
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
this.extras = new();
|
||||
this.priority = 4;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.NotificationConnectors;
|
||||
|
||||
public class Lunasea(string id)
|
||||
: NotificationConnector(TokenGen.CreateToken(typeof(Lunasea), 64), NotificationConnectorType.LunaSea)
|
||||
{
|
||||
public string Id { get; init; } = id;
|
||||
public override void SendNotification(string title, string notificationText)
|
||||
{
|
||||
MessageData message = new(title, notificationText);
|
||||
HttpRequestMessage request = new(HttpMethod.Post, $"https://notify.lunasea.app/v1/custom/{id}");
|
||||
request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json");
|
||||
HttpResponseMessage response = _client.Send(request);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
StreamReader sr = new (response.Content.ReadAsStream());
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageData
|
||||
{
|
||||
// ReSharper disable twice UnusedAutoPropertyAccessor.Local
|
||||
public string title { get; }
|
||||
public string body { get; }
|
||||
|
||||
public MessageData(string title, string body)
|
||||
{
|
||||
this.title = title;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,82 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text;
|
||||
using log4net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace API.Schema.NotificationConnectors;
|
||||
|
||||
[PrimaryKey("NotificationConnectorId")]
|
||||
public abstract class NotificationConnector(string notificationConnectorId, NotificationConnectorType notificationConnectorType)
|
||||
[PrimaryKey("Name")]
|
||||
public class NotificationConnector(string name, string url, Dictionary<string, string> headers, string httpMethod, string body)
|
||||
{
|
||||
[MaxLength(64)]
|
||||
public string NotificationConnectorId { get; } = notificationConnectorId;
|
||||
public NotificationConnectorType NotificationConnectorType { get; init; } = notificationConnectorType;
|
||||
[StringLength(64)]
|
||||
[Required]
|
||||
public string Name { get; init; } = name;
|
||||
|
||||
[StringLength(2048)]
|
||||
[Required]
|
||||
[Url]
|
||||
public string Url { get; internal set; } = url;
|
||||
|
||||
[Required]
|
||||
public Dictionary<string, string> Headers { get; internal set; } = headers;
|
||||
|
||||
[StringLength(8)]
|
||||
[Required]
|
||||
public string HttpMethod { get; internal set; } = httpMethod;
|
||||
|
||||
[StringLength(4096)]
|
||||
[Required]
|
||||
public string Body { get; internal set; } = body;
|
||||
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
private readonly HttpClient Client = new()
|
||||
{
|
||||
DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } }
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
protected readonly HttpClient _client = new();
|
||||
|
||||
public abstract void SendNotification(string title, string notificationText);
|
||||
protected ILog Log = LogManager.GetLogger(name);
|
||||
|
||||
public void SendNotification(string title, string notificationText)
|
||||
{
|
||||
Log.Info($"Sending notification: {title} - {notificationText}");
|
||||
CustomWebhookFormatProvider formatProvider = new (title, notificationText);
|
||||
string formattedUrl = string.Format(formatProvider, Url);
|
||||
string formattedBody = string.Format(formatProvider, Body, title, notificationText);
|
||||
Dictionary<string, string> formattedHeaders = Headers.ToDictionary(h => h.Key,
|
||||
h => string.Format(formatProvider, h.Value, title, notificationText));
|
||||
|
||||
HttpRequestMessage request = new(System.Net.Http.HttpMethod.Parse(HttpMethod), formattedUrl);
|
||||
foreach (var (key, value) in formattedHeaders)
|
||||
request.Headers.Add(key, value);
|
||||
request.Content = new StringContent(formattedBody);
|
||||
Log.Debug($"Request: {request}");
|
||||
|
||||
HttpResponseMessage response = Client.Send(request);
|
||||
Log.Debug($"Response status code: {response.StatusCode}");
|
||||
}
|
||||
|
||||
private class CustomWebhookFormatProvider(string title, string text) : IFormatProvider
|
||||
{
|
||||
public object? GetFormat(Type? formatType)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public string Format(string fmt, object arg, IFormatProvider provider)
|
||||
{
|
||||
if(arg.GetType() != typeof(string))
|
||||
return arg.ToString() ?? string.Empty;
|
||||
|
||||
StringBuilder sb = new StringBuilder(fmt);
|
||||
sb.Replace("%title", title);
|
||||
sb.Replace("%text", text);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
namespace API.Schema.NotificationConnectors;
|
||||
|
||||
|
||||
public enum NotificationConnectorType : byte
|
||||
{
|
||||
Gotify = 0,
|
||||
LunaSea = 1,
|
||||
Ntfy = 2
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user