mirror of
https://github.com/C9Glax/tranga.git
synced 2025-06-14 15:27:53 +02:00
Compare commits
303 Commits
6a8df2f5f8
...
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 | |||
be6b3da1be | |||
280d715a7c | |||
d0b775444d | |||
b4edcccafe | |||
268441a47d | |||
b818f63f2a | |||
1701881f4b | |||
78a9322036 | |||
e5be5703f8 | |||
cc32b3dfae | |||
110a0bf481 | |||
fdbe585aa0 | |||
ce217aae4f | |||
123a8b06b2 | |||
2350c5a04b | |||
f532e2ff76 | |||
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
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is the Website free to access?
|
label: Is the Website free to access?
|
||||||
description: We can't support pay-to-use sites.
|
description: We can't support pay-to-use sites, or captcha-proxied sites as Cloudflare.
|
||||||
options:
|
options:
|
||||||
- label: The Website is freely accessible.
|
- label: The Website is freely accessible.
|
||||||
required: true
|
required: true
|
||||||
@ -20,4 +20,4 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Anything else?
|
label: Anything else?
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
@ -17,12 +17,12 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/setup-qemu-action#usage
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.2.0
|
uses: docker/setup-qemu-action@v3.6.0
|
||||||
|
|
||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: 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
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push API
|
- name: Build and push API
|
||||||
uses: docker/build-push-action@v6.9.0
|
uses: docker/build-push-action@v6.15.0
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile
|
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
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
- name: Set up QEMU
|
- 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
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: 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
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push API
|
- name: Build and push API
|
||||||
uses: docker/build-push-action@v6.9.0
|
uses: docker/build-push-action@v6.15.0
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile
|
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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "Server-V2" ]
|
branches: [ "postgres-Server-V2" ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -17,12 +17,12 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/setup-qemu-action#usage
|
# https://github.com/docker/setup-qemu-action#usage
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.2.0
|
uses: docker/setup-qemu-action@v3.6.0
|
||||||
|
|
||||||
# https://github.com/marketplace/actions/docker-setup-buildx
|
# https://github.com/marketplace/actions/docker-setup-buildx
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: 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
|
# https://github.com/docker/login-action#docker-hub
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
# https://github.com/docker/build-push-action#multi-platform-image
|
# https://github.com/docker/build-push-action#multi-platform-image
|
||||||
- name: Build and push API
|
- name: Build and push API
|
||||||
uses: docker/build-push-action@v6.9.0
|
uses: docker/build-push-action@v6.15.0
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
@ -11,23 +11,25 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.71" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
|
||||||
<PackageReference Include="log4net" Version="3.0.3" />
|
<PackageReference Include="log4net" Version="3.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.5" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Npgsql" Version="9.0.2" />
|
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||||
<PackageReference Include="PuppeteerSharp" Version="20.0.5" />
|
<PackageReference Include="PuppeteerSharp" Version="20.1.3" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||||
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.697" />
|
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.929" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="9.0.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="8.1.0" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="9.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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 API.Schema.Jobs;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
|
using log4net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
|
|
||||||
[ApiVersion(2)]
|
[ApiVersion(2)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Produces("application/json")]
|
|
||||||
[Route("v{version:apiVersion}/[controller]")]
|
[Route("v{version:apiVersion}/[controller]")]
|
||||||
public class JobController(PgsqlContext context) : Controller
|
public class JobController(PgsqlContext context, ILog Log) : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all Jobs
|
/// Returns all Jobs
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Array of Jobs</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<Job[]>(Status200OK)]
|
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||||
public IActionResult GetAllJobs()
|
public IActionResult GetAllJobs()
|
||||||
{
|
{
|
||||||
Job[] ret = context.Jobs.ToArray();
|
Job[] ret = context.Jobs.ToArray();
|
||||||
@ -28,9 +32,9 @@ public class JobController(PgsqlContext context) : Controller
|
|||||||
/// Returns Jobs with requested Job-IDs
|
/// Returns Jobs with requested Job-IDs
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ids">Array of Job-IDs</param>
|
/// <param name="ids">Array of Job-IDs</param>
|
||||||
/// <returns>Array of Jobs</returns>
|
/// <response code="200"></response>
|
||||||
[HttpPost("WithIDs")]
|
[HttpPost("WithIDs")]
|
||||||
[ProducesResponseType<Job[]>(Status200OK)]
|
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||||
public IActionResult GetJobs([FromBody]string[] ids)
|
public IActionResult GetJobs([FromBody]string[] ids)
|
||||||
{
|
{
|
||||||
Job[] ret = context.Jobs.Where(job => ids.Contains(job.JobId)).ToArray();
|
Job[] ret = context.Jobs.Where(job => ids.Contains(job.JobId)).ToArray();
|
||||||
@ -40,40 +44,55 @@ public class JobController(PgsqlContext context) : Controller
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all Jobs in requested State
|
/// Get all Jobs in requested State
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="state">Requested Job-State</param>
|
/// <param name="JobState">Requested Job-State</param>
|
||||||
/// <returns>Array of Jobs</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet("State/{state}")]
|
[HttpGet("State/{JobState}")]
|
||||||
[ProducesResponseType<Job[]>(Status200OK)]
|
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||||
public IActionResult GetJobsInState(JobState state)
|
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);
|
return Ok(jobsInState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all Jobs of requested Type
|
/// Returns all Jobs of requested Type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">Requested Job-Type</param>
|
/// <param name="JobType">Requested Job-Type</param>
|
||||||
/// <returns>Array of Jobs</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet("Type/{type}")]
|
[HttpGet("Type/{JobType}")]
|
||||||
[ProducesResponseType<Job[]>(Status200OK)]
|
[ProducesResponseType<Job[]>(Status200OK, "application/json")]
|
||||||
public IActionResult GetJobsOfType(JobType type)
|
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);
|
return Ok(jobsOfType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return Job with ID
|
/// Return Job with ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Job-ID</param>
|
/// <param name="JobId">Job-ID</param>
|
||||||
/// <returns>Job</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet("{id}")]
|
/// <response code="404">Job with ID could not be found</response>
|
||||||
[ProducesResponseType<Job>(Status200OK)]
|
[HttpGet("{JobId}")]
|
||||||
|
[ProducesResponseType<Job>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status404NotFound)]
|
[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
|
return (ret is not null) switch
|
||||||
{
|
{
|
||||||
true => Ok(ret),
|
true => Ok(ret),
|
||||||
@ -82,58 +101,98 @@ public class JobController(PgsqlContext context) : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new CreateNewDownloadChapterJob
|
/// Create a new DownloadAvailableChaptersJob
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">ID of the Manga, and how often we check again</param>
|
/// <param name="MangaId">ID of Manga</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <param name="record">Job-Configuration</param>
|
||||||
[HttpPut("NewDownloadChapterJob/{mangaId}")]
|
/// <response code="201">Job-IDs</response>
|
||||||
[ProducesResponseType(Status201Created)]
|
/// <response code="400">Could not find ToLibrary with ID</response>
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
/// <response code="404">Could not find Manga with ID</response>
|
||||||
public IActionResult CreateNewDownloadChapterJob(string mangaId, [FromBody]ulong recurrenceTime)
|
/// <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);
|
if (context.Mangas.Find(MangaId) is not { } m)
|
||||||
return AddJob(job);
|
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>
|
/// <summary>
|
||||||
/// Create a new DownloadSingleChapterJob
|
/// Create a new DownloadSingleChapterJob
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="chapterId">ID of the Chapter</param>
|
/// <param name="ChapterId">ID of the Chapter</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="201">Job-IDs</response>
|
||||||
[HttpPut("DownloadSingleChapterJob/{chapterId}")]
|
/// <response code="404">Could not find Chapter with ID</response>
|
||||||
[ProducesResponseType(Status201Created)]
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[HttpPut("DownloadSingleChapterJob/{ChapterId}")]
|
||||||
public IActionResult CreateNewDownloadChapterJob(string chapterId)
|
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||||
|
[ProducesResponseType(Status404NotFound)]
|
||||||
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
|
public IActionResult CreateNewDownloadChapterJob(string ChapterId)
|
||||||
{
|
{
|
||||||
Job job = new DownloadSingleChapterJob(chapterId);
|
if(context.Chapters.Find(ChapterId) is not { } c)
|
||||||
return AddJob(job);
|
return NotFound();
|
||||||
|
Job job = new DownloadSingleChapterJob(c);
|
||||||
|
return AddJobs([job]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new UpdateMetadataJob
|
/// Create a new UpdateChaptersDownloadedJob
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="mangaId">ID of the Manga</param>
|
/// <param name="MangaId">ID of the Manga</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="201">Job-IDs</response>
|
||||||
[HttpPut("UpdateMetadataJob/{mangaId}")]
|
/// <response code="201">Could not find Manga with ID</response>
|
||||||
[ProducesResponseType(Status201Created)]
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[HttpPut("UpdateFilesJob/{MangaId}")]
|
||||||
public IActionResult CreateUpdateMetadataJob(string mangaId)
|
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||||
|
[ProducesResponseType(Status404NotFound)]
|
||||||
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
|
public IActionResult CreateUpdateFilesDownloadedJob(string MangaId)
|
||||||
{
|
{
|
||||||
Job job = new UpdateMetadataJob(0, mangaId);
|
if(context.Mangas.Find(MangaId) is not { } m)
|
||||||
return AddJob(job);
|
return NotFound();
|
||||||
|
Job job = new UpdateChaptersDownloadedJob(m, 0);
|
||||||
|
return AddJobs([job]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new UpdateMetadataJob for all Manga
|
/// Create a new UpdateMetadataJob for all Manga
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="201">Job-IDs</response>
|
||||||
[HttpPut("UpdateMetadataJob")]
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[ProducesResponseType(Status201Created)]
|
[HttpPut("UpdateAllFilesJob")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||||
public IActionResult CreateUpdateAllMetadataJob()
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
|
public IActionResult CreateUpdateAllFilesDownloadedJob()
|
||||||
{
|
{
|
||||||
List<string> ids = context.Manga.Select(m => m.MangaId).ToList();
|
List<UpdateChaptersDownloadedJob> jobs = context.Mangas.Select(m => new UpdateChaptersDownloadedJob(m, 0, null, null)).ToList();
|
||||||
List<UpdateMetadataJob> jobs = ids.Select(id => new UpdateMetadataJob(0, id)).ToList();
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
context.Jobs.AddRange(jobs);
|
context.Jobs.AddRange(jobs);
|
||||||
@ -142,49 +201,124 @@ public class JobController(PgsqlContext context) : Controller
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
return StatusCode(500, e.Message);
|
Log.Error(e);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IActionResult AddJob(Job job)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.Jobs.Add(job);
|
|
||||||
context.SaveChanges();
|
|
||||||
return Created();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return StatusCode(500, e.Message);
|
return StatusCode(500, e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delete Job with ID
|
/// Not Implemented: Create a new UpdateMetadataJob
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Job-ID</param>
|
/// <param name="MangaId">ID of the Manga</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="201">Job-IDs</response>
|
||||||
[HttpDelete("{id}")]
|
/// <response code="404">Could not find Manga with ID</response>
|
||||||
[ProducesResponseType(Status200OK)]
|
/// <response code="500">Error during Database Operation</response>
|
||||||
|
[HttpPut("UpdateMetadataJob/{MangaId}")]
|
||||||
|
[ProducesResponseType<string[]>(Status201Created, "application/json")]
|
||||||
[ProducesResponseType(Status404NotFound)]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
public IActionResult DeleteJob(string id)
|
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
|
try
|
||||||
{
|
{
|
||||||
Job? ret = context.Jobs.Find(id);
|
context.Jobs.AddRange(jobs);
|
||||||
switch (ret is not null)
|
context.SaveChanges();
|
||||||
{
|
return new CreatedResult((string?)null, jobs.Select(j => j.JobId).ToArray());
|
||||||
case true:
|
|
||||||
context.Remove(ret);
|
|
||||||
context.SaveChanges();
|
|
||||||
return Ok();
|
|
||||||
case false: return NotFound();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
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);
|
return StatusCode(500, e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,32 +326,52 @@ public class JobController(PgsqlContext context) : Controller
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the Job with the requested ID
|
/// Starts the Job with the requested ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Job-ID</param>
|
/// <param name="JobId">Job-ID</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <param name="startDependencies">Start Jobs necessary for execution</param>
|
||||||
[HttpPost("{id}/Start")]
|
/// <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(Status202Accepted)]
|
||||||
[ProducesResponseType(Status404NotFound)]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
[ProducesResponseType(Status409Conflict)]
|
||||||
public IActionResult StartJob(string id)
|
[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)
|
if (ret is null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
List<Job> dependencies = startDependencies ? ret.GetDependenciesAndSelf() : [ret];
|
||||||
|
|
||||||
try
|
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();
|
context.SaveChanges();
|
||||||
return Accepted();
|
return Accepted();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Log.Error(e);
|
||||||
return StatusCode(500, e.Message);
|
return StatusCode(500, e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/Stop")]
|
/// <summary>
|
||||||
public IActionResult StopJob(string id)
|
/// 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;
|
||||||
|
using API.Schema.Contexts;
|
||||||
using API.Schema.LibraryConnectors;
|
using API.Schema.LibraryConnectors;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
|
using log4net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
|
|
||||||
@ -8,16 +10,15 @@ namespace API.Controllers;
|
|||||||
|
|
||||||
[ApiVersion(2)]
|
[ApiVersion(2)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Produces("application/json")]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
[Route("v{v:apiVersion}/[controller]")]
|
||||||
public class LibraryConnectorController(PgsqlContext context) : Controller
|
public class LibraryConnectorController(LibraryContext context, ILog Log) : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all configured Library-Connectors
|
/// Gets all configured ToLibrary-Connectors
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Array of configured Library-Connectors</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<LibraryConnector[]>(Status200OK)]
|
[ProducesResponseType<LibraryConnector[]>(Status200OK, "application/json")]
|
||||||
public IActionResult GetAllConnectors()
|
public IActionResult GetAllConnectors()
|
||||||
{
|
{
|
||||||
LibraryConnector[] connectors = context.LibraryConnectors.ToArray();
|
LibraryConnector[] connectors = context.LibraryConnectors.ToArray();
|
||||||
@ -25,16 +26,17 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns Library-Connector with requested ID
|
/// Returns ToLibrary-Connector with requested ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Library-Connector-ID</param>
|
/// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
|
||||||
/// <returns>Library-Connector</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet("{id}")]
|
/// <response code="404">Connector with ID not found.</response>
|
||||||
[ProducesResponseType<LibraryConnector>(Status200OK)]
|
[HttpGet("{LibraryControllerId}")]
|
||||||
|
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status404NotFound)]
|
[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
|
return (ret is not null) switch
|
||||||
{
|
{
|
||||||
true => Ok(ret),
|
true => Ok(ret),
|
||||||
@ -43,13 +45,14 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new Library-Connector
|
/// Creates a new ToLibrary-Connector
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryConnector">Library-Connector</param>
|
/// <param name="libraryConnector">ToLibrary-Connector</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="201"></response>
|
||||||
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status201Created)]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector)
|
public IActionResult CreateConnector([FromBody]LibraryConnector libraryConnector)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -60,35 +63,37 @@ public class LibraryConnectorController(PgsqlContext context) : Controller
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Log.Error(e);
|
||||||
return StatusCode(500, e.Message);
|
return StatusCode(500, e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes the Library-Connector with the requested ID
|
/// Deletes the ToLibrary-Connector with the requested ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Library-Connector-ID</param>
|
/// <param name="LibraryControllerId">ToLibrary-Connector-ID</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="200"></response>
|
||||||
[HttpDelete("{id}")]
|
/// <response code="404">Connector with ID not found.</response>
|
||||||
|
/// <response code="500">Error during Database Operation</response>
|
||||||
|
[HttpDelete("{LibraryControllerId}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType(Status404NotFound)]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
public IActionResult DeleteConnector(string id)
|
public IActionResult DeleteConnector(string LibraryControllerId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LibraryConnector? ret = context.LibraryConnectors.Find(id);
|
LibraryConnector? ret = context.LibraryConnectors.Find(LibraryControllerId);
|
||||||
switch (ret is not null)
|
if (ret is null)
|
||||||
{
|
return NotFound();
|
||||||
case true:
|
|
||||||
context.Remove(ret);
|
context.Remove(ret);
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
return Ok();
|
return Ok();
|
||||||
case false: return NotFound();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Log.Error(e);
|
||||||
return StatusCode(500, e.Message);
|
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;
|
||||||
|
using API.Schema.Contexts;
|
||||||
|
using API.Schema.Jobs;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
|
using log4net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
|
|
||||||
[ApiVersion(2)]
|
[ApiVersion(2)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Produces("application/json")]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
[Route("v{v:apiVersion}/[controller]")]
|
||||||
public class MangaController(PgsqlContext context) : Controller
|
public class MangaController(PgsqlContext context, ILog Log) : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all cached Manga
|
/// Returns all cached Manga
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Array of Manga</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<Manga[]>(Status200OK)]
|
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||||
public IActionResult GetAllManga()
|
public IActionResult GetAllManga()
|
||||||
{
|
{
|
||||||
Manga[] ret = context.Manga.ToArray();
|
Manga[] ret = context.Mangas.ToArray();
|
||||||
return Ok(ret);
|
return Ok(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,137 +34,319 @@ public class MangaController(PgsqlContext context) : Controller
|
|||||||
/// Returns all cached Manga with IDs
|
/// Returns all cached Manga with IDs
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ids">Array of Manga-IDs</param>
|
/// <param name="ids">Array of Manga-IDs</param>
|
||||||
/// <returns>Array of Manga</returns>
|
/// <response code="200"></response>
|
||||||
[HttpPost("WithIDs")]
|
[HttpPost("WithIDs")]
|
||||||
[ProducesResponseType<Manga[]>(Status200OK)]
|
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
|
||||||
public IActionResult GetManga([FromBody]string[] ids)
|
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);
|
return Ok(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return Manga with ID
|
/// Return Manga with ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Manga-ID</param>
|
/// <param name="MangaId">Manga-ID</param>
|
||||||
/// <returns>Manga</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet("{id}")]
|
/// <response code="404">Manga with ID not found</response>
|
||||||
[ProducesResponseType<Manga>(Status200OK)]
|
[HttpGet("{MangaId}")]
|
||||||
|
[ProducesResponseType<Manga>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status404NotFound)]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
public IActionResult GetManga(string id)
|
public IActionResult GetManga(string MangaId)
|
||||||
{
|
{
|
||||||
Manga? ret = context.Manga.Find(id);
|
Manga? ret = context.Mangas.Find(MangaId);
|
||||||
return (ret is not null) switch
|
if (ret is null)
|
||||||
{
|
return NotFound();
|
||||||
true => Ok(ret),
|
return Ok(ret);
|
||||||
false => NotFound()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delete Manga with ID
|
/// Delete Manga with ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Manga-ID</param>
|
/// <param name="MangaId">Manga-ID</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="200"></response>
|
||||||
[HttpDelete("{id}")]
|
/// <response code="404">Manga with ID not found</response>
|
||||||
|
/// <response code="500">Error during Database Operation</response>
|
||||||
|
[HttpDelete("{MangaId}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType(Status404NotFound)]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
public IActionResult DeleteManga(string id)
|
public IActionResult DeleteManga(string MangaId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Manga? ret = context.Manga.Find(id);
|
Manga? ret = context.Mangas.Find(MangaId);
|
||||||
switch (ret is not null)
|
if (ret is null)
|
||||||
{
|
return NotFound();
|
||||||
case true:
|
|
||||||
context.Remove(ret);
|
context.Remove(ret);
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
return Ok();
|
return Ok();
|
||||||
case false: return NotFound();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Log.Error(e);
|
||||||
return StatusCode(500, e.Message);
|
return StatusCode(500, e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns URL of Cover of Manga
|
/// Returns Cover of Manga
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Manga-ID</param>
|
/// <param name="MangaId">Manga-ID</param>
|
||||||
/// <returns>URL of Cover</returns>
|
/// <param name="width">If width is provided, height needs to also be provided</param>
|
||||||
[HttpGet("{id}/Cover")]
|
/// <param name="height">If height is provided, width needs to also be provided</param>
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
/// <response code="200">JPEG Image</response>
|
||||||
public IActionResult GetCover(string id)
|
/// <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>
|
/// <summary>
|
||||||
/// Returns all Chapters of Manga
|
/// Returns all Chapters of Manga
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Manga-ID</param>
|
/// <param name="MangaId">Manga-ID</param>
|
||||||
/// <returns>Array of Chapters</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet("{id}/Chapters")]
|
/// <response code="404">Manga with ID not found</response>
|
||||||
[ProducesResponseType<Chapter[]>(Status200OK)]
|
[HttpGet("{MangaId}/Chapters")]
|
||||||
[ProducesResponseType<string>(Status404NotFound)]
|
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||||
public IActionResult GetChapters(string id)
|
[ProducesResponseType(Status404NotFound)]
|
||||||
|
public IActionResult GetChapters(string MangaId)
|
||||||
{
|
{
|
||||||
Manga? m = context.Manga.Find(id);
|
if(context.Mangas.Find(MangaId) is not { } m)
|
||||||
if (m is null)
|
return NotFound();
|
||||||
return NotFound("Manga could not be found");
|
|
||||||
Chapter[] ret = context.Chapters.Where(c => c.ParentManga.MangaId == m.MangaId).ToArray();
|
Chapter[] chapters = m.Chapters.ToArray();
|
||||||
return Ok(ret);
|
return Ok(chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the latest Chapter of requested Manga
|
/// Returns all downloaded Chapters for Manga with ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Manga-ID</param>
|
/// <param name="MangaId">Manga-ID</param>
|
||||||
/// <returns>Latest Chapter</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet("{id}/Chapter/Latest")]
|
/// <response code="204">No available chapters</response>
|
||||||
[ProducesResponseType<Chapter>(Status200OK)]
|
/// <response code="404">Manga with ID not found.</response>
|
||||||
[ProducesResponseType<string>(Status404NotFound)]
|
[HttpGet("{MangaId}/Chapters/Downloaded")]
|
||||||
public IActionResult GetLatestChapter(string id)
|
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
|
||||||
|
[ProducesResponseType(Status204NoContent)]
|
||||||
|
[ProducesResponseType(Status404NotFound)]
|
||||||
|
public IActionResult GetChaptersDownloaded(string MangaId)
|
||||||
{
|
{
|
||||||
Manga? m = context.Manga.Find(id);
|
if(context.Mangas.Find(MangaId) is not { } m)
|
||||||
if (m is null)
|
return NotFound();
|
||||||
return NotFound("Manga could not be found");
|
|
||||||
List<Chapter> chapters = context.Chapters.Where(c => c.ParentManga.MangaId == m.MangaId).ToList();
|
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();
|
Chapter? max = chapters.Max();
|
||||||
if (max is null)
|
if (max is null)
|
||||||
return NotFound("Chapter could not be found");
|
return StatusCode(500, "Max chapter could not be found");
|
||||||
|
|
||||||
return Ok(max);
|
return Ok(max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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>
|
/// <summary>
|
||||||
/// Configure the cut-off for Manga
|
/// Configure the cut-off for Manga
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This is important for the DownloadNewChapters-Job</remarks>
|
/// <param name="MangaId">Manga-ID</param>
|
||||||
/// <param name="id">Manga-ID</param>
|
/// <param name="chapterThreshold">Threshold (Chapter Number)</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="200"></response>
|
||||||
[HttpPatch("{id}/IgnoreChaptersBefore")]
|
/// <response code="404">Manga with ID not found.</response>
|
||||||
[ProducesResponseType<float>(Status200OK)]
|
/// <response code="500">Error during Database Operation</response>
|
||||||
public IActionResult IgnoreChaptersBefore(string id)
|
[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)
|
if (m is null)
|
||||||
return NotFound("Manga could not be found");
|
return NotFound();
|
||||||
return Ok(m.IgnoreChapterBefore);
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
m.IgnoreChaptersBefore = chapterThreshold;
|
||||||
|
context.SaveChanges();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(e);
|
||||||
|
return StatusCode(500, e.Message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Move the Directory the .cbz-files are located in
|
/// Move Manga to different ToLibrary
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Manga-ID</param>
|
/// <param name="MangaId">Manga-ID</param>
|
||||||
/// <param name="folder">New Directory-Path</param>
|
/// <param name="LibraryId">ToLibrary-Id</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="202">Folder is going to be moved</response>
|
||||||
[HttpPost("{id}/MoveFolder")]
|
/// <response code="404">MangaId or LibraryId not found</response>
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
/// <response code="500">Error during Database Operation</response>
|
||||||
public IActionResult MoveFolder(string id, [FromBody]string folder)
|
[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 API.Schema.NotificationConnectors;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
|
using log4net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
|
|
||||||
@ -10,14 +13,14 @@ namespace API.Controllers;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
[Route("v{v:apiVersion}/[controller]")]
|
||||||
public class NotificationConnectorController(PgsqlContext context) : Controller
|
public class NotificationConnectorController(NotificationsContext context, ILog Log) : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all configured Notification-Connectors
|
/// Gets all configured Notification-Connectors
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Array of configured Notification-Connectors</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<NotificationConnector[]>(Status200OK)]
|
[ProducesResponseType<NotificationConnector[]>(Status200OK, "application/json")]
|
||||||
public IActionResult GetAllConnectors()
|
public IActionResult GetAllConnectors()
|
||||||
{
|
{
|
||||||
NotificationConnector[] ret = context.NotificationConnectors.ToArray();
|
NotificationConnector[] ret = context.NotificationConnectors.ToArray();
|
||||||
@ -27,14 +30,15 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns Notification-Connector with requested ID
|
/// Returns Notification-Connector with requested ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Notification-Connector-ID</param>
|
/// <param name="NotificationConnectorId">Notification-Connector-ID</param>
|
||||||
/// <returns>Notification-Connector</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet("{id}")]
|
/// <response code="404">NotificationConnector with ID not found</response>
|
||||||
[ProducesResponseType<NotificationConnector>(Status200OK)]
|
[HttpGet("{NotificationConnectorId}")]
|
||||||
|
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
|
||||||
[ProducesResponseType(Status404NotFound)]
|
[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
|
return (ret is not null) switch
|
||||||
{
|
{
|
||||||
true => Ok(ret),
|
true => Ok(ret),
|
||||||
@ -43,52 +47,145 @@ public class NotificationConnectorController(PgsqlContext context) : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new Notification-Connector
|
/// Creates a new REST-Notification-Connector
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks>
|
||||||
/// <param name="notificationConnector">Notification-Connector</param>
|
/// <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]
|
[HttpPut]
|
||||||
[ProducesResponseType<NotificationConnector[]>(Status200OK)]
|
[ProducesResponseType<string>(Status201Created, "application/json")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType(Status409Conflict)]
|
||||||
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector)
|
public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector)
|
||||||
{
|
{
|
||||||
|
if (context.NotificationConnectors.Find(notificationConnector.Name) is not null)
|
||||||
|
return Conflict();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
context.NotificationConnectors.Add(notificationConnector);
|
context.NotificationConnectors.Add(notificationConnector);
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
return Created();
|
return Created(notificationConnector.Name, notificationConnector);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Log.Error(e);
|
||||||
return StatusCode(500, e.Message);
|
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>
|
/// <summary>
|
||||||
/// Deletes the Notification-Connector with the requested ID
|
/// Deletes the Notification-Connector with the requested ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Notification-Connector-ID</param>
|
/// <param name="NotificationConnectorId">Notification-Connector-ID</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="200"></response>
|
||||||
[HttpDelete("{id}")]
|
/// <response code="404">NotificationConnector with ID not found</response>
|
||||||
|
/// <response code="500">Error during Database Operation</response>
|
||||||
|
[HttpDelete("{NotificationConnectorId}")]
|
||||||
[ProducesResponseType(Status200OK)]
|
[ProducesResponseType(Status200OK)]
|
||||||
[ProducesResponseType(Status404NotFound)]
|
[ProducesResponseType(Status404NotFound)]
|
||||||
[ProducesResponseType(Status500InternalServerError)]
|
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
|
||||||
public IActionResult DeleteConnector(string id)
|
public IActionResult DeleteConnector(string NotificationConnectorId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
NotificationConnector? ret = context.NotificationConnectors.Find(id);
|
NotificationConnector? ret = context.NotificationConnectors.Find(NotificationConnectorId);
|
||||||
switch (ret is not null)
|
if(ret is null)
|
||||||
{
|
return NotFound();
|
||||||
case true:
|
|
||||||
context.Remove(ret);
|
context.Remove(ret);
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
return Ok();
|
return Ok();
|
||||||
case false: return NotFound();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Log.Error(e);
|
||||||
return StatusCode(500, e.Message);
|
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 API.Schema.MangaConnectors;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
|
using log4net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Soenneker.Utils.String.NeedlemanWunsch;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
|
|
||||||
[ApiVersion(2)]
|
[ApiVersion(2)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Produces("application/json")]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
[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>
|
/// <summary>
|
||||||
/// Initiate a search for a Manga on a specific Connector
|
/// Initiate a search for a Manga on a specific Connector
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">Manga-Connector-ID</param>
|
/// <param name="MangaConnectorName"></param>
|
||||||
/// <param name="name">Name/Title of the Manga</param>
|
/// <param name="Query"></param>
|
||||||
/// <returns>Manga</returns>
|
/// <response code="200"></response>
|
||||||
[HttpPost("{id}/{name}")]
|
/// <response code="404">MangaConnector with ID not found</response>
|
||||||
[ProducesResponseType<Manga[]>(Status200OK)]
|
/// <response code="406">MangaConnector with ID is disabled</response>
|
||||||
[ProducesResponseType<ProblemResponse>(Status404NotFound)]
|
/// <response code="500">Error during Database Operation</response>
|
||||||
[ProducesResponseType<ProblemResponse>(Status500InternalServerError)]
|
[HttpGet("{MangaConnectorName}/{Query}")]
|
||||||
public IActionResult SearchManga(string id, string name)
|
[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(context.MangaConnectors.Find(MangaConnectorName) is not { } connector)
|
||||||
if (connector is null)
|
return NotFound();
|
||||||
return NotFound(new ProblemResponse("Connector not found."));
|
else if (connector.Enabled is false)
|
||||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] mangas = connector.GetManga(name);
|
return StatusCode(Status406NotAcceptable);
|
||||||
|
|
||||||
|
Manga[] mangas = connector.SearchManga(Query);
|
||||||
List<Manga> retMangas = new();
|
List<Manga> retMangas = new();
|
||||||
foreach ((Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links, List<MangaAltTitle>? altTitles) in mangas)
|
foreach (Manga manga in mangas)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Manga? add = AddMangaToContext(manga, authors, tags, links, altTitles);
|
if(AddMangaToContext(manga) is { } add)
|
||||||
if(add is not null)
|
|
||||||
retMangas.Add(add);
|
retMangas.Add(add);
|
||||||
}
|
}
|
||||||
catch (DbUpdateException e)
|
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());
|
return Ok(retMangas.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Manga? AddMangaToContext(Manga? manga, List<Author>? authors, List<MangaTag>? tags, List<Link>? links,
|
/// <summary>
|
||||||
List<MangaAltTitle>? altTitles)
|
/// 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;
|
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);
|
|
||||||
}
|
}
|
||||||
|
return manga;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 Asp.Versioning;
|
||||||
|
using log4net;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
|
|
||||||
[ApiVersion(2)]
|
[ApiVersion(2)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Produces("application/json")]
|
|
||||||
[Route("v{v:apiVersion}/[controller]")]
|
[Route("v{v:apiVersion}/[controller]")]
|
||||||
public class SettingsController(PgsqlContext context) : Controller
|
public class SettingsController(PgsqlContext context, ILog Log) : Controller
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all Settings
|
/// Get all Settings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType<JObject>(Status200OK, "application/json")]
|
||||||
public IActionResult GetSettings()
|
public IActionResult GetSettings()
|
||||||
{
|
{
|
||||||
return StatusCode(500, "Not implemented"); //TODO
|
return Ok(JObject.Parse(TrangaSettings.Serialize()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the current UserAgent used by Tranga
|
/// Get the current UserAgent used by Tranga
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>UserAgent as string</returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet("UserAgent")]
|
[HttpGet("UserAgent")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType<string>(Status200OK, "text/plain")]
|
||||||
public IActionResult GetUserAgent()
|
public IActionResult GetUserAgent()
|
||||||
{
|
{
|
||||||
return StatusCode(500, "Not implemented"); //TODO
|
return Ok(TrangaSettings.userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set a new UserAgent
|
/// Set a new UserAgent
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="200"></response>
|
||||||
[HttpPatch("UserAgent")]
|
[HttpPatch("UserAgent")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public IActionResult SetUserAgent()
|
public IActionResult SetUserAgent([FromBody]string userAgent)
|
||||||
{
|
{
|
||||||
return StatusCode(500, "Not implemented"); //TODO
|
TrangaSettings.UpdateUserAgent(userAgent);
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reset the UserAgent to default
|
/// Reset the UserAgent to default
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="200"></response>
|
||||||
[HttpDelete("UserAgent")]
|
[HttpDelete("UserAgent")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public IActionResult ResetUserAgent()
|
public IActionResult ResetUserAgent()
|
||||||
{
|
{
|
||||||
return StatusCode(500, "Not implemented"); //TODO
|
TrangaSettings.UpdateUserAgent(TrangaSettings.DefaultUserAgent);
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get all Request-Limits
|
/// Get all Request-Limits
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <response code="200"></response>
|
||||||
[HttpGet("RequestLimits")]
|
[HttpGet("RequestLimits")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType<Dictionary<RequestType,int>>(Status200OK, "application/json")]
|
||||||
public IActionResult GetRequestLimits()
|
public IActionResult GetRequestLimits()
|
||||||
{
|
{
|
||||||
return StatusCode(500, "Not implemented"); //TODO
|
return Ok(TrangaSettings.requestLimits);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update all Request-Limits to new values
|
/// Update all Request-Limits to new values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Nothing</returns>
|
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
|
||||||
[HttpPatch("RequestLimits")]
|
[HttpPatch("RequestLimits")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType(Status501NotImplemented)]
|
||||||
public IActionResult SetRequestLimits()
|
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>
|
/// <summary>
|
||||||
/// Reset all Request-Limits
|
/// Reset Request-Limit
|
||||||
/// </summary>
|
/// </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")]
|
[HttpDelete("RequestLimits")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType<string>(Status200OK)]
|
||||||
public IActionResult ResetRequestLimits()
|
public IActionResult ResetRequestLimits()
|
||||||
{
|
{
|
||||||
return StatusCode(500, "Not implemented"); //TODO
|
TrangaSettings.ResetRequestLimits();
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns Level of Image-Compression for Images
|
/// Returns Level of Image-Compression for Images
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <response code="200">JPEG compression-level as Integer</response>
|
||||||
[HttpGet("ImageCompression")]
|
[HttpGet("ImageCompression")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType<int>(Status200OK, "text/plain")]
|
||||||
public IActionResult GetImageCompression()
|
public IActionResult GetImageCompression()
|
||||||
{
|
{
|
||||||
return StatusCode(500, "Not implemented"); //TODO
|
return Ok(TrangaSettings.compression);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the Image-Compression-Level for Images
|
/// Set the Image-Compression-Level for Images
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="percentage">100 to disable, 0-99 for JPEG compression-Level</param>
|
/// <param name="level">100 to disable, 0-99 for JPEG compression-Level</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="200"></response>
|
||||||
|
/// <response code="400">Level outside permitted range</response>
|
||||||
[HttpPatch("ImageCompression")]
|
[HttpPatch("ImageCompression")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public IActionResult SetImageCompression(int percentage)
|
[ProducesResponseType(Status400BadRequest)]
|
||||||
|
public IActionResult SetImageCompression([FromBody]int level)
|
||||||
{
|
{
|
||||||
return StatusCode(500, "Not implemented"); //TODO
|
if (level < 1 || level > 100)
|
||||||
|
return BadRequest();
|
||||||
|
TrangaSettings.UpdateCompressImages(level);
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get state of Black/White-Image setting
|
/// Get state of Black/White-Image setting
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>True if enabled</returns>
|
/// <response code="200">True if enabled</response>
|
||||||
[HttpGet("BWImages")]
|
[HttpGet("BWImages")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType<bool>(Status200OK, "text/plain")]
|
||||||
public IActionResult GetBwImagesToggle()
|
public IActionResult GetBwImagesToggle()
|
||||||
{
|
{
|
||||||
return StatusCode(500, "Not implemented"); //TODO
|
return Ok(TrangaSettings.bwImages);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enable/Disable conversion of Images to Black and White
|
/// Enable/Disable conversion of Images to Black and White
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="enabled">true to enable</param>
|
/// <param name="enabled">true to enable</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="200"></response>
|
||||||
[HttpPatch("BWImages")]
|
[HttpPatch("BWImages")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public IActionResult SetBwImagesToggle(bool enabled)
|
public IActionResult SetBwImagesToggle([FromBody]bool enabled)
|
||||||
{
|
{
|
||||||
return StatusCode(500, "Not implemented"); //TODO
|
TrangaSettings.UpdateBwImages(enabled);
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get state of April Fools Mode
|
/// Get state of April Fools Mode
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
|
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
|
||||||
/// <returns>True if enabled</returns>
|
/// <response code="200">True if enabled</response>
|
||||||
[HttpGet("AprilFoolsMode")]
|
[HttpGet("AprilFoolsMode")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType<bool>(Status200OK, "text/plain")]
|
||||||
public IActionResult GetAprilFoolsMode()
|
public IActionResult GetAprilFoolsMode()
|
||||||
{
|
{
|
||||||
return StatusCode(500, "Not implemented"); //TODO
|
return Ok(TrangaSettings.aprilFoolsMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -151,11 +194,101 @@ public class SettingsController(PgsqlContext context) : Controller
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
|
/// <remarks>April Fools Mode disables all downloads on April 1st</remarks>
|
||||||
/// <param name="enabled">true to enable</param>
|
/// <param name="enabled">true to enable</param>
|
||||||
/// <returns>Nothing</returns>
|
/// <response code="200"></response>
|
||||||
[HttpPatch("AprilFoolsMode")]
|
[HttpPatch("AprilFoolsMode")]
|
||||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
[ProducesResponseType(Status200OK)]
|
||||||
public IActionResult SetAprilFoolsMode(bool enabled)
|
public IActionResult SetAprilFoolsMode([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;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
using log4net;
|
||||||
using PuppeteerSharp;
|
using PuppeteerSharp;
|
||||||
|
|
||||||
namespace API.MangaDownloadClients;
|
namespace API.MangaDownloadClients;
|
||||||
@ -9,10 +10,11 @@ namespace API.MangaDownloadClients;
|
|||||||
internal class ChromiumDownloadClient : DownloadClient
|
internal class ChromiumDownloadClient : DownloadClient
|
||||||
{
|
{
|
||||||
private static IBrowser? _browser;
|
private static IBrowser? _browser;
|
||||||
private const int StartTimeoutMs = 10000;
|
|
||||||
private readonly HttpDownloadClient _httpDownloadClient;
|
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
|
return await Puppeteer.LaunchAsync(new LaunchOptions
|
||||||
{
|
{
|
||||||
@ -22,44 +24,37 @@ internal class ChromiumDownloadClient : DownloadClient
|
|||||||
"--disable-dev-shm-usage",
|
"--disable-dev-shm-usage",
|
||||||
"--disable-setuid-sandbox",
|
"--disable-setuid-sandbox",
|
||||||
"--no-sandbox"},
|
"--no-sandbox"},
|
||||||
Timeout = StartTimeoutMs
|
Timeout = 30000
|
||||||
}, new LoggerFactory([new LogProvider()])); //TODO
|
}, new LoggerFactory([new Provider(log)]));
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChromiumDownloadClient()
|
public ChromiumDownloadClient()
|
||||||
{
|
{
|
||||||
_httpDownloadClient = new();
|
_httpDownloadClient = new();
|
||||||
if(_browser is null)
|
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)(\?.*)?");
|
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)
|
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||||
{
|
{
|
||||||
|
Log.Debug($"Requesting {url}");
|
||||||
return _imageUrlRex.IsMatch(url)
|
return _imageUrlRex.IsMatch(url)
|
||||||
? _httpDownloadClient.MakeRequestInternal(url, referrer)
|
? _httpDownloadClient.MakeRequestInternal(url, referrer)
|
||||||
: MakeRequestBrowser(url, referrer, clickButton);
|
: MakeRequestBrowser(url, referrer, clickButton);
|
||||||
@ -67,16 +62,23 @@ internal class ChromiumDownloadClient : DownloadClient
|
|||||||
|
|
||||||
private RequestResult MakeRequestBrowser(string url, string? referrer = null, string? clickButton = null)
|
private RequestResult MakeRequestBrowser(string url, string? referrer = null, string? clickButton = null)
|
||||||
{
|
{
|
||||||
|
if (_browser is null)
|
||||||
|
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||||
IPage page = _browser.NewPageAsync().Result;
|
IPage page = _browser.NewPageAsync().Result;
|
||||||
page.DefaultTimeout = 10000;
|
_openPages.Add(new(page, DateTime.Now));
|
||||||
|
page.SetExtraHttpHeadersAsync(new() { { "Referer", referrer } });
|
||||||
|
page.DefaultTimeout = 30000;
|
||||||
IResponse response;
|
IResponse response;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
response = page.GoToAsync(url, WaitUntilNavigation.Networkidle0).Result;
|
response = page.GoToAsync(url, WaitUntilNavigation.Networkidle0).Result;
|
||||||
|
Log.Debug($"Page loaded. {url}");
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Log.Info($"Could not load Page {url}\n{e.Message}");
|
||||||
page.CloseAsync();
|
page.CloseAsync();
|
||||||
|
_openPages.Remove(_openPages.Find(i => i.Key == page));
|
||||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,11 +102,50 @@ internal class ChromiumDownloadClient : DownloadClient
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
page.CloseAsync();
|
page.CloseAsync().Wait();
|
||||||
|
_openPages.Remove(_openPages.Find(i => i.Key == page));
|
||||||
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
|
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, "");
|
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 System.Net;
|
||||||
using API.Schema;
|
using log4net;
|
||||||
|
|
||||||
namespace API.MangaDownloadClients;
|
namespace API.MangaDownloadClients;
|
||||||
|
|
||||||
internal abstract class DownloadClient
|
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()
|
protected DownloadClient()
|
||||||
{
|
{
|
||||||
this._lastExecutedRateLimit = new();
|
this.Log = LogManager.GetLogger(GetType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
|
public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
|
||||||
{
|
{
|
||||||
|
Log.Debug($"Requesting {requestType} {url}");
|
||||||
if (!TrangaSettings.requestLimits.ContainsKey(requestType))
|
if (!TrangaSettings.requestLimits.ContainsKey(requestType))
|
||||||
{
|
{
|
||||||
return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
|
return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
|
||||||
@ -24,17 +26,20 @@ internal abstract class DownloadClient
|
|||||||
: TrangaSettings.requestLimits[requestType];
|
: TrangaSettings.requestLimits[requestType];
|
||||||
|
|
||||||
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
|
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
|
||||||
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(timeBetweenRequests));
|
DateTime now = DateTime.Now;
|
||||||
|
LastExecutedRateLimit.TryAdd(requestType, now.Subtract(timeBetweenRequests));
|
||||||
TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType]));
|
|
||||||
|
|
||||||
|
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)
|
if (rateLimitTimeout > TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
Thread.Sleep(rateLimitTimeout);
|
Thread.Sleep(rateLimitTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
|
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
|
||||||
_lastExecutedRateLimit[requestType] = DateTime.Now;
|
LastExecutedRateLimit[requestType] = DateTime.UtcNow;
|
||||||
|
Log.Debug($"Result {url}: {result}");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using API.Schema;
|
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
namespace API.MangaDownloadClients;
|
namespace API.MangaDownloadClients;
|
||||||
@ -18,16 +17,15 @@ internal class HttpDownloadClient : DownloadClient
|
|||||||
|
|
||||||
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
|
||||||
{
|
{
|
||||||
//TODO
|
if (clickButton is not null)
|
||||||
//if (clickButton is not null)
|
Log.Warn("Can not click button on static site.");
|
||||||
//Log("Can not click button on static site.");
|
|
||||||
HttpResponseMessage? response = null;
|
HttpResponseMessage? response = null;
|
||||||
while (response is null)
|
while (response is null)
|
||||||
{
|
{
|
||||||
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
|
||||||
if (referrer is not null)
|
if (referrer is not null)
|
||||||
requestMessage.Headers.Referrer = new Uri(referrer);
|
requestMessage.Headers.Referrer = new Uri(referrer);
|
||||||
//Log($"Requesting {requestType} {url}");
|
Log.Debug($"Requesting {url}");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
response = Client.Send(requestMessage);
|
response = Client.Send(requestMessage);
|
||||||
@ -48,8 +46,17 @@ internal class HttpDownloadClient : DownloadClient
|
|||||||
{
|
{
|
||||||
return new RequestResult(response.StatusCode, null, Stream.Null);
|
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;
|
HtmlDocument? document = null;
|
||||||
|
|
||||||
|
@ -24,4 +24,10 @@ public struct RequestResult
|
|||||||
this.hasBeenRedirected = hasBeenRedirected;
|
this.hasBeenRedirected = hasBeenRedirected;
|
||||||
redirectedToUrl = redirectedTo;
|
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 />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using API.Schema;
|
using API.Schema.Contexts;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace API.Migrations
|
namespace API.Migrations.pgsql
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PgsqlContext))]
|
[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
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "9.0.0")
|
.HasAnnotation("ProductVersion", "9.0.3")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@ -30,7 +33,8 @@ namespace API.Migrations
|
|||||||
|
|
||||||
b.Property<string>("AuthorName")
|
b.Property<string>("AuthorName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
b.HasKey("AuthorId");
|
b.HasKey("AuthorId");
|
||||||
|
|
||||||
@ -43,29 +47,34 @@ namespace API.Migrations
|
|||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.Property<string>("ArchiveFileName")
|
b.Property<string>("ChapterNumber")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("character varying(10)");
|
||||||
b.Property<float>("ChapterNumber")
|
|
||||||
.HasColumnType("real");
|
|
||||||
|
|
||||||
b.Property<bool>("Downloaded")
|
b.Property<bool>("Downloaded")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b.Property<string>("ParentMangaId")
|
b.Property<string>("ParentMangaId")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.HasColumnType("text");
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b.Property<string>("Url")
|
b.Property<string>("Url")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
b.Property<float?>("VolumeNumber")
|
b.Property<int?>("VolumeNumber")
|
||||||
.HasColumnType("real");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.HasKey("ChapterId");
|
b.HasKey("ChapterId");
|
||||||
|
|
||||||
@ -80,12 +89,8 @@ namespace API.Migrations
|
|||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("DependsOnJobsIds")
|
b.Property<bool>("Enabled")
|
||||||
.HasMaxLength(64)
|
.HasColumnType("boolean");
|
||||||
.HasColumnType("text[]");
|
|
||||||
|
|
||||||
b.Property<string>("JobId1")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<byte>("JobType")
|
b.Property<byte>("JobType")
|
||||||
.HasColumnType("smallint");
|
.HasColumnType("smallint");
|
||||||
@ -100,13 +105,11 @@ namespace API.Migrations
|
|||||||
b.Property<decimal>("RecurrenceMs")
|
b.Property<decimal>("RecurrenceMs")
|
||||||
.HasColumnType("numeric(20,0)");
|
.HasColumnType("numeric(20,0)");
|
||||||
|
|
||||||
b.Property<int>("state")
|
b.Property<byte>("state")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("smallint");
|
||||||
|
|
||||||
b.HasKey("JobId");
|
b.HasKey("JobId");
|
||||||
|
|
||||||
b.HasIndex("JobId1");
|
|
||||||
|
|
||||||
b.HasIndex("ParentJobId");
|
b.HasIndex("ParentJobId");
|
||||||
|
|
||||||
b.ToTable("Jobs");
|
b.ToTable("Jobs");
|
||||||
@ -116,54 +119,25 @@ namespace API.Migrations
|
|||||||
b.UseTphMappingStrategy();
|
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)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.Property<string>("Auth")
|
b.Property<string>("BasePath")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b.Property<string>("BaseUrl")
|
b.Property<string>("LibraryName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
b.Property<byte>("LibraryType")
|
b.HasKey("LocalLibraryId");
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("LibraryConnectorId");
|
b.ToTable("LocalLibraries");
|
||||||
|
|
||||||
b.ToTable("LibraryConnectors");
|
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("LibraryType");
|
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Link", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("LinkId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("LinkProvider")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("LinkUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.HasKey("LinkId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("Link");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Manga", b =>
|
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||||
@ -172,80 +146,68 @@ namespace API.Migrations
|
|||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.Property<string>("ConnectorId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("CoverFileNameInCache")
|
b.Property<string>("CoverFileNameInCache")
|
||||||
.HasColumnType("text");
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
b.Property<string>("CoverUrl")
|
b.Property<string>("CoverUrl")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("FolderName")
|
b.Property<string>("DirectoryName")
|
||||||
.IsRequired()
|
.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");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b.Property<string>("MangaConnectorId")
|
b.Property<string>("LibraryId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("MangaConnectorName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)");
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
b.Property<string>("OriginalLanguage")
|
b.Property<string>("OriginalLanguage")
|
||||||
.HasColumnType("text");
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("character varying(8)");
|
||||||
|
|
||||||
b.Property<byte>("ReleaseStatus")
|
b.Property<byte>("ReleaseStatus")
|
||||||
.HasColumnType("smallint");
|
.HasColumnType("smallint");
|
||||||
|
|
||||||
b.Property<string>("WebsiteUrl")
|
b.Property<string>("WebsiteUrl")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
b.Property<long>("Year")
|
b.Property<long>("Year")
|
||||||
.HasColumnType("bigint");
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
b.HasKey("MangaId");
|
b.HasKey("MangaId");
|
||||||
|
|
||||||
b.HasIndex("MangaConnectorId");
|
b.HasIndex("LibraryId");
|
||||||
|
|
||||||
b.ToTable("Manga");
|
b.HasIndex("MangaConnectorName");
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
b.ToTable("Mangas");
|
||||||
{
|
|
||||||
b.Property<string>("AltTitleId")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Language")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(8)
|
|
||||||
.HasColumnType("character varying(8)");
|
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("AltTitleId");
|
|
||||||
|
|
||||||
b.HasIndex("MangaId");
|
|
||||||
|
|
||||||
b.ToTable("AltTitles");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b =>
|
||||||
@ -256,10 +218,20 @@ namespace API.Migrations
|
|||||||
|
|
||||||
b.PrimitiveCollection<string[]>("BaseUris")
|
b.PrimitiveCollection<string[]>("BaseUris")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
.HasColumnType("text[]");
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("IconUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
b.PrimitiveCollection<string[]>("SupportedLanguages")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
|
.HasMaxLength(8)
|
||||||
.HasColumnType("text[]");
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
b.HasKey("Name");
|
b.HasKey("Name");
|
||||||
@ -274,87 +246,60 @@ namespace API.Migrations
|
|||||||
modelBuilder.Entity("API.Schema.MangaTag", b =>
|
modelBuilder.Entity("API.Schema.MangaTag", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Tag")
|
b.Property<string>("Tag")
|
||||||
.HasColumnType("text");
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.HasKey("Tag");
|
b.HasKey("Tag");
|
||||||
|
|
||||||
b.ToTable("Tags");
|
b.ToTable("Tags");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Notification", b =>
|
modelBuilder.Entity("AuthorToManga", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("NotificationId")
|
b.Property<string>("AuthorIds")
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.Property<DateTime>("Date")
|
b.Property<string>("MangaIds")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.Property<string>("Message")
|
b.HasKey("AuthorIds", "MangaIds");
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.HasIndex("MangaIds");
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<byte>("Urgency")
|
b.ToTable("AuthorToManga");
|
||||||
.HasColumnType("smallint");
|
|
||||||
|
|
||||||
b.HasKey("NotificationId");
|
|
||||||
|
|
||||||
b.ToTable("Notifications");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b =>
|
modelBuilder.Entity("JobJob", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("NotificationConnectorId")
|
b.Property<string>("DependsOnJobsJobId")
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.Property<byte>("NotificationConnectorType")
|
b.Property<string>("JobId")
|
||||||
.HasColumnType("smallint");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.HasKey("NotificationConnectorId");
|
b.HasKey("DependsOnJobsJobId", "JobId");
|
||||||
|
|
||||||
b.ToTable("NotificationConnectors");
|
b.HasIndex("JobId");
|
||||||
|
|
||||||
b.HasDiscriminator<byte>("NotificationConnectorType");
|
b.ToTable("JobJob");
|
||||||
|
|
||||||
b.UseTphMappingStrategy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
modelBuilder.Entity("MangaTagToManga", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("AuthorsAuthorId")
|
b.Property<string>("MangaTagIds")
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
b.Property<string>("MangaId")
|
b.Property<string>("MangaIds")
|
||||||
.HasColumnType("character varying(64)");
|
.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 =>
|
modelBuilder.Entity("API.Schema.Jobs.DownloadAvailableChaptersJob", b =>
|
||||||
{
|
|
||||||
b.Property<string>("MangaId")
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<string>("TagsTag")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("MangaId", "TagsTag");
|
|
||||||
|
|
||||||
b.HasIndex("TagsTag");
|
|
||||||
|
|
||||||
b.ToTable("MangaMangaTag");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b =>
|
|
||||||
{
|
{
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
b.HasBaseType("API.Schema.Jobs.Job");
|
||||||
|
|
||||||
@ -365,9 +310,29 @@ namespace API.Migrations
|
|||||||
|
|
||||||
b.HasIndex("MangaId");
|
b.HasIndex("MangaId");
|
||||||
|
|
||||||
|
b.ToTable("Jobs", t =>
|
||||||
|
{
|
||||||
|
t.Property("MangaId")
|
||||||
|
.HasColumnName("DownloadAvailableChaptersJob_MangaId");
|
||||||
|
});
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
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 =>
|
modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b =>
|
||||||
{
|
{
|
||||||
b.HasBaseType("API.Schema.Jobs.Job");
|
b.HasBaseType("API.Schema.Jobs.Job");
|
||||||
@ -388,16 +353,70 @@ namespace API.Migrations
|
|||||||
|
|
||||||
b.Property<string>("FromLocation")
|
b.Property<string>("FromLocation")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b.Property<string>("ToLocation")
|
b.Property<string>("ToLocation")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)3);
|
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");
|
b.HasBaseType("API.Schema.Jobs.Job");
|
||||||
|
|
||||||
@ -411,38 +430,17 @@ namespace API.Migrations
|
|||||||
b.ToTable("Jobs", t =>
|
b.ToTable("Jobs", t =>
|
||||||
{
|
{
|
||||||
t.Property("MangaId")
|
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 =>
|
modelBuilder.Entity("API.Schema.MangaConnectors.Global", b =>
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)1);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue((byte)0);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b =>
|
|
||||||
{
|
{
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("AsuraToon");
|
b.HasDiscriminator().HasValue("Global");
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b =>
|
|
||||||
{
|
|
||||||
b.HasBaseType("API.Schema.MangaConnectors.MangaConnector");
|
|
||||||
|
|
||||||
b.HasDiscriminator().HasValue("Bato");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b =>
|
||||||
@ -452,117 +450,10 @@ namespace API.Migrations
|
|||||||
b.HasDiscriminator().HasValue("MangaDex");
|
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 =>
|
modelBuilder.Entity("API.Schema.Chapter", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("API.Schema.Manga", "ParentManga")
|
b.HasOne("API.Schema.Manga", "ParentManga")
|
||||||
.WithMany()
|
.WithMany("Chapters")
|
||||||
.HasForeignKey("ParentMangaId")
|
.HasForeignKey("ParentMangaId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
@ -572,73 +463,153 @@ namespace API.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("API.Schema.Jobs.Job", b =>
|
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")
|
b.HasOne("API.Schema.Jobs.Job", "ParentJob")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("ParentJobId");
|
.HasForeignKey("ParentJobId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
b.Navigation("ParentJob");
|
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 =>
|
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")
|
b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("MangaConnectorId")
|
.HasForeignKey("MangaConnectorName")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.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");
|
b.Navigation("MangaConnector");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("API.Schema.MangaAltTitle", b =>
|
modelBuilder.Entity("AuthorToManga", b =>
|
||||||
{
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
|
||||||
.WithMany("AltTitles")
|
|
||||||
.HasForeignKey("MangaId");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("AuthorManga", b =>
|
|
||||||
{
|
{
|
||||||
b.HasOne("API.Schema.Author", null)
|
b.HasOne("API.Schema.Author", null)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("AuthorsAuthorId")
|
.HasForeignKey("AuthorIds")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("API.Schema.Manga", null)
|
b.HasOne("API.Schema.Manga", null)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("MangaId")
|
.HasForeignKey("MangaIds")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.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)
|
b.HasOne("API.Schema.Manga", null)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("MangaId")
|
.HasForeignKey("MangaIds")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("API.Schema.MangaTag", null)
|
b.HasOne("API.Schema.MangaTag", null)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("TagsTag")
|
.HasForeignKey("MangaTagIds")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.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")
|
b.HasOne("API.Schema.Manga", "Manga")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
@ -660,7 +631,26 @@ namespace API.Migrations
|
|||||||
b.Navigation("Chapter");
|
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")
|
b.HasOne("API.Schema.Manga", "Manga")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
@ -671,16 +661,20 @@ namespace API.Migrations
|
|||||||
b.Navigation("Manga");
|
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 =>
|
modelBuilder.Entity("API.Schema.Manga", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("AltTitles");
|
b.Navigation("Chapters");
|
||||||
|
|
||||||
b.Navigation("Links");
|
|
||||||
});
|
});
|
||||||
#pragma warning restore 612, 618
|
#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);
|
|
124
API/Program.cs
124
API/Program.cs
@ -1,13 +1,15 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using API;
|
using API;
|
||||||
using API.Schema;
|
using API.Schema;
|
||||||
|
using API.Schema.Contexts;
|
||||||
using API.Schema.Jobs;
|
using API.Schema.Jobs;
|
||||||
using API.Schema.MangaConnectors;
|
using API.Schema.MangaConnectors;
|
||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using Asp.Versioning.Builder;
|
using Asp.Versioning.Builder;
|
||||||
using Asp.Versioning.Conventions;
|
using Asp.Versioning.Conventions;
|
||||||
|
using log4net;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@ -25,25 +27,27 @@ builder.Services.AddCors(options =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddApiVersioning(option =>
|
builder.Services.AddApiVersioning(option =>
|
||||||
{
|
{
|
||||||
option.AssumeDefaultVersionWhenUnspecified = true;
|
option.AssumeDefaultVersionWhenUnspecified = true;
|
||||||
option.DefaultApiVersion = new ApiVersion(2);
|
option.DefaultApiVersion = new ApiVersion(2);
|
||||||
option.ReportApiVersions = true;
|
option.ReportApiVersions = true;
|
||||||
option.ApiVersionReader = ApiVersionReader.Combine(
|
option.ApiVersionReader = ApiVersionReader.Combine(
|
||||||
new UrlSegmentApiVersionReader(),
|
new UrlSegmentApiVersionReader(),
|
||||||
new QueryStringApiVersionReader("api-version"),
|
new QueryStringApiVersionReader("api-version"),
|
||||||
new HeaderApiVersionReader("X-Version"),
|
new HeaderApiVersionReader("X-Version"),
|
||||||
new MediaTypeApiVersionReader("x-version"));
|
new MediaTypeApiVersionReader("x-version"));
|
||||||
})
|
})
|
||||||
.AddMvc(options =>
|
.AddMvc(options =>
|
||||||
{
|
{
|
||||||
options.Conventions.Add(new VersionByNamespaceConvention());
|
options.Conventions.Add(new VersionByNamespaceConvention());
|
||||||
})
|
})
|
||||||
.AddApiExplorer(options => {
|
.AddApiExplorer(options =>
|
||||||
options.GroupNameFormat = "'v'V";
|
{
|
||||||
options.SubstituteApiVersionInUrl = true;
|
options.GroupNameFormat = "'v'V";
|
||||||
});
|
options.SubstituteApiVersionInUrl = true;
|
||||||
|
});
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGenNewtonsoftSupport();
|
||||||
builder.Services.AddSwaggerGen(opt =>
|
builder.Services.AddSwaggerGen(opt =>
|
||||||
{
|
{
|
||||||
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||||
@ -51,16 +55,28 @@ builder.Services.AddSwaggerGen(opt =>
|
|||||||
});
|
});
|
||||||
builder.Services.ConfigureOptions<NamedSwaggerGenOptions>();
|
builder.Services.ConfigureOptions<NamedSwaggerGenOptions>();
|
||||||
|
|
||||||
builder.Services.AddDbContext<PgsqlContext>(options =>
|
string ConnectionString = $"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "localhost:5432"}; " +
|
||||||
options.UseNpgsql($"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST")??"localhost:5432"}; " +
|
$"Database={Environment.GetEnvironmentVariable("POSTGRES_DB") ?? "postgres"}; " +
|
||||||
$"Database={Environment.GetEnvironmentVariable("POSTGRES_DB")??"postgres"}; " +
|
$"Username={Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "postgres"}; " +
|
||||||
$"Username={Environment.GetEnvironmentVariable("POSTGRES_USER")??"postgres"}; " +
|
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "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 =>
|
builder.Services.AddControllers().AddNewtonsoftJson(opts =>
|
||||||
{
|
{
|
||||||
opts.SerializerSettings.Converters.Add(new StringEnumConverter());
|
opts.SerializerSettings.Converters.Add(new StringEnumConverter());
|
||||||
|
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
||||||
});
|
});
|
||||||
|
builder.Services.AddScoped<ILog>(opts => LogManager.GetLogger("API"));
|
||||||
|
|
||||||
builder.WebHost.UseUrls("http://*:6531");
|
builder.WebHost.UseUrls("http://*:6531");
|
||||||
|
|
||||||
@ -87,34 +103,42 @@ app.UseSwaggerUI(options =>
|
|||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
app.UseMiddleware<RequestTimeMiddleware>();
|
||||||
{
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<PgsqlContext>();
|
|
||||||
db.Database.Migrate();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 =
|
MangaConnector[] connectors =
|
||||||
[
|
[
|
||||||
new AsuraToon(),
|
new MangaDex(),
|
||||||
new Bato(),
|
new ComickIo(),
|
||||||
new MangaDex(),
|
new Global(scope.ServiceProvider.GetService<PgsqlContext>()!)
|
||||||
new MangaHere(),
|
];
|
||||||
new MangaKatana(),
|
|
||||||
new MangaLife(),
|
|
||||||
new Manganato(),
|
|
||||||
new Mangasee(),
|
|
||||||
new Mangaworld(),
|
|
||||||
new ManhuaPlus(),
|
|
||||||
new Weebcentral()
|
|
||||||
];
|
|
||||||
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
|
MangaConnector[] newConnectors = connectors.Where(c => !context.MangaConnectors.Contains(c)).ToArray();
|
||||||
context.MangaConnectors.AddRange(newConnectors);
|
context.MangaConnectors.AddRange(newConnectors);
|
||||||
|
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));
|
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=)"};
|
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));
|
context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High));
|
||||||
@ -125,8 +149,14 @@ using (var scope = app.Services.CreateScope())
|
|||||||
|
|
||||||
TrangaSettings.Load();
|
TrangaSettings.Load();
|
||||||
Tranga.StartLogger();
|
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");
|
app.UseCors("AllowAll");
|
||||||
|
|
||||||
|
@ -6,7 +6,15 @@ namespace API.Schema;
|
|||||||
[PrimaryKey("AuthorId")]
|
[PrimaryKey("AuthorId")]
|
||||||
public class Author(string authorName)
|
public class Author(string authorName)
|
||||||
{
|
{
|
||||||
[MaxLength(64)]
|
[StringLength(64)]
|
||||||
public string AuthorId { get; init; } = TokenGen.CreateToken(typeof(Author), 64);
|
[Required]
|
||||||
|
public string AuthorId { get; init; } = TokenGen.CreateToken(typeof(Author), authorName);
|
||||||
|
[StringLength(128)]
|
||||||
|
[Required]
|
||||||
public string AuthorName { get; init; } = authorName;
|
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;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using API.Schema.Jobs;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace API.Schema;
|
namespace API.Schema;
|
||||||
|
|
||||||
[PrimaryKey("ChapterId")]
|
[PrimaryKey("ChapterId")]
|
||||||
public class Chapter : IComparable<Chapter>
|
public class Chapter : IComparable<Chapter>
|
||||||
{
|
{
|
||||||
[MaxLength(64)]
|
[StringLength(64)] [Required] public string ChapterId { get; init; }
|
||||||
public string ChapterId { get; init; } = TokenGen.CreateToken(typeof(Chapter), 64);
|
|
||||||
|
[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 int? VolumeNumber { get; private set; }
|
||||||
public ChapterNumber ChapterNumber { get; private set; }
|
[StringLength(10)] [Required] public string 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; }
|
|
||||||
|
|
||||||
public Chapter(Manga parentManga, string url, ChapterNumber chapterNumber, int? volumeNumber = null, string? title = null)
|
[StringLength(2048)] [Required] [Url] public string Url { get; internal set; }
|
||||||
: this(parentManga.MangaId, url, chapterNumber, volumeNumber, title)
|
|
||||||
|
[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;
|
this.ParentManga = parentManga;
|
||||||
}
|
this.VolumeNumber = volumeNumber;
|
||||||
|
this.ChapterNumber = chapterNumber;
|
||||||
public Chapter(string parentMangaId, string url, ChapterNumber chapterNumber,
|
|
||||||
int? volumeNumber = null, string? title = null)
|
|
||||||
{
|
|
||||||
this.ParentMangaId = parentMangaId;
|
|
||||||
this.Url = url;
|
this.Url = url;
|
||||||
this.ChapterNumber = chapterNumber;
|
|
||||||
this.VolumeNumber = volumeNumber;
|
|
||||||
this.Title = title;
|
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>
|
/// <summary>
|
||||||
/// Creates full file path of chapter-archive
|
/// EF ONLY!!!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Filepath</returns>
|
internal Chapter(string chapterId, string parentMangaId, int? volumeNumber, string chapterNumber, string url, string? idOnConnectorSite, string? title, string fileName, bool downloaded)
|
||||||
internal string GetArchiveFilePath()
|
|
||||||
{
|
{
|
||||||
return Path.Join(TrangaSettings.downloadLocation, ParentManga.FolderName, ArchiveFileName);
|
this.ChapterId = chapterId;
|
||||||
}
|
this.IdOnConnectorSite = idOnConnectorSite;
|
||||||
|
this.ParentMangaId = parentMangaId;
|
||||||
public bool IsDownloaded()
|
this.VolumeNumber = volumeNumber;
|
||||||
{
|
this.ChapterNumber = chapterNumber;
|
||||||
string path = GetArchiveFilePath();
|
this.Url = url;
|
||||||
return File.Exists(path);
|
this.Title = title;
|
||||||
|
this.FileName = fileName;
|
||||||
|
this.Downloaded = downloaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CompareTo(Chapter? other)
|
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}");
|
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,
|
||||||
>0 => 1,
|
> 0 => 1,
|
||||||
_ => this.ChapterNumber.CompareTo(otherChapter.ChapterNumber)
|
_ => 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()
|
internal string GetComicInfoXmlString()
|
||||||
{
|
{
|
||||||
XElement comicInfo = new XElement("ComicInfo",
|
XElement comicInfo = new("ComicInfo",
|
||||||
new XElement("Tags", string.Join(',', ParentManga.Tags.Select(tag => tag.Tag))),
|
new XElement("Number", ChapterNumber)
|
||||||
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)
|
|
||||||
);
|
);
|
||||||
|
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();
|
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.ComponentModel.DataAnnotations;
|
||||||
using System.IO.Compression;
|
using API.Schema.Contexts;
|
||||||
using System.Runtime.InteropServices;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using API.MangaDownloadClients;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using API.Schema.MangaConnectors;
|
using Newtonsoft.Json;
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
|
||||||
using SixLabors.ImageSharp.Processing;
|
|
||||||
using SixLabors.ImageSharp.Processing.Processors.Binarization;
|
|
||||||
using static System.IO.UnixFileMode;
|
|
||||||
|
|
||||||
namespace API.Schema.Jobs;
|
namespace API.Schema.Jobs;
|
||||||
|
|
||||||
public class DownloadMangaCoverJob(string chapterId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
public class DownloadMangaCoverJob : Job
|
||||||
: Job(TokenGen.CreateToken(typeof(DownloadMangaCoverJob), 64), JobType.DownloadMangaCoverJob, 0, parentJobId, dependsOnJobsIds)
|
|
||||||
{
|
{
|
||||||
[MaxLength(64)]
|
[StringLength(64)] [Required] public string MangaId { get; init; }
|
||||||
public string ChapterId { get; init; } = chapterId;
|
|
||||||
public Chapter? Chapter { 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)
|
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||||
{
|
{
|
||||||
MangaConnector connector = Chapter.ParentManga?.MangaConnector ?? context.MangaConnectors.Find(context.Manga.Find(Chapter.ParentMangaId)?.MangaId)!;
|
try
|
||||||
DownloadChapterImages(Chapter, connector);
|
{
|
||||||
|
Manga.CoverFileNameInCache = Manga.MangaConnector.SaveCoverImageToCache(Manga);
|
||||||
|
context.SaveChanges();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException e)
|
||||||
|
{
|
||||||
|
Log.Error(e);
|
||||||
|
}
|
||||||
return [];
|
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.IO.Compression;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using API.MangaDownloadClients;
|
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;
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
@ -11,77 +14,127 @@ using static System.IO.UnixFileMode;
|
|||||||
|
|
||||||
namespace API.Schema.Jobs;
|
namespace API.Schema.Jobs;
|
||||||
|
|
||||||
public class DownloadSingleChapterJob(string chapterId, string? parentJobId = null, ICollection<string>? dependsOnJobsIds = null)
|
public class DownloadSingleChapterJob : Job
|
||||||
: Job(TokenGen.CreateToken(typeof(DownloadSingleChapterJob), 64), JobType.DownloadSingleChapterJob, 0, parentJobId, dependsOnJobsIds)
|
|
||||||
{
|
{
|
||||||
[MaxLength(64)]
|
[StringLength(64)] [Required] public string ChapterId { get; init; }
|
||||||
public string ChapterId { get; init; } = chapterId;
|
|
||||||
public Chapter? Chapter { 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)
|
protected override IEnumerable<Job> RunInternal(PgsqlContext context)
|
||||||
{
|
{
|
||||||
Chapter c = Chapter ?? context.Chapters.Find(ChapterId)!;
|
string[] imageUrls = Chapter.ParentManga.MangaConnector.GetChapterImageUrls(Chapter);
|
||||||
Manga m = c.ParentManga ?? context.Manga.Find(c.ParentMangaId)!;
|
if (imageUrls.Length < 1)
|
||||||
MangaConnector connector = m.MangaConnector ?? context.MangaConnectors.Find(m.MangaConnectorId)!;
|
{
|
||||||
DownloadChapterImages(c, connector, m);
|
Log.Info($"No imageUrls for chapter {ChapterId}");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
context.Entry(Chapter.ParentManga).Reference<LocalLibrary>(m => m.Library).Load(); //Need to explicitly load, because we are not accessing navigation directly...
|
||||||
private bool DownloadChapterImages(Chapter chapter, MangaConnector connector, Manga manga)
|
string saveArchiveFilePath = Chapter.FullArchiveFilePath;
|
||||||
{
|
Log.Debug($"Chapter path: {saveArchiveFilePath}");
|
||||||
string[] imageUrls = connector.GetChapterImageUrls(chapter);
|
|
||||||
string saveArchiveFilePath = chapter.GetArchiveFilePath();
|
|
||||||
|
|
||||||
//Check if Publication Directory already exists
|
//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))
|
if (!Directory.Exists(directoryPath))
|
||||||
|
{
|
||||||
|
Log.Info($"Creating publication Directory: {directoryPath}");
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
Directory.CreateDirectory(directoryPath,
|
Directory.CreateDirectory(directoryPath,
|
||||||
UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute );
|
UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute );
|
||||||
else
|
else
|
||||||
Directory.CreateDirectory(directoryPath);
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (File.Exists(saveArchiveFilePath)) //Don't download twice. Redownload
|
if (File.Exists(saveArchiveFilePath)) //Don't download twice. Redownload
|
||||||
|
{
|
||||||
|
Log.Info($"Archive {saveArchiveFilePath} already existed, but deleting and re-downloading.");
|
||||||
File.Delete(saveArchiveFilePath);
|
File.Delete(saveArchiveFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
//Create a temporary folder to store images
|
//Create a temporary folder to store images
|
||||||
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
|
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
|
||||||
|
Log.Debug($"Created temp folder: {tempFolder}");
|
||||||
|
|
||||||
|
Log.Info($"Downloading images: {ChapterId}");
|
||||||
int chapterNum = 0;
|
int chapterNum = 0;
|
||||||
//Download all Images to temporary Folder
|
//Download all Images to temporary Folder
|
||||||
if (imageUrls.Length == 0)
|
|
||||||
{
|
|
||||||
Directory.Delete(tempFolder, true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (string imageUrl in imageUrls)
|
foreach (string imageUrl in imageUrls)
|
||||||
{
|
{
|
||||||
string extension = imageUrl.Split('.')[^1].Split('?')[0];
|
string extension = imageUrl.Split('.')[^1].Split('?')[0];
|
||||||
string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
|
string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
|
||||||
bool status = DownloadImage(imageUrl, imagePath);
|
bool status = DownloadImage(imageUrl, imagePath);
|
||||||
if (status is false)
|
if (status is false)
|
||||||
return false;
|
{
|
||||||
|
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
|
//ZIP-it and ship-it
|
||||||
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
|
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
|
||||||
Directory.Delete(tempFolder, true); //Cleanup
|
Directory.Delete(tempFolder, true); //Cleanup
|
||||||
|
|
||||||
return true;
|
Chapter.Downloaded = true;
|
||||||
|
context.SaveChanges();
|
||||||
|
|
||||||
|
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)
|
private void ProcessImage(string imagePath)
|
||||||
{
|
{
|
||||||
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
|
if (!TrangaSettings.bwImages && TrangaSettings.compression == 100)
|
||||||
|
{
|
||||||
|
Log.Debug($"No processing requested for image");
|
||||||
return;
|
return;
|
||||||
DateTime start = DateTime.Now;
|
}
|
||||||
|
|
||||||
|
Log.Debug($"Processing image: {imagePath}");
|
||||||
|
|
||||||
using Image image = Image.Load(imagePath);
|
using Image image = Image.Load(imagePath);
|
||||||
File.Delete(imagePath);
|
File.Delete(imagePath);
|
||||||
if(TrangaSettings.bwImages)
|
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
|
//Check if Publication already has a Folder and cover
|
||||||
string publicationFolder = manga.CreatePublicationFolder();
|
string publicationFolder = manga.CreatePublicationFolder();
|
||||||
DirectoryInfo dirInfo = new (publicationFolder);
|
DirectoryInfo dirInfo = new (publicationFolder);
|
||||||
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
|
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
|
||||||
{
|
{
|
||||||
|
Log.Debug($"Cover already exists at {publicationFolder}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string? fileInCache = manga.CoverFileNameInCache;
|
Log.Info($"Copying cover to {publicationFolder}");
|
||||||
if (fileInCache is null || !File.Exists(fileInCache))
|
string? fileInCache = manga.CoverFileNameInCache ?? manga.MangaConnector.SaveCoverImageToCache(manga);
|
||||||
|
if (fileInCache is null)
|
||||||
{
|
{
|
||||||
if (retries > 0)
|
Log.Error($"File {fileInCache} does not exist");
|
||||||
{
|
|
||||||
manga.SaveCoverImageToCache();
|
|
||||||
CopyCoverFromCacheToDownloadLocation(manga, --retries);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
|
||||||
File.Copy(fileInCache, newFilePath, true);
|
File.Copy(fileInCache, newFilePath, true);
|
||||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
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)
|
private bool DownloadImage(string imageUrl, string savePath)
|
||||||
@ -129,7 +181,7 @@ public class DownloadSingleChapterJob(string chapterId, string? parentJobId = nu
|
|||||||
if (requestResult.result == Stream.Null)
|
if (requestResult.result == Stream.Null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
FileStream fs = new (savePath, FileMode.Create);
|
FileStream fs = new (savePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
requestResult.result.CopyTo(fs);
|
requestResult.result.CopyTo(fs);
|
||||||
fs.Close();
|
fs.Close();
|
||||||
ProcessImage(savePath);
|
ProcessImage(savePath);
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using API.Schema.Contexts;
|
||||||
|
using log4net;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace API.Schema.Jobs;
|
namespace API.Schema.Jobs;
|
||||||
@ -8,48 +11,126 @@ namespace API.Schema.Jobs;
|
|||||||
[PrimaryKey("JobId")]
|
[PrimaryKey("JobId")]
|
||||||
public abstract class Job
|
public abstract class Job
|
||||||
{
|
{
|
||||||
[MaxLength(64)]
|
[StringLength(64)]
|
||||||
|
[Required]
|
||||||
public string JobId { get; init; }
|
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)
|
[StringLength(64)] public string? ParentJobId { get; private set; }
|
||||||
: this(jobId, jobType, recurrenceMs, parentJob?.JobId, dependsOnJobs?.Select(j => j.JobId).ToList())
|
[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.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;
|
this.LazyLoader = lazyLoader;
|
||||||
ParentJobId = parentJobId;
|
this.JobId = jobId;
|
||||||
DependsOnJobsIds = dependsOnJobsIds;
|
this.JobType = jobType;
|
||||||
JobType = jobType;
|
this.RecurrenceMs = recurrenceMs;
|
||||||
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;
|
Log.Info($"Running job {JobId}");
|
||||||
IEnumerable<Job> newJobs = RunInternal(context);
|
DateTime jobStart = DateTime.UtcNow;
|
||||||
this.state = JobState.Completed;
|
Job[]? ret = null;
|
||||||
return newJobs;
|
|
||||||
|
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);
|
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;
|
namespace API.Schema.Jobs;
|
||||||
|
|
||||||
public enum JobState
|
public enum JobState : byte
|
||||||
{
|
{
|
||||||
Waiting,
|
//Values 0-63 Preparation Stages
|
||||||
Running,
|
FirstExecution = 0,
|
||||||
Completed
|
//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
|
public enum JobType : byte
|
||||||
{
|
{
|
||||||
DownloadSingleChapterJob = 0,
|
DownloadSingleChapterJob = 0,
|
||||||
DownloadNewChaptersJob = 1,
|
DownloadAvailableChaptersJob = 1,
|
||||||
UpdateMetaDataJob = 2,
|
|
||||||
MoveFileOrFolderJob = 3,
|
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)
|
namespace API.Schema.Jobs;
|
||||||
: Job(TokenGen.CreateToken(typeof(MoveFileOrFolderJob), 64), JobType.MoveFileOrFolderJob, 0, parentJobId, dependsOnJobsIds)
|
|
||||||
|
public class MoveFileOrFolderJob : Job
|
||||||
{
|
{
|
||||||
public string FromLocation { get; init; } = fromLocation;
|
[StringLength(256)]
|
||||||
public string ToLocation { get; init; } = toLocation;
|
[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)
|
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 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()
|
protected override void UpdateLibraryInternal()
|
||||||
{
|
{
|
||||||
foreach (KavitaLibrary lib in GetLibraries())
|
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()
|
internal override bool Test()
|
||||||
{
|
{
|
||||||
foreach (KavitaLibrary lib in GetLibraries())
|
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 true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -70,15 +70,17 @@ public class Kavita : LibraryConnector
|
|||||||
/// <returns>Array of KavitaLibrary</returns>
|
/// <returns>Array of KavitaLibrary</returns>
|
||||||
private IEnumerable<KavitaLibrary> GetLibraries()
|
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)
|
if (data == Stream.Null)
|
||||||
{
|
{
|
||||||
return Array.Empty<KavitaLibrary>();
|
Log.Info("No libraries found");
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
||||||
if (result is null)
|
if (result is null)
|
||||||
{
|
{
|
||||||
return Array.Empty<KavitaLibrary>();
|
Log.Info("No libraries found");
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
List<KavitaLibrary> ret = new();
|
List<KavitaLibrary> ret = new();
|
||||||
|
@ -5,7 +5,7 @@ namespace API.Schema.LibraryConnectors;
|
|||||||
|
|
||||||
public class Komga : LibraryConnector
|
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)
|
baseUrl, auth)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -38,12 +38,14 @@ public class Komga : LibraryConnector
|
|||||||
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/v1/libraries", "Basic", Auth);
|
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/v1/libraries", "Basic", Auth);
|
||||||
if (data == Stream.Null)
|
if (data == Stream.Null)
|
||||||
{
|
{
|
||||||
return Array.Empty<KomgaLibrary>();
|
Log.Info("No libraries found");
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
|
||||||
if (result is null)
|
if (result is null)
|
||||||
{
|
{
|
||||||
return Array.Empty<KomgaLibrary>();
|
Log.Info("No libraries found");
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
HashSet<KomgaLibrary> ret = new();
|
HashSet<KomgaLibrary> ret = new();
|
||||||
|
@ -1,18 +1,32 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using log4net;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace API.Schema.LibraryConnectors;
|
namespace API.Schema.LibraryConnectors;
|
||||||
|
|
||||||
[PrimaryKey("LibraryConnectorId")]
|
[PrimaryKey("LibraryConnectorId")]
|
||||||
public abstract class LibraryConnector(string libraryConnectorId, LibraryType libraryType, string baseUrl, string auth)
|
public abstract class LibraryConnector(string libraryConnectorId, LibraryType libraryType, string baseUrl, string auth)
|
||||||
{
|
{
|
||||||
[MaxLength(64)]
|
[StringLength(64)]
|
||||||
|
[Required]
|
||||||
public string LibraryConnectorId { get; } = libraryConnectorId;
|
public string LibraryConnectorId { get; } = libraryConnectorId;
|
||||||
|
|
||||||
|
[Required]
|
||||||
public LibraryType LibraryType { get; init; } = libraryType;
|
public LibraryType LibraryType { get; init; } = libraryType;
|
||||||
|
[StringLength(256)]
|
||||||
|
[Required]
|
||||||
|
[Url]
|
||||||
public string BaseUrl { get; init; } = baseUrl;
|
public string BaseUrl { get; init; } = baseUrl;
|
||||||
|
[StringLength(256)]
|
||||||
|
[Required]
|
||||||
public string Auth { get; init; } = auth;
|
public string Auth { get; init; } = auth;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[NotMapped]
|
||||||
|
protected ILog Log { get; init; } = LogManager.GetLogger($"{libraryType.ToString()} {baseUrl}");
|
||||||
|
|
||||||
protected abstract void UpdateLibraryInternal();
|
protected abstract void UpdateLibraryInternal();
|
||||||
internal abstract bool Test();
|
internal abstract bool Test();
|
||||||
}
|
}
|
@ -1,48 +1,52 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
using log4net;
|
||||||
|
|
||||||
namespace API.Schema.LibraryConnectors;
|
namespace API.Schema.LibraryConnectors;
|
||||||
|
|
||||||
public class NetClient
|
public class NetClient
|
||||||
{
|
{
|
||||||
|
private static ILog Log = LogManager.GetLogger(typeof(NetClient));
|
||||||
|
|
||||||
public static Stream MakeRequest(string url, string authScheme, string auth)
|
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();
|
Method = HttpMethod.Get,
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, auth);
|
RequestUri = new Uri(url)
|
||||||
|
};
|
||||||
HttpRequestMessage requestMessage = new ()
|
try
|
||||||
{
|
{
|
||||||
Method = HttpMethod.Get,
|
HttpResponseMessage response = client.Send(requestMessage);
|
||||||
RequestUri = new Uri(url)
|
|
||||||
};
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
HttpResponseMessage response = client.Send(requestMessage);
|
if (response.StatusCode is HttpStatusCode.Unauthorized &&
|
||||||
|
response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
||||||
if (response.StatusCode is HttpStatusCode.Unauthorized &&
|
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth);
|
||||||
response.RequestMessage!.RequestUri!.AbsoluteUri != url)
|
else if (response.IsSuccessStatusCode)
|
||||||
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth);
|
return response.Content.ReadAsStream();
|
||||||
else if (response.IsSuccessStatusCode)
|
else
|
||||||
return response.Content.ReadAsStream();
|
|
||||||
else
|
|
||||||
return Stream.Null;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
switch (e)
|
|
||||||
{
|
|
||||||
case HttpRequestException:
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
return Stream.Null;
|
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()
|
HttpClient client = new()
|
||||||
{
|
{
|
||||||
|
@ -6,15 +6,19 @@ namespace API.Schema;
|
|||||||
[PrimaryKey("LinkId")]
|
[PrimaryKey("LinkId")]
|
||||||
public class Link(string linkProvider, string linkUrl)
|
public class Link(string linkProvider, string linkUrl)
|
||||||
{
|
{
|
||||||
[MaxLength(64)]
|
[StringLength(64)]
|
||||||
public string LinkId { get; init; } = TokenGen.CreateToken(typeof(Link), 64);
|
[Required]
|
||||||
|
public string LinkId { get; init; } = TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl);
|
||||||
|
[StringLength(64)]
|
||||||
|
[Required]
|
||||||
public string LinkProvider { get; init; } = linkProvider;
|
public string LinkProvider { get; init; } = linkProvider;
|
||||||
|
[StringLength(2048)]
|
||||||
|
[Required]
|
||||||
|
[Url]
|
||||||
public string LinkUrl { get; init; } = linkUrl;
|
public string LinkUrl { get; init; } = linkUrl;
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
public override string ToString()
|
||||||
{
|
{
|
||||||
if (obj is not Link other)
|
return $"{LinkId} {LinkProvider} {LinkUrl}";
|
||||||
return false;
|
|
||||||
return other.LinkProvider == LinkProvider && other.LinkUrl == 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.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text;
|
||||||
using API.MangaDownloadClients;
|
|
||||||
using API.Schema.Jobs;
|
|
||||||
using API.Schema.MangaConnectors;
|
using API.Schema.MangaConnectors;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using static System.IO.UnixFileMode;
|
using static System.IO.UnixFileMode;
|
||||||
|
|
||||||
namespace API.Schema;
|
namespace API.Schema;
|
||||||
@ -13,118 +13,149 @@ namespace API.Schema;
|
|||||||
[PrimaryKey("MangaId")]
|
[PrimaryKey("MangaId")]
|
||||||
public class Manga
|
public class Manga
|
||||||
{
|
{
|
||||||
[MaxLength(64)]
|
[StringLength(64)]
|
||||||
public string MangaId { get; init; } = TokenGen.CreateToken(typeof(Manga), 64);
|
[Required]
|
||||||
[MaxLength(64)]
|
public string MangaId { get; init; }
|
||||||
public string ConnectorId { 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 ICollection<Author> Authors { get; internal set; }= null!;
|
||||||
public string Description { get; internal set; }
|
public ICollection<MangaTag> MangaTags { get; internal set; }= null!;
|
||||||
public string WebsiteUrl { get; internal set; }
|
public ICollection<Link> Links { get; internal set; }= null!;
|
||||||
public string CoverUrl { get; internal set; }
|
public ICollection<MangaAltTitle> AltTitles { get; internal set; } = null!;
|
||||||
public string? CoverFileNameInCache { get; internal set; }
|
[Required] public float IgnoreChaptersBefore { get; internal set; }
|
||||||
public uint Year { get; internal set; }
|
[StringLength(1024)] [Required] public string DirectoryName { get; private 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 string MangaConnectorId { get; private set; }
|
[JsonIgnore] [StringLength(512)] public string? CoverFileNameInCache { get; internal set; } = null;
|
||||||
public MangaConnector? MangaConnector { get; private set; }
|
public uint? Year { get; internal init; }
|
||||||
|
[StringLength(8)] public string? OriginalLanguage { get; internal init; }
|
||||||
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; }
|
|
||||||
|
|
||||||
public Manga(string connectorId, string name, string description, string websiteUrl, string coverUrl,
|
[JsonIgnore]
|
||||||
string? coverFileNameInCache, uint year, string? originalLanguage, MangaReleaseStatus releaseStatus,
|
[NotMapped]
|
||||||
float ignoreChapterBefore, MangaConnector mangaConnector, ICollection<Author> authors,
|
public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
|
||||||
ICollection<MangaTag> tags, ICollection<Link> links, ICollection<MangaAltTitle> altTitles)
|
|
||||||
: this(connectorId, name, description, websiteUrl, coverUrl, coverFileNameInCache, year, originalLanguage,
|
[NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.ChapterId).ToList();
|
||||||
releaseStatus, ignoreChapterBefore, mangaConnector.Name)
|
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.Authors = authors;
|
||||||
this.Tags = tags;
|
this.MangaTags = mangaTags;
|
||||||
this.Links = links;
|
this.Links = links;
|
||||||
this.AltTitles = altTitles;
|
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()
|
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))
|
if(!Directory.Exists(publicationFolder))
|
||||||
Directory.CreateDirectory(publicationFolder);
|
Directory.CreateDirectory(publicationFolder);
|
||||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
File.SetUnixFileMode(publicationFolder, GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute | UserRead | UserWrite | UserExecute);
|
File.SetUnixFileMode(publicationFolder, GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute | UserRead | UserWrite | UserExecute);
|
||||||
return publicationFolder;
|
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;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace API.Schema;
|
namespace API.Schema;
|
||||||
@ -7,9 +6,18 @@ namespace API.Schema;
|
|||||||
[PrimaryKey("AltTitleId")]
|
[PrimaryKey("AltTitleId")]
|
||||||
public class MangaAltTitle(string language, string title)
|
public class MangaAltTitle(string language, string title)
|
||||||
{
|
{
|
||||||
[MaxLength(64)]
|
[StringLength(64)]
|
||||||
public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle", 64);
|
[Required]
|
||||||
[MaxLength(8)]
|
public string AltTitleId { get; init; } = TokenGen.CreateToken("AltTitle");
|
||||||
|
[StringLength(8)]
|
||||||
|
[Required]
|
||||||
public string Language { get; init; } = language;
|
public string Language { get; init; } = language;
|
||||||
|
[StringLength(256)]
|
||||||
|
[Required]
|
||||||
public string Title { get; set; } = title;
|
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;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using API.MangaDownloadClients;
|
using API.MangaDownloadClients;
|
||||||
|
using log4net;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace API.Schema.MangaConnectors;
|
namespace API.Schema.MangaConnectors;
|
||||||
|
|
||||||
[PrimaryKey("Name")]
|
[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]
|
[JsonIgnore]
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
internal DownloadClient downloadClient { get; init; } = null!;
|
internal DownloadClient downloadClient { get; init; } = null!;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[NotMapped]
|
||||||
|
protected ILog Log { get; init; } = LogManager.GetLogger(name);
|
||||||
|
|
||||||
public Chapter[] GetNewChapters(Manga manga)
|
[StringLength(32)]
|
||||||
{
|
[Required]
|
||||||
Chapter[] allChapters = GetChapters(manga);
|
public string Name { get; init; } = name;
|
||||||
if (allChapters.Length < 1)
|
[StringLength(8)]
|
||||||
return [];
|
[Required]
|
||||||
|
public string[] SupportedLanguages { get; init; } = supportedLanguages;
|
||||||
return allChapters.Where(chapter => !chapter.IsDownloaded()).ToArray();
|
[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);
|
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.RegularExpressions;
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using API.MangaDownloadClients;
|
using API.MangaDownloadClients;
|
||||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace API.Schema.MangaConnectors;
|
namespace API.Schema.MangaConnectors;
|
||||||
|
|
||||||
@ -11,271 +9,327 @@ public class MangaDex : MangaConnector
|
|||||||
//https://api.mangadex.org/docs/3-enumerations/#language-codes--localization
|
//https://api.mangadex.org/docs/3-enumerations/#language-codes--localization
|
||||||
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
|
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
|
||||||
//https://gist.github.com/Josantonius/b455e315bc7f790d14b136d61d9ae469
|
//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();
|
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
|
Log.Info($"Searching Manga: {mangaSearchName}");
|
||||||
int offset = 0; //"Page"
|
List<Manga> mangas = new ();
|
||||||
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();
|
|
||||||
|
|
||||||
//Request all search-results
|
int offset = 0;
|
||||||
while (offset < total) //As long as we haven't requested all "Pages"
|
int total = int.MaxValue;
|
||||||
|
while(offset < total)
|
||||||
{
|
{
|
||||||
//Request next Page
|
string requestUrl =
|
||||||
RequestResult requestResult = downloadClient.MakeRequest(
|
$"https://api.mangadex.org/manga?limit={Limit}&offset={offset}&title={mangaSearchName}" +
|
||||||
$"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=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'";
|
||||||
$"&contentRating%5B%5D=pornographic" +
|
offset += Limit;
|
||||||
$"&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;
|
|
||||||
|
|
||||||
if (result.ContainsKey("data"))
|
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
|
||||||
results.AddRange(result["data"]!.AsArray()!);//Manga-data-Array
|
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)
|
Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
|
||||||
{
|
return mangas.ToArray();
|
||||||
if(MangaFromJsonObject(mangaNode.AsObject()) is { } manga)
|
|
||||||
retManga.Add(manga); //Add Publication (Manga) to result
|
|
||||||
}
|
|
||||||
return retManga.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 =
|
Log.Info($"Getting Manga: {url}");
|
||||||
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 (!UrlMatchesConnector(url))
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
{
|
||||||
|
Log.Debug($"Url is not for Connector. {url}");
|
||||||
return null;
|
return null;
|
||||||
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
|
}
|
||||||
if(result is not null)
|
|
||||||
return MangaFromJsonObject(result["data"]!.AsObject());
|
Match match = GetMangaIdFromUrl.Match(url);
|
||||||
return null;
|
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);
|
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))
|
Log.Info($"Getting Manga: {mangaIdOnSite}");
|
||||||
return null;
|
string requestUrl =
|
||||||
string publicationId = idNode!.GetValue<string>();
|
$"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'";
|
||||||
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>()
|
|
||||||
};
|
|
||||||
|
|
||||||
Dictionary<string, string> altTitlesDict = new();
|
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
|
||||||
if (attributes.TryGetPropertyValue("altTitles", out JsonNode? altTitlesNode))
|
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||||
{
|
{
|
||||||
foreach (JsonNode? altTitleNode in altTitlesNode!.AsArray())
|
Log.Error("Request failed");
|
||||||
{
|
|
||||||
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))
|
|
||||||
return null;
|
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
|
using StreamReader sr = new (result.result);
|
||||||
{
|
JObject jObject = JObject.Parse(sr.ReadToEnd());
|
||||||
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();
|
|
||||||
|
|
||||||
Manga pub = new (publicationId, sortName, description, $"https://mangadex.org/title/{publicationId}", coverUrl, null, year,
|
if (jObject.Value<string>("result") != "ok")
|
||||||
originalLanguage, releaseStatus, -1,
|
{
|
||||||
this,
|
JArray? errors = jObject["errors"] as JArray;
|
||||||
authors,
|
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
|
||||||
mangaTags,
|
return null;
|
||||||
links,
|
}
|
||||||
altTitles);
|
|
||||||
|
JObject? data = jObject["data"] as JObject;
|
||||||
return (pub, authors, mangaTags, links, altTitles);
|
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
|
Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
|
||||||
int offset = 0; //"Page"
|
List<Chapter> chapters = new ();
|
||||||
int total = int.MaxValue; //How many total results are there, is updated on first request
|
|
||||||
List<Chapter> chapters = new();
|
int offset = 0;
|
||||||
//As long as we haven't requested all "Pages"
|
int total = int.MaxValue;
|
||||||
while (offset < total)
|
while(offset < total)
|
||||||
{
|
{
|
||||||
//Request next "Page"
|
string requestUrl =
|
||||||
RequestResult requestResult =
|
$"https://api.mangadex.org/manga/{manga.IdOnConnectorSite}/feed?limit={Limit}&offset={offset}&" +
|
||||||
downloadClient.MakeRequest(
|
$"translatedLanguage%5B%5D={language}&" +
|
||||||
$"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);
|
$"contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&includeFutureUpdates=0&includes%5B%5D=";
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
offset += Limit;
|
||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Return Chapters ordered by Chapter-Number
|
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
|
||||||
return chapters.Order().ToArray();
|
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)
|
internal override string[] GetChapterImageUrls(Chapter chapter)
|
||||||
{//Request URLs for Chapter-Images
|
{
|
||||||
RequestResult requestResult =
|
Log.Info($"Getting Chapter Image-Urls: {chapter.Url}");
|
||||||
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.ChapterId}?forcePort443=false", RequestType.MangaDexImage);
|
if (!UrlMatchesConnector(chapter.Url))
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
|
|
||||||
{
|
{
|
||||||
|
Log.Debug($"Url is not for Connector. {chapter.Url}");
|
||||||
return [];
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
string baseUrl = result["baseUrl"]!.GetValue<string>();
|
|
||||||
string hash = result["chapter"]!["hash"]!.GetValue<string>();
|
string id = match.Groups[1].Value;
|
||||||
JsonArray imageFileNames = result["chapter"]!["data"]!.AsArray();
|
string requestUrl = $"https://api.mangadex.org/at-home/server/{id}";
|
||||||
//Loop through all imageNames and construct urls (imageUrl)
|
|
||||||
List<string> imageUrls = new();
|
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
||||||
foreach (JsonNode? image in imageFileNames)
|
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
|
||||||
imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}");
|
{
|
||||||
return imageUrls.ToArray();
|
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,215 +0,0 @@
|
|||||||
using System.Data;
|
|
||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
using API.MangaDownloadClients;
|
|
||||||
using HtmlAgilityPack;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Soenneker.Utils.String.NeedlemanWunsch;
|
|
||||||
|
|
||||||
namespace API.Schema.MangaConnectors;
|
|
||||||
|
|
||||||
public class Mangasee : MangaConnector
|
|
||||||
{
|
|
||||||
public Mangasee() : base("Mangasee", ["en"], ["mangasee123.com"])
|
|
||||||
{
|
|
||||||
this.downloadClient = new ChromiumDownloadClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct SearchResult
|
|
||||||
{
|
|
||||||
public string i { get; set; }
|
|
||||||
public string s { get; set; }
|
|
||||||
public string[] a { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)[] GetManga(string publicationTitle = "")
|
|
||||||
{
|
|
||||||
string requestUrl = "https://mangasee123.com/_search.php";
|
|
||||||
RequestResult requestResult =
|
|
||||||
downloadClient.MakeRequest(requestUrl, RequestType.Default);
|
|
||||||
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
SearchResult[] searchResults = JsonConvert.DeserializeObject<SearchResult[]>(requestResult.htmlDocument!.DocumentNode.InnerText) ??
|
|
||||||
throw new NoNullAllowedException();
|
|
||||||
SearchResult[] filteredResults = FilteredResults(publicationTitle, searchResults);
|
|
||||||
|
|
||||||
|
|
||||||
string[] urls = filteredResults.Select(result => $"https://mangasee123.com/manga/{result.i}").ToArray();
|
|
||||||
List<(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)> searchResultManga = new();
|
|
||||||
foreach (string url in urls)
|
|
||||||
{
|
|
||||||
(Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? newManga = GetMangaFromUrl(url);
|
|
||||||
if(newManga is { } manga)
|
|
||||||
searchResultManga.Add(manga);
|
|
||||||
}
|
|
||||||
return searchResultManga.ToArray();
|
|
||||||
}
|
|
||||||
catch (NoNullAllowedException)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly string[] _filterWords = {"a", "the", "of", "as", "to", "no", "for", "on", "with", "be", "and", "in", "wa", "at", "be", "ni"};
|
|
||||||
private string ToFilteredString(string input) => string.Join(' ', input.ToLower().Split(' ').Where(word => _filterWords.Contains(word) == false));
|
|
||||||
private SearchResult[] FilteredResults(string publicationTitle, SearchResult[] unfilteredSearchResults)
|
|
||||||
{
|
|
||||||
Dictionary<SearchResult, int> similarity = new();
|
|
||||||
foreach (SearchResult sr in unfilteredSearchResults)
|
|
||||||
{
|
|
||||||
List<int> scores = new();
|
|
||||||
string filteredPublicationString = ToFilteredString(publicationTitle);
|
|
||||||
string filteredSString = ToFilteredString(sr.s);
|
|
||||||
scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(filteredSString, filteredPublicationString));
|
|
||||||
foreach (string srA in sr.a)
|
|
||||||
{
|
|
||||||
string filteredAString = ToFilteredString(srA);
|
|
||||||
scores.Add(NeedlemanWunschStringUtil.CalculateSimilarity(filteredAString, filteredPublicationString));
|
|
||||||
}
|
|
||||||
similarity.Add(sr, scores.Sum() / scores.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<SearchResult> ret = similarity.OrderBy(s => s.Value).Take(10).Select(s => s.Key).ToList();
|
|
||||||
return ret.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromId(string publicationId)
|
|
||||||
{
|
|
||||||
return GetMangaFromUrl($"https://mangasee123.com/manga/{publicationId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override (Manga, List<Author>?, List<MangaTag>?, List<Link>?, List<MangaAltTitle>?)? GetMangaFromUrl(string url)
|
|
||||||
{
|
|
||||||
Regex publicationIdRex = new(@"https:\/\/mangasee123.com\/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)
|
|
||||||
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("//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")
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{manga.MangaId}.xml");
|
|
||||||
XElement[] chapterItems = doc.Descendants("item").ToArray();
|
|
||||||
List<Chapter> chapters = new();
|
|
||||||
Regex chVolRex = new(@".*chapter-([0-9\.]+)(?:-index-([0-9\.]+))?.*");
|
|
||||||
foreach (XElement chapter in chapterItems)
|
|
||||||
{
|
|
||||||
string url = chapter.Descendants("link").First().Value;
|
|
||||||
Match m = chVolRex.Match(url);
|
|
||||||
int? volumeNumber = m.Groups[2].Success ? int.Parse(m.Groups[2].Value) : null;
|
|
||||||
if(!ChapterNumber.CanParse(m.Groups[1].Value))
|
|
||||||
continue;
|
|
||||||
ChapterNumber chapterNumber = new(m.Groups[1].Value);
|
|
||||||
|
|
||||||
string chapterUrl = Regex.Replace(url, @"-page-[0-9]+(\.html)", ".html");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
chapters.Add(new Chapter(manga, chapterUrl,chapterNumber, volumeNumber, null));
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Return Chapters ordered by Chapter-Number
|
|
||||||
return chapters.Order().ToArray();
|
|
||||||
}
|
|
||||||
catch (HttpRequestException e)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,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;
|
namespace API.Schema;
|
||||||
|
|
||||||
[PrimaryKey("Tag")]
|
[PrimaryKey("Tag")]
|
||||||
public class MangaTag(string tag)
|
public class MangaTag(string tag)
|
||||||
{
|
{
|
||||||
|
[StringLength(64)]
|
||||||
|
[Required]
|
||||||
public string Tag { get; init; } = tag;
|
public string Tag { get; init; } = tag;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Tag}";
|
||||||
|
}
|
||||||
}
|
}
|
@ -4,18 +4,49 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
namespace API.Schema;
|
namespace API.Schema;
|
||||||
|
|
||||||
[PrimaryKey("NotificationId")]
|
[PrimaryKey("NotificationId")]
|
||||||
public class Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
|
public class Notification
|
||||||
{
|
{
|
||||||
[MaxLength(64)]
|
[StringLength(64)]
|
||||||
public string NotificationId { get; init; } = TokenGen.CreateToken("Notification", 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;
|
[Required]
|
||||||
|
public DateTime Date { get; init; }
|
||||||
public Notification() : this("") { }
|
|
||||||
|
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;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text;
|
||||||
|
using log4net;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace API.Schema.NotificationConnectors;
|
namespace API.Schema.NotificationConnectors;
|
||||||
|
|
||||||
[PrimaryKey("NotificationConnectorId")]
|
[PrimaryKey("Name")]
|
||||||
public abstract class NotificationConnector(string notificationConnectorId, NotificationConnectorType notificationConnectorType)
|
public class NotificationConnector(string name, string url, Dictionary<string, string> headers, string httpMethod, string body)
|
||||||
{
|
{
|
||||||
[MaxLength(64)]
|
[StringLength(64)]
|
||||||
public string NotificationConnectorId { get; } = notificationConnectorId;
|
[Required]
|
||||||
public NotificationConnectorType NotificationConnectorType { get; init; } = notificationConnectorType;
|
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]
|
[JsonIgnore]
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
protected readonly HttpClient _client = new();
|
protected ILog Log = LogManager.GetLogger(name);
|
||||||
|
|
||||||
public abstract void SendNotification(string title, string notificationText);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user