2
0

Compare commits

..

169 Commits

Author SHA1 Message Date
483dcc41df Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-11-02 17:52:26 +01:00
55cc2a2e84
Merge pull request #277 from C9Glax/asuratoon
Asuratoon
2024-11-02 17:51:12 +01:00
b619109ea1 fix #141 chapternames 2024-11-02 17:48:18 +01:00
72943330c3 Merge branch 'refs/heads/cuttingedge' into asuratoon 2024-11-02 17:45:13 +01:00
bc44a5333b Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-11-02 17:44:38 +01:00
38bc1e4d53 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-11-02 17:44:30 +01:00
47479f7a0d Fix chaptermarkers.
Don't create one if Chapter does not have an ID
2024-11-02 17:44:23 +01:00
b2381be860 #141 fix ParsePublicationsFromHtml, statusNode, titleNode, firstChapterNode
fix ParseChaptersFromHtml nodeCollection of ChapterURls
fix ParseImageUrlsFromHtml xPath
fix Chapterparsing names
2024-11-02 17:42:26 +01:00
657e1b338b resolves #141 Asuratoon connector 2024-11-02 17:19:17 +01:00
5018800d09 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Jobs/JobBoss.cs
2024-11-02 16:25:49 +01:00
ee265a7519 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-11-02 16:24:55 +01:00
5b0624654b rename duplicates to append ".duplicate" 2024-11-02 16:24:44 +01:00
a75549c699 Only try loading .json files on startup (exclude .failed for example) 2024-11-02 16:24:25 +01:00
c7dc5e75f2 Add "Expires" Header to image responses 2024-10-31 23:00:33 +01:00
3f37eefe72 Include modified date in image responses for cachecontrol 2024-10-31 22:53:05 +01:00
b7bc04a045 Add zstd compression to all API Traffic 2024-10-31 22:16:18 +01:00
f7daacf0d4 Use Robidoux algorithm for resizing covers 2024-10-31 21:50:46 +01:00
1cb8899195 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-10-31 20:43:21 +01:00
f46244cb9c Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-10-31 20:43:11 +01:00
9db3f1b0da Extend logging on startup 2024-10-31 20:42:56 +01:00
dc9cd4b1dd Append ".failed" to job-files that werent successfully added. 2024-10-31 20:41:46 +01:00
3566ad774d Moved logging to actually say if we added a job to the list 2024-10-31 20:41:21 +01:00
94b81969c7 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-10-30 22:40:31 +01:00
3e581e2ddb Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Jobs/JobBoss.cs
2024-10-30 22:34:17 +01:00
bd8cb86c52 Always set directory-permissions 2024-10-30 22:29:32 +01:00
34c5436b33 Always set directory-permissions 2024-10-30 22:29:16 +01:00
4690394437 Formatting 2024-10-30 22:27:55 +01:00
02cf8578c9 Explicitly set File/Directory permissions for jobs 2024-10-30 22:27:50 +01:00
067497ddd0 Delete duplicate files on startup. 2024-10-30 20:38:53 +01:00
4b88cdbd90 When updating Jobfiles, dont write a new file if we werent able to successfully delete the old one 2024-10-30 20:31:16 +01:00
420013f07b Delete chapterMarkers if the file doesn't exist anymore. 2024-10-30 18:23:14 +01:00
8cee11aa22 Fix #272 Manhuaplus missing year string 2024-10-29 19:15:19 +01:00
07c6081c03 #236 2024-10-27 03:49:55 +01:00
585d7e3380 Fix order of startup: Load Manga first, the jobs 2024-10-27 03:42:50 +01:00
febce6b92a Downloaded Image processing:
- Compression
- B/W threshold
2024-10-27 03:40:07 +01:00
fb7ed21d82 Update Types doc with last merge for Chapters 2024-10-27 03:39:40 +01:00
2db85e5070 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-10-27 02:09:16 +01:00
198bbdcf94 Set hidden Attribute to Markerfiles 2024-10-27 02:58:50 +02:00
c58adf64fa #271 Create Marker-files for Chapters.
If a Connector provides a unique ID for a chapter, Tranga will create a markerfile, containing the current name of the Chapter
This should prevent duplicates, or missing chapters.
2024-10-27 02:41:28 +02:00
957debea01 Mangahere change list-2 to list-1 in selector 2024-10-27 02:22:58 +02:00
96b5921ed6 GET LibraryTypes Create and Test set url to lowercase
Set Komga to also require username and password
2024-10-23 02:29:08 +02:00
9d47445339 Assign numbers to ProgressToken.State
Update type docs
2024-10-20 20:58:59 +02:00
93696fbac1 docs Types documentation NotificationConnector 2024-10-20 20:43:40 +02:00
582b3af89c Add docs types LibraryConnector 2024-10-20 18:53:03 +02:00
f57667bc8f add documentation types settings 2024-10-20 18:33:19 +02:00
f9a30f2587 Types documentation add quotation marks 2024-10-20 18:32:49 +02:00
240af81fa9 Add doc types chapter 2024-10-20 02:16:22 +02:00
26b2910000 Add GET /v2/Jobs/Standby 2024-10-20 01:30:50 +02:00
a88b85e599 Add numbers to JobTypes (and type documentation) 2024-10-20 01:08:22 +02:00
27f823cfeb GET V2Manga with internalIds return distinct array. 2024-10-20 01:06:24 +02:00
70993a692a Add ReleaseStatus to docs/types.md 2024-10-18 19:31:09 +02:00
1a631362c9 Use Sixlabors.Imagesharp for resizing coverimages. 2024-10-18 19:30:57 +02:00
00c4f0533f Update documentation 2024-10-18 17:57:59 +02:00
8670863810 Add Job and ProgressToken Types to docs 2024-10-18 17:51:53 +02:00
2c9bd2532e Fix order of RequestPaths 2024-10-18 17:51:37 +02:00
575fb739cc typo 2024-10-18 00:48:58 +02:00
d4af068f0e Add BaseUris: string[] field to MangaConnector, to match Connector to uri 2024-10-18 00:48:46 +02:00
6a4d454a08 Extend Types.md documentation 2024-10-18 00:29:29 +02:00
225db8beda Change return type of api request to get Connectors to get connector-list instead of dictionary 2024-10-17 21:03:37 +02:00
d80fcd9039 Manga website url nullable 2024-09-30 23:19:17 +02:00
4871bc801d Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-29 01:07:51 +02:00
1c5f105a4d Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-27 15:04:03 +02:00
26a07f4a2f Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-27 14:58:02 +02:00
48ab44c28d Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-27 14:51:20 +02:00
32ecdcda76 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-23 15:41:02 +02:00
a92eba2d14 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/TrangaSettings.cs
2024-09-22 00:17:30 +02:00
7b6724ad38 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-18 18:47:28 +02:00
be68ddc9b7 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-17 00:52:06 +02:00
96e2845a5b Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-17 00:23:49 +02:00
c36204c7a8 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-16 22:51:57 +02:00
18edcef1c3 Resolve #247
Modify API call:
`/v2/Connector/Types`
Returns: Dictionary with Connector-Names and supported languages.
2024-09-16 21:25:08 +02:00
73ad881600 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-16 21:19:30 +02:00
c6cfd9eb6c Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Server.cs
2024-09-16 21:17:32 +02:00
99df9a9dfd Fix #248
Move contents of old DownloadLocation and WorkingDirectory to new paths. Overwrite existing files, and add from oldPath.
2024-09-16 20:10:38 +02:00
77bb309dfa Fix #248 double closing OutputStream in response 2024-09-16 19:58:26 +02:00
3b9d4a6735 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-11 14:41:49 +02:00
190fa8cba7 Fix #239 multiple enumeration on Export 2024-09-09 09:54:09 +02:00
217700d08d Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	.gitignore
2024-09-09 09:44:36 +02:00
75eea8c761 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-08-26 20:47:19 +02:00
06cdbbd283 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-08-26 20:28:57 +02:00
054c88712e Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-08-26 20:18:51 +02:00
e95eb0497c #229 Resize cover Images if requested 2024-08-26 19:34:29 +02:00
3c3f7bb95a Merge recent changes to TrangaSettings backend 2024-08-26 19:08:59 +02:00
032ee95716 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Jobs/DownloadNewChapters.cs
#	Tranga/Jobs/JobBoss.cs
#	Tranga/Jobs/UpdateMetadata.cs
#	Tranga/Server.cs
#	Tranga/TrangaArgs.cs
#	Tranga/TrangaSettings.cs
2024-08-26 19:04:05 +02:00
fc884adc9f Fix HandleRequest trying to send more than one response 2024-08-10 21:52:14 +02:00
960d3f7c62 Fix Cover location 2024-08-10 21:45:47 +02:00
6520aebcdf Cleanup MangaCache 2024-08-10 21:42:09 +02:00
1ee9b644aa Fix Permissions for manga-directory 2024-08-10 21:37:43 +02:00
2f36701fef Reduce Logspam 2024-08-10 21:25:24 +02:00
b18f8e4059 Fix GET /v2/Job/Types 2024-08-10 21:00:38 +02:00
8145abb744 Fix workign Directory in TrangaArgsMain 2024-08-10 18:53:18 +02:00
9dd52178b9 Update MangaHere bad ManhuaPlus to v2 architecture 2024-08-10 17:34:45 +02:00
cf242f81e1 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Manga.cs
2024-08-10 17:32:56 +02:00
6b9ddca711 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-07-31 17:44:42 +02:00
d73bf70868 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Server.cs
2024-07-11 15:46:35 +02:00
d221532e0d Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-06-29 22:50:14 +02:00
5bc2a8909d Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Server.cs
2024-06-29 19:38:01 +02:00
f3e0959be8
Merge pull request #198 from C9Glax/master
Merge Github Actions
2024-06-29 19:23:37 +02:00
8607bd2c89 #187 NTFY JsonConverter 2024-06-15 21:40:28 +02:00
fab30dc5a7 Documentation
https://github.com/C9Glax/tranga/issues/187
2024-06-15 21:27:24 +02:00
fd20b9febf NTFY use Username and Password
https://github.com/C9Glax/tranga/issues/187
2024-06-15 21:26:23 +02:00
ee6de661c8 Merge branch 'refs/heads/C9Glax-tranga-issue-187' into Server-V2 2024-06-15 21:24:41 +02:00
790e77b00c Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-06-02 01:05:29 +02:00
4f14903538 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-06-02 00:23:29 +02:00
6ae3918679 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-06-02 00:11:33 +02:00
8ccb6c0cb5 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-05-26 23:04:35 +02:00
beb455308f Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-05-26 22:52:05 +02:00
5c309131ad Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Jobs/JobBoss.cs
2024-05-26 18:56:30 +02:00
27a559834f Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Jobs/JobBoss.cs
2024-05-26 18:26:06 +02:00
2cfc7ac2c5 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-04-27 19:09:31 +02:00
017f31ca83 Clean 2024-04-26 16:39:39 +02:00
4021237888 Add Endpoint GET /v2/Manga/Search GlobalSearch
Resolves #124
#167
2024-04-26 00:51:18 +02:00
7ed3846c5f Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-04-26 00:49:08 +02:00
7f95ab9439 Add Endpoint GET /v2/Manga to request multiple Manga from internalIds #167 2024-04-26 00:22:17 +02:00
49a9b7ccb0 Corrected Job->Manga in return 2024-04-26 00:19:38 +02:00
0735e2c588 Change GET /v2/Manga to /v2/Mangas 2024-04-26 00:16:28 +02:00
5b22246c41 Add Endpoint GET /v2/Job returns list of jobs specified by jobid 2024-04-26 00:14:46 +02:00
2e1f633f40 Add Endpoint POST /v2/Manga/internalId/moveFolder #167 2024-04-26 00:05:48 +02:00
8887cea718 Add Endpoint POST /v2/Manga/internalId/ignoreChaptersBelow #167 2024-04-26 00:03:46 +02:00
061da1b4bf Add field customFolder and startChapter to CreateJob Endpoint
https://github.com/C9Glax/tranga/pull/167#issuecomment-2077909075
#167
2024-04-25 23:55:31 +02:00
80dc8fbe65 Resolves #176 Return 409 conflict if job already exists. 2024-04-25 23:50:06 +02:00
28a0efe488 Add Endpoint /v2/Manga/internalId/Chapters/Latest 2024-04-25 23:45:13 +02:00
3d08b1f9f2 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/GlobalBase.cs
#	Tranga/Jobs/JobBoss.cs
#	Tranga/Jobs/UpdateMetadata.cs
#	Tranga/Manga.cs
#	Tranga/MangaConnectors/Bato.cs
#	Tranga/MangaConnectors/MangaKatana.cs
#	Tranga/MangaConnectors/MangaLife.cs
#	Tranga/MangaConnectors/Manganato.cs
#	Tranga/MangaConnectors/Mangasee.cs
#	Tranga/MangaConnectors/Mangaworld.cs
2024-04-25 23:34:56 +02:00
2651a0c53b Implemented /v2/NotificationConnector/* 2024-04-23 00:58:19 +02:00
0ced3a7dd9 Implement /v2/LibraryConnector/* 2024-04-23 00:51:24 +02:00
a56555eee4 Add LibraryConnector.Test to see if requests can be made to endpoint. 2024-04-23 00:48:08 +02:00
cee7870aad Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Server.cs
2024-04-23 00:21:18 +02:00
bce77180bc Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Jobs/UpdateMetadata.cs
#	Tranga/Manga.cs
2024-04-22 23:54:26 +02:00
8c66bbc89f Use publicationCache to store and update Manga 2024-04-22 23:45:51 +02:00
e360037fda Add "(?:/?)" to the end of all Regex RequestPaths 2024-04-22 04:43:08 +02:00
ea866e0136 Added Endpoint /v2/Manga lists all known Manga
Implemented /v2/Manga/*
2024-04-22 04:42:10 +02:00
c3231327f9 nullable 2024-04-22 04:21:39 +02:00
03e90eccd3 No longer require connector name to create job 2024-04-22 04:21:30 +02:00
64482931a3 Implemented GET /v2/LogFile 2024-04-22 03:19:56 +02:00
cce4901a5d Implement all /v2/Settings 2024-04-22 03:03:17 +02:00
3adb103fc4 Fix API-Path prematurely triggering match. 2024-04-22 03:02:49 +02:00
b6ffb97a04 Merge branch 'refs/heads/cuttingedge' into Server-V2 2024-04-22 02:27:34 +02:00
49cfff8a2f Changed the Creation Job API to a single Endpoint /v2/Job/Create/<Type>
Added and implemented GET /v2/Job/Types
Implemented /v2/Job/<jobId>
Implemented /v2/Job/<jobId>/StartNow
Implemented /v2/Job/<jobId>/Cancel
Implemented /v2/Job/<jobId>/SetInterval
2024-04-22 00:00:35 +02:00
6d48a100ca Implemented GET
/v2/Jobs
/v2/Jobs/Running
/v2/Jobs/Waiting
/v2/Jobs/Monitoring
/v2/Job/<jobId>
/v2/Job/<jobId>/Progress
2024-04-21 21:48:24 +02:00
4104169c19 Fix path excluding symbols that are used in requests 2024-04-21 21:46:52 +02:00
4cb7c941a2 Implemented /v2/Connector/<ConnectorName>/GetManga 2024-04-21 21:32:03 +02:00
b3fb53f6d8 Corrected link 2024-04-20 18:55:54 +02:00
8b9769b816 Merge branch 'refs/heads/master' into Server-V2 2024-04-20 18:49:08 +02:00
9a02859f6b Docker Image build 2024-04-20 18:46:00 +02:00
e96dd07521 Link API Documentation in README.md 2024-04-20 18:41:12 +02:00
a610eff8f0 Merge branch 'refs/heads/cuttingedge' into Server-V2 2024-04-20 18:39:56 +02:00
c41f04d92d All Valid Request Paths return "Not Implemented".
Ping returns Pong.
2024-04-20 18:34:20 +02:00
5e647099cd Spelling 2024-04-20 17:56:54 +02:00
011af9c7a8 #114 API Documentation 2024-04-20 16:59:51 +02:00
630e507564 #74 API Documentation 2024-04-20 16:59:32 +02:00
fa2598084f Hard cutover https://github.com/C9Glax/tranga/pull/167#issuecomment-2067689986 2024-04-20 16:54:58 +02:00
f79743ee93 actually use v2 API 2024-04-19 22:20:24 +02:00
2828fec316 Merge 2024-04-19 22:08:03 +02:00
bd14722791 Merge remote-tracking branch 'refs/remotes/db-2001/json-api' into Server-V2 2024-04-19 22:06:55 +02:00
d22b49cfa8 Change Method Header for Handlers to return the response to HandleRequest so we don't forget to send a response. 2024-04-19 21:58:29 +02:00
595051b0fe Merge remote-tracking branch 'origin/Server-V2' into Server-V2 2024-04-19 21:54:21 +02:00
238395a3da Return JobIds instead of full jobs.
/v2/Jobs
/v2/Jobs/Running
/v2/Jobs/Waiting
/v2/Jobs/Monitoring
2024-04-19 21:54:16 +02:00
0313d81204 Return JobIds instead of full jobs.
/v2/Jobs
/v2/Jobs/Running
/v2/Jobs/Waiting
/v2/Jobs/Monitoring
2024-04-19 21:40:31 +02:00
f5cecb9e30 Github Reference Link Style 2024-04-19 21:35:38 +02:00
7e5fa6ce41 API v2 2024-04-19 21:23:15 +02:00
db-2001
a8aa7d3370 Okay, actually write request variables to log. 2024-04-18 18:45:19 -04:00
db-2001
01bab62190 Log request if unknown 2024-04-18 18:32:49 -04:00
db-2001
2768ab38e6 Merge remote-tracking branch 'upstream/cuttingedge' into json-api 2024-04-18 18:24:26 -04:00
db-2001
33b8ede492 Use new requestParams variable for AprilFoolsMode setting 2024-04-18 17:58:23 -04:00
db-2001
dbc1b94124 Merge branch 'cuttingedge' into json-api
Solved Merge conflicts with cuttingedge branch
2024-04-18 17:56:44 -04:00
Dity
6f5fb7e0bb API rewrite to parse JSON body for POST and DELETE 2024-04-07 18:20:28 -04:00
Dity
7628510b87 Documentation for API Calls 2024-04-06 18:13:31 -04:00
Dity
dd965d886a Revert "Added initial documentation for API Calls"
This reverts commit 7e54577c54.
2024-04-06 18:11:03 -04:00
The Estrada Lab (University of Michigan)
7e54577c54 Added initial documentation for API Calls 2024-04-06 18:05:27 -04:00
40 changed files with 3377 additions and 1031 deletions

2
.gitignore vendored
View File

@ -20,6 +20,8 @@ riderModule.iml
cover.jpg
cover.png
/.vscode
/.vs/
Tranga/Properties/launchSettings.json
/Manga
/settings
*.DotSettings.user

View File

@ -2,7 +2,7 @@
<br />
<div align="center">
<h3 align="center">Tranga</h3>
<h3 align="center">Tranga v2</h3>
<p align="center">
Automatic Manga and Metadata downloader
@ -61,7 +61,8 @@ Notifications can be sent to your devices using [Gotify](https://gotify.net/), [
### What this does and doesn't do
Tranga (this git-repo) will open a port (standard 6531) and listen for requests to add Jobs to Monitor and/or download specific Manga.
The configuration is all done through HTTP-Requests.
The configuration is all done through HTTP-Requests. [Documentation](docs/API_Calls_v2.md)
_**For a web-frontend use [tranga-website](https://github.com/C9Glax/tranga-website).**_
This project downloads the images for a Manga from the specified Scanlation-Website and packages them with some metadata - from that same website - in a .cbz-archive (per chapter).
@ -89,6 +90,8 @@ That is why I wanted to create my own project, in a language I understand, and t
- [PuppeteerSharp](https://www.puppeteersharp.com/)
- [Html Agility Pack (HAP)](https://html-agility-pack.net/)
- [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch)
- [Sixlabors.ImageSharp](https://docs-v2.sixlabors.com/articles/imagesharp/index.html#license)
- [zstd-wrapper](https://github.com/oleg-st/ZstdSharp) [zstd](https://github.com/facebook/zstd)
- 💙 Blåhaj 🦈
<p align="right">(<a href="#readme-top">back to top</a>)</p>

View File

@ -1,5 +1,7 @@
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using static System.IO.UnixFileMode;
namespace Tranga;
@ -17,23 +19,21 @@ public readonly struct Chapter : IComparable
public string url { get; }
// ReSharper disable once MemberCanBePrivate.Global
public string fileName { get; }
public string? id { get; }
private static readonly Regex LegalCharacters = new (@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
private static readonly Regex IllegalStrings = new(@"(Vol(ume)?|Ch(apter)?)\.?", RegexOptions.IgnoreCase);
private static readonly Regex Digits = new(@"[0-9\.]*");
public Chapter(Manga parentManga, string? name, string? volumeNumber, string chapterNumber, string url)
public Chapter(Manga parentManga, string? name, string? volumeNumber, string chapterNumber, string url, string? id = null)
{
this.parentManga = parentManga;
this.name = name;
this.volumeNumber = volumeNumber is not null ? string.Concat(Digits.Matches(volumeNumber).Select(x => x.Value)) : "0";
this.chapterNumber = string.Concat(Digits.Matches(chapterNumber).Select(x => x.Value));
this.url = url;
this.id = id;
string chapterVolNumStr;
if (volumeNumber is not null && volumeNumber.Length > 0)
chapterVolNumStr = $"Vol.{volumeNumber} Ch.{chapterNumber}";
else
chapterVolNumStr = $"Ch.{chapterNumber}";
string chapterVolNumStr = $"Vol.{this.volumeNumber} Ch.{chapterNumber}";
if (name is not null && name.Length > 0)
{
@ -87,24 +87,49 @@ public readonly struct Chapter : IComparable
string mangaDirectory = Path.Join(TrangaSettings.downloadLocation, parentManga.folderName);
if (!Directory.Exists(mangaDirectory))
return false;
FileInfo[] archives = new DirectoryInfo(mangaDirectory).GetFiles("*.cbz");
Regex volChRex = new(@"(?:Vol(?:ume)?\.([0-9]+)\D*)?Ch(?:apter)?\.([0-9]+(?:\.[0-9]+)*)");
Chapter t = this;
string correctPath = GetArchiveFilePath();
FileInfo? archive = archives.FirstOrDefault(archive =>
FileInfo? mangaArchive = null;
string markerPath = Path.Join(mangaDirectory, $".{id}");
if (this.id is not null && File.Exists(markerPath))
{
Match m = volChRex.Match(archive.Name);
/*Uncommenting this section will only allow *Version without Volume number* -> *Version with Volume number* but not the other way
if (m.Groups[1].Success)
return m.Groups[1].Value == t.volumeNumber && m.Groups[2].Value == t.chapterNumber;
else*/
return m.Groups[2].Value == t.chapterNumber;
});
if(archive is not null && archive.FullName != correctPath)
archive.MoveTo(correctPath, true);
return (archive is not null);
if(File.Exists(File.ReadAllText(markerPath)))
mangaArchive = new FileInfo(File.ReadAllText(markerPath));
else
File.Delete(markerPath);
}
if(mangaArchive is null)
{
FileInfo[] archives = new DirectoryInfo(mangaDirectory).GetFiles("*.cbz");
Regex volChRex = new(@"(?:Vol(?:ume)?\.([0-9]+)\D*)?Ch(?:apter)?\.([0-9]+(?:\.[0-9]+)*)");
Chapter t = this;
mangaArchive = archives.FirstOrDefault(archive =>
{
Match m = volChRex.Match(archive.Name);
if (m.Groups[1].Success)
return m.Groups[1].Value == t.volumeNumber && m.Groups[2].Value == t.chapterNumber;
else
return m.Groups[2].Value == t.chapterNumber;
});
}
string correctPath = GetArchiveFilePath();
if(mangaArchive is not null && mangaArchive.FullName != correctPath)
mangaArchive.MoveTo(correctPath, true);
return (mangaArchive is not null);
}
public void CreateChapterMarker()
{
if (this.id is null)
return;
string path = Path.Join(TrangaSettings.downloadLocation, parentManga.folderName, $".{id}");
File.WriteAllText(path, GetArchiveFilePath());
File.SetAttributes(path, FileAttributes.Hidden);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(path, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
}
/// <summary>
/// Creates full file path of chapter-archive
/// </summary>

View File

@ -1,8 +1,11 @@
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using Logging;
using Newtonsoft.Json;
using Tranga.LibraryConnectors;
using Tranga.MangaConnectors;
using Tranga.NotificationConnectors;
namespace Tranga;
@ -14,6 +17,7 @@ public abstract class GlobalBase
protected HashSet<NotificationConnector> notificationConnectors { get; init; }
protected HashSet<LibraryConnector> libraryConnectors { get; init; }
private Dictionary<string, Manga> cachedPublications { get; init; }
protected HashSet<MangaConnector> _connectors;
public static readonly NumberFormatInfo numberFormatDecimalPoint = new (){ NumberDecimalSeparator = "." };
protected static readonly Regex baseUrlRex = new(@"https?:\/\/[0-9A-z\.-]+(:[0-9]+)?");
@ -23,6 +27,7 @@ public abstract class GlobalBase
this.notificationConnectors = clone.notificationConnectors;
this.libraryConnectors = clone.libraryConnectors;
this.cachedPublications = clone.cachedPublications;
this._connectors = clone._connectors;
}
protected GlobalBase(Logger? logger)
@ -31,15 +36,7 @@ public abstract class GlobalBase
this.notificationConnectors = TrangaSettings.LoadNotificationConnectors(this);
this.libraryConnectors = TrangaSettings.LoadLibraryConnectors(this);
this.cachedPublications = new();
}
protected void AddMangaToCache(Manga manga)
{
if (!this.cachedPublications.TryAdd(manga.internalId, manga))
{
Log($"Overwriting Manga {manga.internalId}");
this.cachedPublications[manga.internalId] = manga;
}
this._connectors = new();
}
protected Manga? GetCachedManga(string internalId)
@ -51,9 +48,71 @@ public abstract class GlobalBase
};
}
protected IEnumerable<Manga> GetAllCachedManga()
protected IEnumerable<Manga> GetAllCachedManga() => cachedPublications.Values;
protected void AddMangaToCache(Manga manga)
{
return cachedPublications.Values;
if (!cachedPublications.TryAdd(manga.internalId, manga))
{
Log($"Overwriting Manga {manga.internalId}");
cachedPublications[manga.internalId] = manga;
}
ExportManga();
}
protected void RemoveMangaFromCache(Manga manga) => RemoveMangaFromCache(manga.internalId);
protected void RemoveMangaFromCache(string internalId)
{
cachedPublications.Remove(internalId);
ExportManga();
}
internal void ImportManga()
{
string folder = TrangaSettings.mangaCacheFolderPath;
Directory.CreateDirectory(folder);
foreach (FileInfo fileInfo in new DirectoryInfo(folder).GetFiles())
{
string content = File.ReadAllText(fileInfo.FullName);
try
{
Manga m = JsonConvert.DeserializeObject<Manga>(content, new MangaConnectorJsonConverter(this, _connectors));
this.cachedPublications.TryAdd(m.internalId, m);
}
catch (JsonException e)
{
Log($"Error parsing Manga {fileInfo.Name}:\n{e.Message}");
}
}
}
private static bool ExportRunning = false;
private void ExportManga()
{
while (ExportRunning)
Thread.Sleep(1);
ExportRunning = true;
string folder = TrangaSettings.mangaCacheFolderPath;
Directory.CreateDirectory(folder);
Manga[] copy = new Manga[cachedPublications.Values.Count];
cachedPublications.Values.CopyTo(copy, 0);
foreach (Manga manga in copy)
{
string content = JsonConvert.SerializeObject(manga, Formatting.Indented);
string filePath = Path.Combine(folder, $"{manga.internalId}.json");
File.WriteAllText(filePath, content, Encoding.UTF8);
}
foreach (FileInfo fileInfo in new DirectoryInfo(folder).GetFiles())
{
if(!cachedPublications.Keys.Any(key => fileInfo.Name.Substring(0, fileInfo.Name.LastIndexOf('.')).Equals(key)))
fileInfo.Delete();
}
ExportRunning = false;
}
protected void Log(string message)

View File

@ -7,12 +7,12 @@ public class DownloadChapter : Job
{
public Chapter chapter { get; init; }
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, connector, lastExecution, parentJobId: parentJobId)
public DownloadChapter(GlobalBase clone, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, lastExecution, parentJobId: parentJobId)
{
this.chapter = chapter;
}
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, connector, parentJobId: parentJobId)
public DownloadChapter(GlobalBase clone, Chapter chapter, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, parentJobId: parentJobId)
{
this.chapter = chapter;
}
@ -44,11 +44,15 @@ public class DownloadChapter : Job
return Array.Empty<Job>();
}
protected override MangaConnector GetMangaConnector()
{
return chapter.parentManga.mangaConnector;
}
public override bool Equals(object? obj)
{
if (obj is not DownloadChapter otherJob)
return false;
return otherJob.mangaConnector == this.mangaConnector &&
otherJob.chapter.Equals(this.chapter);
return otherJob.chapter.Equals(this.chapter);
}
}

View File

@ -1,29 +1,29 @@
using Tranga.MangaConnectors;
using Newtonsoft.Json;
using Tranga.MangaConnectors;
namespace Tranga.Jobs;
public class DownloadNewChapters : Job
{
public Manga manga { get; set; }
public string mangaInternalId { get; set; }
[JsonIgnore] private Manga? manga => GetCachedManga(mangaInternalId);
public string translatedLanguage { get; init; }
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, DateTime lastExecution,
bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, connector, lastExecution, recurring,
recurrence, parentJobId)
public DownloadNewChapters(GlobalBase clone, string mangaInternalId, DateTime lastExecution, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, lastExecution, recurring, recurrence, parentJobId)
{
this.manga = manga;
this.mangaInternalId = mangaInternalId;
this.translatedLanguage = translatedLanguage;
}
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base (clone, JobType.DownloadNewChaptersJob, connector, recurring, recurrence, parentJobId)
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, string mangaInternalId, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base (clone, JobType.DownloadNewChaptersJob, recurring, recurrence, parentJobId)
{
this.manga = manga;
this.mangaInternalId = mangaInternalId;
this.translatedLanguage = translatedLanguage;
}
protected override string GetId()
{
return $"{GetType()}-{manga.internalId}";
return $"{GetType()}-{mangaInternalId}";
}
public override string ToString()
@ -33,27 +33,38 @@ public class DownloadNewChapters : Job
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss)
{
manga.SaveSeriesInfoJson();
Chapter[] chapters = mangaConnector.GetNewChapters(manga, this.translatedLanguage);
if (manga is null)
{
Log($"Manga {mangaInternalId} is missing! Can not execute job.");
return Array.Empty<Job>();
}
manga.Value.SaveSeriesInfoJson();
Chapter[] chapters = manga.Value.mangaConnector.GetNewChapters(manga.Value, this.translatedLanguage);
this.progressToken.increments = chapters.Length;
List<Job> jobs = new();
mangaConnector.CopyCoverFromCacheToDownloadLocation(manga);
manga.Value.mangaConnector.CopyCoverFromCacheToDownloadLocation(manga.Value);
foreach (Chapter chapter in chapters)
{
DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id);
DownloadChapter downloadChapterJob = new(this, chapter, parentJobId: this.id);
jobs.Add(downloadChapterJob);
}
UpdateMetadata updateMetadataJob = new(this, this.mangaConnector, this.manga, parentJobId: this.id);
UpdateMetadata updateMetadataJob = new(this, mangaInternalId, parentJobId: this.id);
jobs.Add(updateMetadataJob);
progressToken.Complete();
return jobs;
}
protected override MangaConnector GetMangaConnector()
{
if (manga is null)
throw new Exception($"Missing Manga {mangaInternalId}");
return manga.Value.mangaConnector;
}
public override bool Equals(object? obj)
{
if (obj is not DownloadNewChapters otherJob)
return false;
return otherJob.mangaConnector == this.mangaConnector &&
otherJob.manga.Equals(this.manga);
return otherJob.manga.Equals(this.manga);
}
}

View File

@ -4,7 +4,6 @@ namespace Tranga.Jobs;
public abstract class Job : GlobalBase
{
public MangaConnector mangaConnector { get; init; }
public ProgressToken progressToken { get; private set; }
public bool recurring { get; init; }
public TimeSpan? recurrenceTime { get; set; }
@ -13,14 +12,15 @@ public abstract class Job : GlobalBase
public string id => GetId();
internal IEnumerable<Job>? subJobs { get; private set; }
public string? parentJobId { get; init; }
public enum JobType : byte { DownloadChapterJob, DownloadNewChaptersJob, UpdateMetaDataJob }
public enum JobType : byte { DownloadChapterJob = 0, DownloadNewChaptersJob = 1, UpdateMetaDataJob = 2, MonitorManga = 3 }
public MangaConnector mangaConnector => GetMangaConnector();
public JobType jobType;
internal Job(GlobalBase clone, JobType jobType, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
internal Job(GlobalBase clone, JobType jobType, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
{
this.jobType = jobType;
this.mangaConnector = connector;
this.progressToken = new ProgressToken(0);
this.recurring = recurring;
if (recurring && recurrenceTime is null)
@ -31,11 +31,10 @@ public abstract class Job : GlobalBase
this.parentJobId = parentJobId;
}
internal Job(GlobalBase clone, JobType jobType, MangaConnector connector, DateTime lastExecution, bool recurring = false,
internal Job(GlobalBase clone, JobType jobType, DateTime lastExecution, bool recurring = false,
TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
{
this.jobType = jobType;
this.mangaConnector = connector;
this.progressToken = new ProgressToken(0);
this.recurring = recurring;
if (recurring && recurrenceTime is null)
@ -95,4 +94,6 @@ public abstract class Job : GlobalBase
}
protected abstract IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss);
protected abstract MangaConnector GetMangaConnector();
}

View File

@ -1,6 +1,8 @@
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Tranga.MangaConnectors;
using static System.IO.UnixFileMode;
namespace Tranga.Jobs;
@ -17,18 +19,21 @@ public class JobBoss : GlobalBase
Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}");
}
public void AddJob(Job job)
public bool AddJob(Job job, string? jobFile = null)
{
if (ContainsJobLike(job))
{
Log($"Already Contains Job {job}");
return false;
}
else
{
if (!this.jobs.Add(job))
return false;
Log($"Added {job}");
this.jobs.Add(job);
UpdateJobFile(job);
UpdateJobFile(job, jobFile);
}
return true;
}
public void AddJobs(IEnumerable<Job> jobsToAdd)
@ -65,11 +70,9 @@ public class JobBoss : GlobalBase
RemoveJob(job);
}
public IEnumerable<Job> GetJobsLike(string? connectorName = null, string? internalId = null, string? chapterNumber = null)
public IEnumerable<Job> GetJobsLike(string? internalId = null, string? chapterNumber = null)
{
IEnumerable<Job> ret = this.jobs;
if (connectorName is not null)
ret = ret.Where(job => job.mangaConnector.name == connectorName);
if (internalId is not null && chapterNumber is not null)
ret = ret.Where(jjob =>
@ -84,18 +87,18 @@ public class JobBoss : GlobalBase
{
if (jjob is not DownloadNewChapters job)
return false;
return job.manga.internalId == internalId;
return job.mangaInternalId == internalId;
});
return ret;
}
public IEnumerable<Job> GetJobsLike(MangaConnector? mangaConnector = null, Manga? publication = null,
public IEnumerable<Job> GetJobsLike(Manga? publication = null,
Chapter? chapter = null)
{
if (chapter is not null)
return GetJobsLike(mangaConnector?.name, chapter.Value.parentManga.internalId, chapter.Value.chapterNumber);
return GetJobsLike(chapter.Value.parentManga.internalId, chapter.Value.chapterNumber);
else
return GetJobsLike(mangaConnector?.name, publication?.internalId);
return GetJobsLike(publication?.internalId);
}
public Job? GetJobById(string jobId)
@ -140,15 +143,17 @@ public class JobBoss : GlobalBase
private void LoadJobsList(HashSet<MangaConnector> connectors)
{
Directory.CreateDirectory(TrangaSettings.jobsFolderPath);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(TrangaSettings.jobsFolderPath, UserRead | UserWrite | UserExecute | GroupRead | OtherRead);
if (!Directory.Exists(TrangaSettings.jobsFolderPath)) //No jobs to load
{
Directory.CreateDirectory(TrangaSettings.jobsFolderPath);
return;
}
Regex idRex = new (@"(.*)\.json");
//Load Manga-Files
ImportManga();
//Load json-job-files
foreach (FileInfo file in new DirectoryInfo(TrangaSettings.jobsFolderPath).EnumerateFiles().Where(fileInfo => idRex.IsMatch(fileInfo.Name)))
foreach (FileInfo file in Directory.GetFiles(TrangaSettings.jobsFolderPath, "*.json").Select(f => new FileInfo(f)))
{
Log($"Adding {file.Name}");
Job? job = JsonConvert.DeserializeObject<Job>(File.ReadAllText(file.FullName),
@ -162,8 +167,12 @@ public class JobBoss : GlobalBase
else
{
Log($"Adding Job {job}");
this.jobs.Add(job);
UpdateJobFile(job, file.Name);
if (!AddJob(job, file.FullName)) //If we detect a duplicate, delete the file.
{
string path = string.Concat(file.FullName, ".duplicate");
file.MoveTo(path);
Log($"Duplicate detected or otherwise not able to add job to list.\nMoved job {job} to {path}");
}
}
}
@ -177,13 +186,25 @@ public class JobBoss : GlobalBase
parentJob.AddSubJob(job);
Log($"Parent Job {parentJob}");
}
if (job is DownloadNewChapters dncJob)
AddMangaToCache(dncJob.manga);
}
string[] jobMangaInternalIds = this.jobs.Where(job => job is DownloadNewChapters)
.Select(dnc => ((DownloadNewChapters)dnc).mangaInternalId).ToArray();
jobMangaInternalIds = jobMangaInternalIds.Concat(
this.jobs.Where(job => job is UpdateMetadata)
.Select(dnc => ((UpdateMetadata)dnc).mangaInternalId)).ToArray();
string[] internalIds = GetAllCachedManga().Select(m => m.internalId).ToArray();
string[] extraneousIds = internalIds.Except(jobMangaInternalIds).ToArray();
foreach (string internalId in extraneousIds)
RemoveMangaFromCache(internalId);
string[] coverFiles = Directory.GetFiles(TrangaSettings.coverImageCache);
foreach(string fileName in coverFiles.Where(fileName => !GetAllCachedManga().Any(manga => manga.coverFileNameInCache == fileName)))
File.Delete(fileName);
string[] mangaFiles = Directory.GetFiles(TrangaSettings.mangaCacheFolderPath);
foreach(string fileName in mangaFiles.Where(fileName => !GetAllCachedManga().Any(manga => fileName.Split('.')[0] == manga.internalId)))
File.Delete(fileName);
}
internal void UpdateJobFile(Job job, string? oldFile = null)
@ -203,7 +224,8 @@ public class JobBoss : GlobalBase
}
catch (Exception e)
{
Log(e.ToString());
Log($"Error deleting {oldFilePath} job {job.id}\n{e}");
return; //Don't export a new file when we haven't actually deleted the old one
}
}
@ -215,6 +237,8 @@ public class JobBoss : GlobalBase
while(IsFileInUse(newJobFilePath))
Thread.Sleep(10);
File.WriteAllText(newJobFilePath, jobStr);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(newJobFilePath, UserRead | UserWrite | GroupRead | OtherRead);
}
}

View File

@ -23,53 +23,32 @@ public class JobJsonConverter : JsonConverter
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
if(!jo.ContainsKey("jobType"))
throw new Exception();
if (jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.UpdateMetaDataJob)
return Enum.Parse<Job.JobType>(jo["jobType"]!.Value<byte>().ToString()) switch
{
return new UpdateMetadata(this._clone,
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
Job.JobType.UpdateMetaDataJob => new UpdateMetadata(_clone,
jo.GetValue("mangaInternalId")!.Value<string>()!,
jo.GetValue("parentJobId")!.Value<string?>()),
Job.JobType.DownloadChapterJob => new DownloadChapter(this._clone,
jo.GetValue("chapter")!.ToObject<Chapter>(JsonSerializer.Create(new JsonSerializerSettings()
{
Converters =
{
this._mangaConnectorJsonConverter
}
}))!,
jo.GetValue("manga")!.ToObject<Manga>(),
jo.GetValue("parentJobId")!.Value<string?>());
}else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.DownloadNewChaptersJob) || jo.ContainsKey("translatedLanguage"))//TODO change to jobType
{
DateTime lastExecution = jo.GetValue("lastExecution") is {} le
? le.ToObject<DateTime>()
: DateTime.UnixEpoch; //TODO do null checks on all variables
return new DownloadNewChapters(this._clone,
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
{
Converters =
{
this._mangaConnectorJsonConverter
}
}))!,
jo.GetValue("manga")!.ToObject<Manga>(),
lastExecution,
Converters = { this._mangaConnectorJsonConverter }
})),
DateTime.UnixEpoch,
jo.GetValue("parentJobId")!.Value<string?>()),
Job.JobType.DownloadNewChaptersJob => new DownloadNewChapters(this._clone,
jo.GetValue("mangaInternalId")!.Value<string>()!,
jo.GetValue("lastExecution") is {} le
? le.ToObject<DateTime>()
: DateTime.UnixEpoch,
jo.GetValue("recurring")!.Value<bool>(),
jo.GetValue("recurrenceTime")!.ToObject<TimeSpan?>(),
jo.GetValue("parentJobId")!.Value<string?>());
}else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.DownloadChapterJob) || jo.ContainsKey("chapter"))//TODO change to jobType
{
return new DownloadChapter(this._clone,
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
{
Converters =
{
this._mangaConnectorJsonConverter
}
}))!,
jo.GetValue("chapter")!.ToObject<Chapter>(),
DateTime.UnixEpoch,
jo.GetValue("parentJobId")!.Value<string?>());
}
throw new Exception();
jo.GetValue("parentJobId")!.Value<string?>()),
_ => throw new Exception()
};
}
public override bool CanWrite => false;

View File

@ -10,7 +10,7 @@ public class ProgressToken
public DateTime executionStarted { get; private set; }
public TimeSpan timeRemaining => GetTimeRemaining();
public enum State { Running, Complete, Standby, Cancelled, Waiting }
public enum State : byte { Running = 0, Complete = 1, Standby = 2, Cancelled = 3, Waiting = 4 }
public State state { get; private set; }
public ProgressToken(int increments)

View File

@ -1,19 +1,21 @@
using Tranga.MangaConnectors;
using System.Text.Json.Serialization;
using Tranga.MangaConnectors;
namespace Tranga.Jobs;
public class UpdateMetadata : Job
{
public Manga manga { get; set; }
public string mangaInternalId { get; set; }
[JsonIgnore] private Manga? manga => GetCachedManga(mangaInternalId);
public UpdateMetadata(GlobalBase clone, MangaConnector connector, Manga manga, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, connector, parentJobId: parentJobId)
public UpdateMetadata(GlobalBase clone, string mangaInternalId, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, parentJobId: parentJobId)
{
this.manga = manga;
this.mangaInternalId = mangaInternalId;
}
protected override string GetId()
{
return $"{GetType()}-{manga.internalId}";
return $"{GetType()}-{mangaInternalId}";
}
public override string ToString()
@ -23,8 +25,14 @@ public class UpdateMetadata : Job
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss)
{
if (manga is null)
{
Log($"Manga {mangaInternalId} is missing! Can not execute job.");
return Array.Empty<Job>();
}
//Retrieve new Metadata
Manga? possibleUpdatedManga = mangaConnector.GetMangaFromId(manga.publicationId);
Manga? possibleUpdatedManga = mangaConnector.GetMangaFromId(manga.Value.publicationId);
if (possibleUpdatedManga is { } updatedManga)
{
if (updatedManga.Equals(this.manga)) //Check if anything changed
@ -33,26 +41,9 @@ public class UpdateMetadata : Job
return Array.Empty<Job>();
}
this.manga = manga.WithMetadata(updatedManga);
this.manga.SaveSeriesInfoJson(true);
this.mangaConnector.CopyCoverFromCacheToDownloadLocation(manga);
foreach (Job job in jobBoss.GetJobsLike(publication: this.manga))
{
string oldFile;
if (job is DownloadNewChapters dc)
{
oldFile = dc.id;
dc.manga = this.manga;
}
else if (job is UpdateMetadata um)
{
oldFile = um.id;
um.manga = this.manga;
}
else
continue;
jobBoss.UpdateJobFile(job, oldFile);
}
AddMangaToCache(manga.Value.WithMetadata(updatedManga));
this.manga.Value.SaveSeriesInfoJson(true);
this.mangaConnector.CopyCoverFromCacheToDownloadLocation((Manga)manga);
this.progressToken.Complete();
}
else
@ -65,12 +56,18 @@ public class UpdateMetadata : Job
return Array.Empty<Job>();
}
protected override MangaConnector GetMangaConnector()
{
if (manga is null)
throw new Exception($"Missing Manga {mangaInternalId}");
return manga.Value.mangaConnector;
}
public override bool Equals(object? obj)
{
if (obj is not UpdateMetadata otherJob)
return false;
return otherJob.mangaConnector == this.mangaConnector &&
otherJob.manga.Equals(this.manga);
return otherJob.manga.Equals(this.manga);
}
}

View File

@ -3,6 +3,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Newtonsoft.Json;
using Tranga.MangaConnectors;
using static System.IO.UnixFileMode;
namespace Tranga;
@ -27,8 +28,6 @@ public struct Manga
// ReSharper disable once MemberCanBePrivate.Global
public int? year { get; private set; }
public string? originalLanguage { get; }
// ReSharper disable twice MemberCanBePrivate.Global
public string status { get; private set; }
public ReleaseStatusByte releaseStatus { get; private set; }
public enum ReleaseStatusByte : byte
{
@ -44,14 +43,15 @@ public struct Manga
public float ignoreChaptersBelow { get; set; }
public float latestChapterDownloaded { get; set; }
public float latestChapterAvailable { get; set; }
public string? websiteUrl { get; private set; }
public string websiteUrl { get; private set; }
public MangaConnector mangaConnector { get; private set; }
private static readonly Regex LegalCharacters = new (@"[A-Za-zÀ-ÖØ-öø-ÿ0-9 \.\-,'\'\)\(~!\+]*");
[JsonConstructor]
public Manga(string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string publicationId, ReleaseStatusByte releaseStatus, string? websiteUrl = null, string? folderName = null, float? ignoreChaptersBelow = 0)
public Manga(MangaConnector mangaConnector, string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string publicationId, ReleaseStatusByte releaseStatus, string? websiteUrl, string? folderName = null, float? ignoreChaptersBelow = 0)
{
this.mangaConnector = mangaConnector;
this.sortName = HttpUtility.HtmlDecode(sortName);
this.authors = authors.Select(HttpUtility.HtmlDecode).ToList()!;
this.description = HttpUtility.HtmlDecode(description);
@ -72,8 +72,7 @@ public struct Manga
this.latestChapterDownloaded = 0;
this.latestChapterAvailable = 0;
this.releaseStatus = releaseStatus;
this.status = Enum.GetName(releaseStatus) ?? "";
this.websiteUrl = websiteUrl;
this.websiteUrl = websiteUrl??"";
}
public Manga WithMetadata(Manga newManga)
@ -86,7 +85,6 @@ public struct Manga
authors = authors.Union(newManga.authors).ToList(),
altTitles = altTitles.UnionBy(newManga.altTitles, kv => kv.Key).ToDictionary(x => x.Key, x => x.Value),
tags = tags.Union(newManga.tags).ToArray(),
status = newManga.status,
releaseStatus = newManga.releaseStatus,
websiteUrl = newManga.websiteUrl,
year = newManga.year,
@ -100,7 +98,6 @@ public struct Manga
return false;
return this.description == compareManga.description &&
this.year == compareManga.year &&
this.status == compareManga.status &&
this.releaseStatus == compareManga.releaseStatus &&
this.sortName == compareManga.sortName &&
this.latestChapterAvailable.Equals(compareManga.latestChapterAvailable) &&

View File

@ -0,0 +1,208 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.Jobs;
namespace Tranga.MangaConnectors;
public class AsuraToon : MangaConnector
{
public AsuraToon(GlobalBase clone) : base(clone, "AsuraToon", ["en"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
public override Manga[] GetManga(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{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 Array.Empty<Manga>();
if (requestResult.htmlDocument is null)
{
Log($"Failed to retrieve site");
return Array.Empty<Manga>();
}
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
public override Manga? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://asuracomic.net/series/{publicationId}");
}
public override Manga? 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)
{
Log($"Failed to retrieve site");
return null;
}
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
}
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
{
HtmlNodeCollection mangaList = document.DocumentNode.SelectNodes("//a[starts-with(@href,'series')]");
if (mangaList.Count < 1)
return Array.Empty<Manga>();
IEnumerable<string> urls = mangaList.Select(a => $"https://asuracomic.net/{a.GetAttributeValue("href", "")}");
List<Manga> ret = new();
foreach (string url in urls)
{
Manga? manga = GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
}
return ret.ToArray();
}
private Manga 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();
HtmlNode statusNode = document.DocumentNode.SelectSingleNode("//h3[text()='Status']/../h3[2]");
Manga.ReleaseStatusByte releaseStatus = statusNode.InnerText.ToLower() switch
{
"ongoing" => Manga.ReleaseStatusByte.Continuing,
"hiatus" => Manga.ReleaseStatusByte.OnHiatus,
"completed" => Manga.ReleaseStatusByte.Completed,
"dropped" => Manga.ReleaseStatusByte.Cancelled,
"season end" => Manga.ReleaseStatusByte.Continuing,
"coming soon" => Manga.ReleaseStatusByte.Unreleased,
_ => Manga.ReleaseStatusByte.Unreleased
};
HtmlNode coverNode =
document.DocumentNode.SelectSingleNode("//img[@alt='poster']");
string coverUrl = coverNode.GetAttributeValue("src", "");
string coverFileNameInCache = SaveCoverImageToCache(coverUrl, publicationId, RequestType.MangaCover);
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()='Author' or text()='_')]");
List<string> authors = authorNodes.Select(a => a.InnerText).Concat(artistNodes.Select(a => a.InnerText)).ToList();
HtmlNode? firstChapterNode = document.DocumentNode.SelectSingleNode("//a[contains(@href, 'chapter/1')]/../following-sibling::h3");
int? year = int.Parse(firstChapterNode?.InnerText.Split(' ')[^1] ?? "2000");
Manga manga = new (sortName, authors, description, altTitles, tags, coverUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
Log($"Getting chapters {manga}");
string requestUrl = $"https://asuracomic.net/series/{manga.publicationId}";
// 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);
Log($"Got {chapters.Count} chapters. {manga}");
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)
{
Log("Failed to load site");
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);
string chapterNumber = 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}";
ret.Add(new Chapter(manga, chapterName, null, chapterNumber, url));
}
return ret;
}
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Manga chapterParentManga = chapter.parentManga;
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
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)
{
progressToken?.Cancel();
return requestResult.statusCode;
}
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(string mangaUrl)
{
RequestResult requestResult =
downloadClient.MakeRequest(mangaUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
return Array.Empty<string>();
}
if (requestResult.htmlDocument is null)
{
Log($"Failed to retrieve site");
return Array.Empty<string>();
}
HtmlNodeCollection images =
requestResult.htmlDocument.DocumentNode.SelectNodes("//img[contains(@alt, 'chapter page')]");
return images.Select(i => i.GetAttributeValue("src", "")).ToArray();
}
}

View File

@ -8,7 +8,7 @@ namespace Tranga.MangaConnectors;
public class Bato : MangaConnector
{
public Bato(GlobalBase clone) : base(clone, "Bato", ["en"])
public Bato(GlobalBase clone) : base(clone, "Bato", ["en"], ["bato.to"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
@ -114,8 +114,8 @@ public class Bato : MangaConnector
case "pending": releaseStatus = Manga.ReleaseStatusByte.Unreleased; break;
}
Manga manga = new (sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary<string, string>(),
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
Manga manga = new (this, sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary<string, string>(),
year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
@ -150,7 +150,7 @@ public class Bato : MangaConnector
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\.]+)");
Regex numberRex = new(@"\/title\/.+\/([0-9])+(?:-vol_([0-9]+))?-ch_([0-9\.]+)");
foreach (HtmlNode chapterInfo in chapterList.SelectNodes("div"))
{
@ -158,6 +158,7 @@ public class Bato : MangaConnector
string chapterUrl = infoNode.GetAttributeValue("href", "");
Match match = numberRex.Match(chapterUrl);
string id = match.Groups[1].Value;
string? volumeNumber = match.Groups[2].Success ? match.Groups[2].Value : null;
string chapterNumber = match.Groups[3].Value;
string chapterName = chapterNumber;
@ -189,11 +190,8 @@ public class Bato : MangaConnector
}
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(), RequestType.MangaImage, comicInfoPath, "https://mangakatana.com/", progressToken:progressToken);
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(string mangaUrl)

View File

@ -2,6 +2,10 @@
using System.Net;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Binarization;
using Tranga.Jobs;
using static System.IO.UnixFileMode;
@ -15,11 +19,13 @@ public abstract class MangaConnector : GlobalBase
{
internal DownloadClient downloadClient { get; init; } = null!;
public string[] SupportedLanguages;
public string[] BaseUris;
protected MangaConnector(GlobalBase clone, string name, string[] supportedLanguages) : base(clone)
protected MangaConnector(GlobalBase clone, string name, string[] supportedLanguages, string[] baseUris) : base(clone)
{
this.name = name;
this.SupportedLanguages = supportedLanguages;
this.BaseUris = baseUris;
Directory.CreateDirectory(TrangaSettings.coverImageCache);
}
@ -214,8 +220,26 @@ public abstract class MangaConnector : GlobalBase
return requestResult.statusCode;
}
protected HttpStatusCode DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, RequestType requestType, string? comicInfoPath = null, string? referrer = null, ProgressToken? progressToken = null)
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
});
Log($"Image processing took {DateTime.Now.Subtract(start):s\\.fff} B/W:{TrangaSettings.bwImages} Compression: {TrangaSettings.compression}");
}
protected HttpStatusCode DownloadChapterImages(string[] imageUrls, Chapter chapter, RequestType requestType, string? referrer = null, ProgressToken? progressToken = null)
{
string saveArchiveFilePath = chapter.GetArchiveFilePath();
if (progressToken?.cancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
Log($"Downloading Images for {saveArchiveFilePath}");
@ -239,7 +263,7 @@ public abstract class MangaConnector : GlobalBase
//Create a temporary folder to store images
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
int chapter = 0;
int chapterNum = 0;
//Download all Images to temporary Folder
if (imageUrls.Length == 0)
{
@ -250,12 +274,15 @@ public abstract class MangaConnector : GlobalBase
progressToken?.Complete();
return HttpStatusCode.NoContent;
}
foreach (string imageUrl in imageUrls)
{
string extension = imageUrl.Split('.')[^1].Split('?')[0];
Log($"Downloading image {chapter + 1:000}/{imageUrls.Length:000}"); //TODO
HttpStatusCode status = DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer);
Log($"{saveArchiveFilePath} {chapter + 1:000}/{imageUrls.Length:000} {status}");
Log($"Downloading image {chapterNum + 1:000}/{imageUrls.Length:000}");
string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}");
HttpStatusCode status = DownloadImage(imageUrl, imagePath, requestType, referrer);
ProcessImage(imagePath);
Log($"{saveArchiveFilePath} {chapterNum + 1:000}/{imageUrls.Length:000} {status}");
if ((int)status < 200 || (int)status >= 300)
{
progressToken?.Complete();
@ -269,16 +296,14 @@ public abstract class MangaConnector : GlobalBase
progressToken?.Increment();
}
if(comicInfoPath is not null){
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
File.Delete(comicInfoPath); //Delete tmp-file
}
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
Log($"Creating archive {saveArchiveFilePath}");
//ZIP-it and ship-it
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute);
chapter.CreateChapterMarker();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
Directory.Delete(tempFolder, true); //Cleanup
progressToken?.Complete();

View File

@ -38,6 +38,7 @@ public class MangaConnectorJsonConverter : JsonConverter
"Manga4Life" => this._connectors.First(c => c is MangaLife),
"ManhuaPlus" => this._connectors.First(c => c is ManhuaPlus),
"MangaHere" => this._connectors.First(c => c is MangaHere),
"AsuraToon" => this._connectors.First(c => c is AsuraToon),
_ => throw new UnreachableException($"Could not find Connector with name {connectorName}")
};
}

View File

@ -10,7 +10,7 @@ public class MangaDex : MangaConnector
//https://api.mangadex.org/docs/3-enumerations/#language-codes--localization
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
//https://gist.github.com/Josantonius/b455e315bc7f790d14b136d61d9ae469
public MangaDex(GlobalBase clone) : base(clone, "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"])
public MangaDex(GlobalBase clone) : base(clone, "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"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
@ -129,10 +129,10 @@ public class MangaDex : MangaConnector
false => null
};
Manga.ReleaseStatusByte status = Manga.ReleaseStatusByte.Unreleased;
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
if (attributes.TryGetPropertyValue("status", out JsonNode? statusNode))
{
status = statusNode?.GetValue<string>().ToLower() switch
releaseStatus = statusNode?.GetValue<string>().ToLower() switch
{
"ongoing" => Manga.ReleaseStatusByte.Continuing,
"completed" => Manga.ReleaseStatusByte.Completed,
@ -176,6 +176,7 @@ public class MangaDex : MangaConnector
}
Manga pub = new(
this,
title,
authors,
description,
@ -187,8 +188,8 @@ public class MangaDex : MangaConnector
year,
originalLanguage,
publicationId,
status,
websiteUrl: $"https://mangadex.org/title/{publicationId}"
releaseStatus,
$"https://mangadex.org/title/{publicationId}"
);
AddMangaToCache(pub);
return pub;
@ -247,7 +248,7 @@ public class MangaDex : MangaConnector
}
if(chapterNum is not "null" && !chapters.Any(chp => chp.volumeNumber.Equals(volume) && chp.chapterNumber.Equals(chapterNum)))
chapters.Add(new Chapter(manga, title, volume, chapterNum, chapterId));
chapters.Add(new Chapter(manga, title, volume, chapterNum, chapterId, chapterId));
}
}
@ -288,11 +289,8 @@ public class MangaDex : MangaConnector
HashSet<string> imageUrls = new();
foreach (JsonNode? image in imageFileNames)
imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}");
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
//Download Chapter-Images
return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(), RequestType.MangaImage, comicInfoPath, progressToken:progressToken);
return DownloadChapterImages(imageUrls.ToArray(), chapter, RequestType.MangaImage, progressToken:progressToken);
}
}

View File

@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors;
public class MangaHere : MangaConnector
{
public MangaHere(GlobalBase clone) : base(clone, "MangaHere", ["en"])
public MangaHere(GlobalBase clone) : base(clone, "MangaHere", ["en"], ["www.mangahere.cc"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
@ -101,7 +101,7 @@ public class MangaHere : MangaConnector
.SelectSingleNode("//p[contains(concat(' ',normalize-space(@class),' '),' fullcontent ')]");
string description = descriptionNode.InnerText;
Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links,
null, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
@ -117,7 +117,7 @@ public class MangaHere : MangaConnector
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-2']/ul//li//a[contains(@href, '/manga/')]")
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\.]+)\/.*");
@ -181,12 +181,9 @@ public class MangaHere : MangaConnector
}
} while (downloaded++ <= images);
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
if (progressToken is not null)
progressToken.increments = images;//we blip to normal length, in downloadchapterimages it is increasaed by the amount of urls again
return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(), RequestType.MangaImage, comicInfoPath, progressToken:progressToken);
return DownloadChapterImages(imageUrls.ToArray(), chapter, RequestType.MangaImage, progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)

View File

@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors;
public class MangaKatana : MangaConnector
{
public MangaKatana(GlobalBase clone) : base(clone, "MangaKatana", ["en"])
public MangaKatana(GlobalBase clone) : base(clone, "MangaKatana", ["en"], ["mangakatana.com"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
@ -141,8 +141,8 @@ public class MangaKatana : MangaConnector
year = Convert.ToInt32(yearString);
}
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
@ -213,11 +213,8 @@ public class MangaKatana : MangaConnector
}
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(), RequestType.MangaImage, comicInfoPath, "https://mangakatana.com/", progressToken:progressToken);
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(string mangaUrl)

View File

@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors;
public class MangaLife : MangaConnector
{
public MangaLife(GlobalBase clone) : base(clone, "Manga4Life", ["en"])
public MangaLife(GlobalBase clone) : base(clone, "Manga4Life", ["en"], ["manga4life.com"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
@ -121,8 +121,8 @@ public class MangaLife : MangaConnector
.Descendants("div").First();
string description = descriptionNode.InnerText;
Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
@ -194,6 +194,6 @@ public class MangaLife : MangaConnector
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(), RequestType.MangaImage, comicInfoPath, progressToken:progressToken);
return DownloadChapterImages(urls.ToArray(), chapter, RequestType.MangaImage, progressToken:progressToken);
}
}

View File

@ -8,7 +8,7 @@ namespace Tranga.MangaConnectors;
public class Manganato : MangaConnector
{
public Manganato(GlobalBase clone) : base(clone, "Manganato", ["en"])
public Manganato(GlobalBase clone) : base(clone, "Manganato", ["en"], ["manganato.com"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
@ -139,8 +139,8 @@ public class Manganato : MangaConnector
int year = DateTime.ParseExact(oldestChapter.GetAttributeValue("title", "Dec 31 2400, 23:59"), pattern,
CultureInfo.InvariantCulture).Year;
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
@ -214,10 +214,7 @@ public class Manganato : MangaConnector
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(), RequestType.MangaImage, comicInfoPath, "https://chapmanganato.com/", progressToken:progressToken);
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, "https://chapmanganato.com/", progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)

View File

@ -11,7 +11,7 @@ namespace Tranga.MangaConnectors;
public class Mangasee : MangaConnector
{
public Mangasee(GlobalBase clone) : base(clone, "Mangasee", ["en"])
public Mangasee(GlobalBase clone) : base(clone, "Mangasee", ["en"], ["mangasee123.com"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
@ -152,9 +152,8 @@ public class Mangasee : MangaConnector
.Descendants("div").First();
string description = descriptionNode.InnerText;
Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
@ -221,10 +220,7 @@ public class Mangasee : MangaConnector
List<string> urls = new();
foreach(HtmlNode galleryImage in images)
urls.Add(galleryImage.GetAttributeValue("src", ""));
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(), RequestType.MangaImage, comicInfoPath, progressToken:progressToken);
return DownloadChapterImages(urls.ToArray(), chapter, RequestType.MangaImage, progressToken:progressToken);
}
}

View File

@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors;
public class Mangaworld: MangaConnector
{
public Mangaworld(GlobalBase clone) : base(clone, "Mangaworld", ["it"])
public Mangaworld(GlobalBase clone) : base(clone, "Mangaworld", ["it"], ["www.mangaworld.ac"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
@ -118,8 +118,8 @@ public class Mangaworld: MangaConnector
string yearString = metadata.SelectSingleNode("//span[text()='Anno di uscita: ']/..").SelectNodes("a").First().InnerText;
int year = Convert.ToInt32(yearString);
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
@ -149,19 +149,21 @@ public class Mangaworld: MangaConnector
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]+).*");
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 volume = Regex.Match(volNode.SelectNodes("div").First(node => node.HasClass("volume")).SelectSingleNode("p").InnerText,
@"[Vv]olume ([0-9]+).*").Groups[1].Value;
string volume = volumeRex.Match(volNode.SelectNodes("div").First(node => node.HasClass("volume")).SelectSingleNode("p").InnerText).Groups[1].Value;
foreach (HtmlNode chNode in volNode.SelectNodes("div").First(node => node.HasClass("volume-chapters")).SelectNodes("div"))
{
string number = Regex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText,
@"[Cc]apitolo ([0-9]+).*").Groups[1].Value;
string number = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
ret.Add(new Chapter(manga, null, volume, number, url));
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
ret.Add(new Chapter(manga, null, volume, number, url, id));
}
}
}
@ -169,10 +171,10 @@ public class Mangaworld: MangaConnector
{
foreach (HtmlNode chNode in chaptersWrapper.SelectNodes("div").Where(node => node.HasClass("chapter")))
{
string number = Regex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText,
@"[Cc]apitolo ([0-9]+).*").Groups[1].Value;
string number = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
ret.Add(new Chapter(manga, null, null, number, url));
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
ret.Add(new Chapter(manga, null, null, number, url, id));
}
}
@ -207,10 +209,7 @@ public class Mangaworld: MangaConnector
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(), RequestType.MangaImage, comicInfoPath, "https://www.mangaworld.bz/", progressToken:progressToken);
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage,"https://www.mangaworld.bz/", progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)

View File

@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors;
public class ManhuaPlus : MangaConnector
{
public ManhuaPlus(GlobalBase clone) : base(clone, "ManhuaPlus", ["en"])
public ManhuaPlus(GlobalBase clone) : base(clone, "ManhuaPlus", ["en"], ["manhuaplus.org"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
@ -108,9 +108,10 @@ public class ManhuaPlus : MangaConnector
Log("No genres found");
}
string yearNodeStr = document.DocumentNode
.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-clock ')]/../span").InnerText.Replace("\n", "");
int year = int.Parse(yearNodeStr.Split(' ')[0].Split('/')[^1]);
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);
int year = match.Success && match.Groups[1].Success ? int.Parse(match.Groups[1].Value) : 1960;
status = document.DocumentNode.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-rss ')]/../span").InnerText.Replace("\n", "");
switch (status.ToLower())
@ -126,7 +127,7 @@ public class ManhuaPlus : MangaConnector
.SelectSingleNode("//div[@id='syn-target']");
string description = descriptionNode.InnerText;
Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
@ -189,10 +190,7 @@ public class ManhuaPlus : MangaConnector
HtmlNode[] images = document.DocumentNode.SelectNodes("//a[contains(concat(' ',normalize-space(@class),' '),' readImg ')]/img").ToArray();
List<string> urls = images.Select(node => node.GetAttributeValue("src", "")).ToList();
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(), RequestType.MangaImage, comicInfoPath, progressToken:progressToken);
return DownloadChapterImages(urls.ToArray(), chapter, RequestType.MangaImage, progressToken:progressToken);
}
}

View File

@ -1,763 +0,0 @@
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Tranga.Jobs;
using Tranga.LibraryConnectors;
using Tranga.MangaConnectors;
using Tranga.NotificationConnectors;
namespace Tranga;
public class Server : GlobalBase
{
private readonly HttpListener _listener = new ();
private readonly Tranga _parent;
public Server(Tranga parent) : base(parent)
{
this._parent = parent;
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
this._listener.Prefixes.Add($"http://*:{TrangaSettings.apiPortNumber}/");
else
this._listener.Prefixes.Add($"http://localhost:{TrangaSettings.apiPortNumber}/");
Thread listenThread = new (Listen);
listenThread.Start();
Thread watchThread = new(WatchRunning);
watchThread.Start();
}
private void WatchRunning()
{
while(_parent.keepRunning)
Thread.Sleep(1000);
this._listener.Close();
}
private void Listen()
{
this._listener.Start();
foreach(string prefix in this._listener.Prefixes)
Log($"Listening on {prefix}");
while (this._listener.IsListening && _parent.keepRunning)
{
try
{
HttpListenerContext context = this._listener.GetContext();
//Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}");
Task t = new(() =>
{
HandleRequest(context);
});
t.Start();
}
catch (HttpListenerException)
{
}
}
}
private void HandleRequest(HttpListenerContext context)
{
HttpListenerRequest request = context.Request;
HttpListenerResponse response = context.Response;
if (request.Url!.LocalPath.Contains("favicon"))
{
SendResponse(HttpStatusCode.NoContent, response);
return;
}
switch (request.HttpMethod)
{
case "GET":
HandleGet(request, response);
break;
case "POST":
HandlePost(request, response);
break;
case "DELETE":
HandleDelete(request, response);
break;
case "OPTIONS":
SendResponse(HttpStatusCode.OK, context.Response);
break;
default:
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
}
private Dictionary<string, string> GetRequestVariables(string query)
{
Dictionary<string, string> ret = new();
Regex queryRex = new (@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*");
if (!queryRex.IsMatch(query))
return ret;
query = query.Substring(1);
foreach (string keyValuePair in query.Split('&').Where(str => str.Length >= 3))
{
string var = keyValuePair.Split('=')[0];
string val = Regex.Replace(keyValuePair.Substring(var.Length + 1), "%20", " ");
val = Regex.Replace(val, "%[0-9]{2}", "");
ret.Add(var, val);
}
return ret;
}
private void HandleGet(HttpListenerRequest request, HttpListenerResponse response)
{
Dictionary<string, string> requestVariables = GetRequestVariables(request.Url!.Query);
string? connectorName, jobId, internalId;
MangaConnector? connector;
Manga? manga;
string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
switch (path)
{
case "Connectors":
SendResponse(HttpStatusCode.OK, response, _parent.GetConnectors().Select(con => con.name).ToArray());
break;
case "Manga/Cover":
if (!requestVariables.TryGetValue("internalId", out internalId) ||
!_parent.TryGetPublicationById(internalId, out manga))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
string filePath = manga?.coverFileNameInCache ?? "";
if (File.Exists(filePath))
{
FileStream coverStream = new(filePath, FileMode.Open);
SendResponse(HttpStatusCode.OK, response, coverStream);
}
else
{
SendResponse(HttpStatusCode.NotFound, response);
}
break;
case "Manga/FromConnector":
requestVariables.TryGetValue("title", out string? title);
requestVariables.TryGetValue("url", out string? url);
if (!requestVariables.TryGetValue("connector", out connectorName) ||
!_parent.TryGetConnector(connectorName, out connector) ||
(title is null && url is null))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
if (url is not null)
{
HashSet<Manga> ret = new();
manga = connector!.GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
SendResponse(HttpStatusCode.OK, response, ret);
}else
SendResponse(HttpStatusCode.OK, response, connector!.GetManga(title!));
break;
case "Manga/Chapters":
if(!requestVariables.TryGetValue("connector", out connectorName) ||
!requestVariables.TryGetValue("internalId", out internalId) ||
!_parent.TryGetConnector(connectorName, out connector) ||
!_parent.TryGetPublicationById(internalId, out manga))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
requestVariables.TryGetValue("translatedLanguage", out string? translatedLanguage);
SendResponse(HttpStatusCode.OK, response, connector!.GetChapters((Manga)manga!, translatedLanguage??"en"));
break;
case "Jobs":
if (!requestVariables.TryGetValue("jobId", out jobId))
{
if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId))
SendResponse(HttpStatusCode.BadRequest, response);
else
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId));
break;
}
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs);
break;
case "Jobs/Progress":
if (requestVariables.TryGetValue("jobId", out jobId))
{
if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId))
SendResponse(HttpStatusCode.BadRequest, response);
else
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId).progressToken);
break;
}
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Select(jjob => jjob.progressToken));
break;
case "Jobs/Running":
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Running));
break;
case "Jobs/Waiting":
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Standby).OrderBy(jjob => jjob.nextExecution));
break;
case "Jobs/MonitorJobs":
SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob is DownloadNewChapters).OrderBy(jjob => ((DownloadNewChapters)jjob).manga.sortName));
break;
case "Settings":
SendResponse(HttpStatusCode.OK, response, TrangaSettings.AsJObject());
break;
case "Settings/userAgent":
SendResponse(HttpStatusCode.OK, response, TrangaSettings.userAgent);
break;
case "Settings/customRequestLimit":
SendResponse(HttpStatusCode.OK, response, TrangaSettings.requestLimits);
break;
case "Settings/AprilFoolsMode":
SendResponse(HttpStatusCode.OK, response, TrangaSettings.aprilFoolsMode);
break;
case "NotificationConnectors":
SendResponse(HttpStatusCode.OK, response, notificationConnectors);
break;
case "NotificationConnectors/Types":
SendResponse(HttpStatusCode.OK, response,
Enum.GetValues<NotificationConnector.NotificationConnectorType>().Select(nc => new KeyValuePair<byte, string?>((byte)nc, Enum.GetName(nc))));
break;
case "LibraryConnectors":
SendResponse(HttpStatusCode.OK, response, libraryConnectors);
break;
case "LibraryConnectors/Types":
SendResponse(HttpStatusCode.OK, response,
Enum.GetValues<LibraryConnector.LibraryType>().Select(lc => new KeyValuePair<byte, string?>((byte)lc, Enum.GetName(lc))));
break;
case "Ping":
SendResponse(HttpStatusCode.OK, response, "Pong");
break;
case "LogMessages":
if (logger is null || !File.Exists(logger?.logFilePath))
{
SendResponse(HttpStatusCode.NotFound, response);
break;
}
if (requestVariables.TryGetValue("count", out string? count))
{
try
{
uint messageCount = uint.Parse(count);
SendResponse(HttpStatusCode.OK, response, logger.Tail(messageCount));
}
catch (FormatException f)
{
SendResponse(HttpStatusCode.InternalServerError, response, f);
}
}else
SendResponse(HttpStatusCode.OK, response, logger.GetLog());
break;
case "LogFile":
if (logger is null || !File.Exists(logger?.logFilePath))
{
SendResponse(HttpStatusCode.NotFound, response);
break;
}
string logDir = new FileInfo(logger.logFilePath).DirectoryName!;
string tmpFilePath = Path.Join(logDir, "Tranga.log");
File.Copy(logger.logFilePath, tmpFilePath);
SendResponse(HttpStatusCode.OK, response, new FileStream(tmpFilePath, FileMode.Open));
File.Delete(tmpFilePath);
break;
default:
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
}
private void HandlePost(HttpListenerRequest request, HttpListenerResponse response)
{
Dictionary<string, string> requestVariables = GetRequestVariables(request.Url!.Query);
string? connectorName, internalId, jobId, chapterNumStr, customFolderName, translatedLanguage, notificationConnectorStr, libraryConnectorStr;
MangaConnector? connector;
Manga? tmpManga;
Manga manga;
Job? job;
NotificationConnector.NotificationConnectorType notificationConnectorType;
LibraryConnector.LibraryType libraryConnectorType;
string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
switch (path)
{
case "Manga":
if(!requestVariables.TryGetValue("internalId", out internalId) ||
!_parent.TryGetPublicationById(internalId, out tmpManga))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
manga = (Manga)tmpManga!;
SendResponse(HttpStatusCode.OK, response, manga);
break;
case "Jobs/MonitorManga":
if(!requestVariables.TryGetValue("connector", out connectorName) ||
!requestVariables.TryGetValue("internalId", out internalId) ||
!requestVariables.TryGetValue("interval", out string? intervalStr) ||
!_parent.TryGetConnector(connectorName, out connector)||
!_parent.TryGetPublicationById(internalId, out tmpManga) ||
!TimeSpan.TryParse(intervalStr, out TimeSpan interval))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
manga = (Manga)tmpManga!;
if (requestVariables.TryGetValue("ignoreBelowChapterNum", out chapterNumStr))
{
if (!float.TryParse(chapterNumStr, numberFormatDecimalPoint, out float chapterNum))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
manga.ignoreChaptersBelow = chapterNum;
}
if (requestVariables.TryGetValue("customFolderName", out customFolderName))
manga.MovePublicationFolder(TrangaSettings.downloadLocation, customFolderName);
requestVariables.TryGetValue("translatedLanguage", out translatedLanguage);
_parent.jobBoss.AddJob(new DownloadNewChapters(this, connector!, manga, true, interval, translatedLanguage: translatedLanguage??"en"));
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Jobs/DownloadNewChapters":
if(!requestVariables.TryGetValue("connector", out connectorName) ||
!requestVariables.TryGetValue("internalId", out internalId) ||
!_parent.TryGetConnector(connectorName, out connector)||
!_parent.TryGetPublicationById(internalId, out tmpManga))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
manga = (Manga)tmpManga!;
if (requestVariables.TryGetValue("ignoreBelowChapterNum", out chapterNumStr))
{
if (!float.TryParse(chapterNumStr, numberFormatDecimalPoint, out float chapterNum))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
manga.ignoreChaptersBelow = chapterNum;
}
if (requestVariables.TryGetValue("customFolderName", out customFolderName))
manga.MovePublicationFolder(TrangaSettings.downloadLocation, customFolderName);
requestVariables.TryGetValue("translatedLanguage", out translatedLanguage);
_parent.jobBoss.AddJob(new DownloadNewChapters(this, connector!, manga, false, translatedLanguage: translatedLanguage??"en"));
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Jobs/UpdateMetadata":
if (!requestVariables.TryGetValue("internalId", out internalId))
{
foreach (Job pJob in _parent.jobBoss.jobs.Where(possibleDncJob =>
possibleDncJob.jobType is Job.JobType.DownloadNewChaptersJob).ToArray())//ToArray to avoid modyifying while adding new jobs
{
DownloadNewChapters dncJob = pJob as DownloadNewChapters ??
throw new Exception("Has to be DownloadNewChapters Job");
_parent.jobBoss.AddJob(new UpdateMetadata(this, dncJob.mangaConnector, dncJob.manga));
}
SendResponse(HttpStatusCode.Accepted, response);
}
else
{
Job[] possibleDncJobs = _parent.jobBoss.GetJobsLike(internalId: internalId).ToArray();
switch (possibleDncJobs.Length)
{
case <1: SendResponse(HttpStatusCode.BadRequest, response, "Could not find matching release"); break;
case >1: SendResponse(HttpStatusCode.BadRequest, response, "Multiple releases??"); break;
default:
DownloadNewChapters dncJob = possibleDncJobs[0] as DownloadNewChapters ??
throw new Exception("Has to be DownloadNewChapters Job");
_parent.jobBoss.AddJob(new UpdateMetadata(this, dncJob.mangaConnector, dncJob.manga));
SendResponse(HttpStatusCode.Accepted, response);
break;
}
}
break;
case "Jobs/StartNow":
if (!requestVariables.TryGetValue("jobId", out jobId) ||
!_parent.jobBoss.TryGetJobById(jobId, out job))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
_parent.jobBoss.AddJobToQueue(job!);
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Jobs/Cancel":
if (!requestVariables.TryGetValue("jobId", out jobId) ||
!_parent.jobBoss.TryGetJobById(jobId, out job))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
job!.Cancel();
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Settings/UpdateDownloadLocation":
if (!requestVariables.TryGetValue("downloadLocation", out string? downloadLocation) ||
!requestVariables.TryGetValue("moveFiles", out string? moveFilesStr) ||
!bool.TryParse(moveFilesStr, out bool moveFiles))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
TrangaSettings.UpdateDownloadLocation(downloadLocation, moveFiles);
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Settings/AprilFoolsMode":
if (!requestVariables.TryGetValue("enabled", out string? aprilFoolsModeEnabledStr) ||
!bool.TryParse(aprilFoolsModeEnabledStr, out bool aprilFoolsModeEnabled))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
TrangaSettings.UpdateAprilFoolsMode(aprilFoolsModeEnabled);
SendResponse(HttpStatusCode.Accepted, response);
break;
/*case "Settings/UpdateWorkingDirectory":
if (!requestVariables.TryGetValue("workingDirectory", out string? workingDirectory))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
settings.UpdateWorkingDirectory(workingDirectory);
SendResponse(HttpStatusCode.Accepted, response);
break;*/
case "Settings/userAgent":
if(!requestVariables.TryGetValue("userAgent", out string? customUserAgent))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
TrangaSettings.UpdateUserAgent(customUserAgent);
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Settings/userAgent/Reset":
TrangaSettings.UpdateUserAgent(null);
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Settings/customRequestLimit":
if (!requestVariables.TryGetValue("requestType", out string? requestTypeStr) ||
!requestVariables.TryGetValue("requestsPerMinute", out string? requestsPerMinuteStr) ||
!Enum.TryParse(requestTypeStr, out RequestType requestType) ||
!int.TryParse(requestsPerMinuteStr, out int requestsPerMinute))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
TrangaSettings.UpdateRateLimit(requestType, requestsPerMinute);
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Settings/customRequestLimit/Reset":
TrangaSettings.ResetRateLimits();
break;
case "NotificationConnectors/Update":
if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) ||
!Enum.TryParse(notificationConnectorStr, out notificationConnectorType))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify)
{
if (!requestVariables.TryGetValue("gotifyUrl", out string? gotifyUrl) ||
!requestVariables.TryGetValue("gotifyAppToken", out string? gotifyAppToken))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
AddNotificationConnector(new Gotify(this, gotifyUrl, gotifyAppToken));
SendResponse(HttpStatusCode.Accepted, response);
}else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea)
{
if (!requestVariables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
AddNotificationConnector(new LunaSea(this, lunaseaWebhook));
SendResponse(HttpStatusCode.Accepted, response);
}else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy)
{
if (!requestVariables.TryGetValue("ntfyUrl", out string? ntfyUrl) ||
!requestVariables.TryGetValue("ntfyUser", out string? ntfyUser)||
!requestVariables.TryGetValue("ntfyPass", out string? ntfyPass))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
AddNotificationConnector(new Ntfy(this, ntfyUrl, ntfyUser, ntfyPass, null));
SendResponse(HttpStatusCode.Accepted, response);
}
else
{
SendResponse(HttpStatusCode.BadRequest, response);
}
break;
case "NotificationConnectors/Test":
NotificationConnector notificationConnector;
if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) ||
!Enum.TryParse(notificationConnectorStr, out notificationConnectorType))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify)
{
if (!requestVariables.TryGetValue("gotifyUrl", out string? gotifyUrl) ||
!requestVariables.TryGetValue("gotifyAppToken", out string? gotifyAppToken))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
notificationConnector = new Gotify(this, gotifyUrl, gotifyAppToken);
}else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea)
{
if (!requestVariables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
notificationConnector = new LunaSea(this, lunaseaWebhook);
}else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy)
{
if (!requestVariables.TryGetValue("ntfyUrl", out string? ntfyUrl) ||
!requestVariables.TryGetValue("ntfyUser", out string? ntfyUser)||
!requestVariables.TryGetValue("ntfyPass", out string? ntfyPass))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
notificationConnector = new Ntfy(this, ntfyUrl, ntfyUser, ntfyPass, null);
}
else
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
notificationConnector.SendNotification("Tranga Test", "This is Test-Notification.");
SendResponse(HttpStatusCode.Accepted, response);
break;
case "NotificationConnectors/Reset":
if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) ||
!Enum.TryParse(notificationConnectorStr, out notificationConnectorType))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
DeleteNotificationConnector(notificationConnectorType);
SendResponse(HttpStatusCode.Accepted, response);
break;
case "LibraryConnectors/Update":
if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) ||
!Enum.TryParse(libraryConnectorStr, out libraryConnectorType))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
if (libraryConnectorType is LibraryConnector.LibraryType.Kavita)
{
if (!requestVariables.TryGetValue("kavitaUrl", out string? kavitaUrl) ||
!requestVariables.TryGetValue("kavitaUsername", out string? kavitaUsername) ||
!requestVariables.TryGetValue("kavitaPassword", out string? kavitaPassword))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
AddLibraryConnector(new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword));
SendResponse(HttpStatusCode.Accepted, response);
}else if (libraryConnectorType is LibraryConnector.LibraryType.Komga)
{
if (!requestVariables.TryGetValue("komgaUrl", out string? komgaUrl) ||
!requestVariables.TryGetValue("komgaAuth", out string? komgaAuth))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
AddLibraryConnector(new Komga(this, komgaUrl, komgaAuth));
SendResponse(HttpStatusCode.Accepted, response);
}
else
{
SendResponse(HttpStatusCode.BadRequest, response);
}
break;
case "LibraryConnectors/Test":
LibraryConnector libraryConnector;
if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) ||
!Enum.TryParse(libraryConnectorStr, out libraryConnectorType))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
if (libraryConnectorType is LibraryConnector.LibraryType.Kavita)
{
if (!requestVariables.TryGetValue("kavitaUrl", out string? kavitaUrl) ||
!requestVariables.TryGetValue("kavitaUsername", out string? kavitaUsername) ||
!requestVariables.TryGetValue("kavitaPassword", out string? kavitaPassword))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
libraryConnector = new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword);
}else if (libraryConnectorType is LibraryConnector.LibraryType.Komga)
{
if (!requestVariables.TryGetValue("komgaUrl", out string? komgaUrl) ||
!requestVariables.TryGetValue("komgaAuth", out string? komgaAuth))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
libraryConnector = new Komga(this, komgaUrl, komgaAuth);
}
else
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
libraryConnector.UpdateLibrary();
SendResponse(HttpStatusCode.Accepted, response);
break;
case "LibraryConnectors/Reset":
if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) ||
!Enum.TryParse(libraryConnectorStr, out libraryConnectorType))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
DeleteLibraryConnector(libraryConnectorType);
SendResponse(HttpStatusCode.Accepted, response);
break;
default:
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
}
private void HandleDelete(HttpListenerRequest request, HttpListenerResponse response)
{
Dictionary<string, string> requestVariables = GetRequestVariables(request.Url!.Query);
string? connectorName, internalId;
MangaConnector connector;
Manga manga;
string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
switch (path)
{
case "Jobs":
if (!requestVariables.TryGetValue("jobId", out string? jobId) ||
!_parent.jobBoss.TryGetJobById(jobId, out Job? job))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
_parent.jobBoss.RemoveJob(job!);
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Jobs/DownloadNewChapters":
if(!requestVariables.TryGetValue("connector", out connectorName) ||
!requestVariables.TryGetValue("internalId", out internalId) ||
_parent.GetConnector(connectorName) is null ||
_parent.GetPublicationById(internalId) is null)
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
connector = _parent.GetConnector(connectorName)!;
manga = (Manga)_parent.GetPublicationById(internalId)!;
_parent.jobBoss.RemoveJobs(_parent.jobBoss.GetJobsLike(connector, manga));
SendResponse(HttpStatusCode.Accepted, response);
break;
case "NotificationConnectors":
if (!requestVariables.TryGetValue("notificationConnector", out string? notificationConnectorStr) ||
!Enum.TryParse(notificationConnectorStr, out NotificationConnector.NotificationConnectorType notificationConnectorType))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
DeleteNotificationConnector(notificationConnectorType);
SendResponse(HttpStatusCode.Accepted, response);
break;
case "LibraryConnectors":
if (!requestVariables.TryGetValue("libraryConnectors", out string? libraryConnectorStr) ||
!Enum.TryParse(libraryConnectorStr,
out LibraryConnector.LibraryType libraryConnectoryType))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
DeleteLibraryConnector(libraryConnectoryType);
SendResponse(HttpStatusCode.Accepted, response);
break;
default:
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
}
private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
{
//Log($"Response: {statusCode} {content}");
response.StatusCode = (int)statusCode;
response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
response.AddHeader("Access-Control-Max-Age", "1728000");
response.AppendHeader("Access-Control-Allow-Origin", "*");
try
{
if (content is not Stream)
{
response.ContentType = "application/json";
response.AddHeader("Cache-Control", "no-store");
response.OutputStream.Write(content is not null
? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))
: Array.Empty<byte>());
response.OutputStream.Close();
}
else if (content is FileStream stream)
{
string contentType = stream.Name.Split('.')[^1];
response.AddHeader("Cache-Control", "max-age=600");
switch (contentType.ToLower())
{
case "gif":
response.ContentType = "image/gif";
break;
case "png":
response.ContentType = "image/png";
break;
case "jpg":
case "jpeg":
response.ContentType = "image/jpeg";
break;
case "log":
response.ContentType = "text/plain";
break;
}
stream.CopyTo(response.OutputStream);
response.OutputStream.Close();
stream.Close();
}
}
catch (Exception e)
{
Log(e.ToString());
}
}
}

View File

@ -0,0 +1,19 @@
using System.Net;
using System.Text.RegularExpressions;
namespace Tranga.Server;
internal struct RequestPath
{
internal readonly string HttpMethod;
internal readonly string RegexStr;
internal readonly Func<GroupCollection, Dictionary<string, string>, ValueTuple<HttpStatusCode, object?>> Method;
public RequestPath(string httpHttpMethod, string regexStr,
Func<GroupCollection, Dictionary<string, string>, ValueTuple<HttpStatusCode, object?>> method)
{
this.HttpMethod = httpHttpMethod;
this.RegexStr = regexStr + "(?:/?)";
this.Method = method;
}
}

269
Tranga/Server/Server.cs Normal file
View File

@ -0,0 +1,269 @@
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using ZstdSharp;
namespace Tranga.Server;
public partial class Server : GlobalBase, IDisposable
{
private readonly HttpListener _listener = new();
private readonly Tranga _parent;
private bool _running = true;
private readonly List<RequestPath> _apiRequestPaths;
public Server(Tranga parent) : base(parent)
{
/*
* Contains all valid Request Methods, Paths (with Regex Group Matching for specific Parameters) and Handling Methods
*/
_apiRequestPaths = new List<RequestPath>
{
new ("GET", @"/v2/Connector/Types", GetV2ConnectorTypes),
new ("GET", @"/v2/Connector/([a-zA-Z]+)/GetManga", GetV2ConnectorConnectorNameGetManga),
new ("GET", @"/v2/Mangas", GetV2Mangas),
new ("GET", @"/v2/Manga/Search", GetV2MangaSearch),
new ("GET", @"/v2/Manga", GetV2Manga),
new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Cover", GetV2MangaInternalIdCover),
new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters", GetV2MangaInternalIdChapters),
new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters/Latest", GetV2MangaInternalIdChaptersLatest),
new ("POST", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/ignoreChaptersBelow", PostV2MangaInternalIdIgnoreChaptersBelow),
new ("POST", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/moveFolder", PostV2MangaInternalIdMoveFolder),
new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", GetV2MangaInternalId),
new ("DELETE", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", DeleteV2MangaInternalId),
new ("GET", @"/v2/Jobs", GetV2Jobs),
new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning),
new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting),
new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring),
new ("GET", @"/v2/Jobs/Standby", GetV2JobsStandby),
new ("GET", @"/v2/Job/Types", GetV2JobTypes),
new ("POST", @"/v2/Job/Create/([a-zA-Z]+)", PostV2JobCreateType),
new ("GET", @"/v2/Job", GetV2Job),
new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Progress", GetV2JobJobIdProgress),
new ("POST", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/StartNow", PostV2JobJobIdStartNow),
new ("POST", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Cancel", PostV2JobJobIdCancel),
new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId),
new ("DELETE", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", DeleteV2JobJobId),
new ("GET", @"/v2/Settings", GetV2Settings),
new ("GET", @"/v2/Settings/UserAgent", GetV2SettingsUserAgent),
new ("POST", @"/v2/Settings/UserAgent", PostV2SettingsUserAgent),
new ("GET", @"/v2/Settings/RateLimit/Types", GetV2SettingsRateLimitTypes),
new ("GET", @"/v2/Settings/RateLimit", GetV2SettingsRateLimit),
new ("POST", @"/v2/Settings/RateLimit", PostV2SettingsRateLimit),
new ("GET", @"/v2/Settings/RateLimit/([a-zA-Z]+)", GetV2SettingsRateLimitType),
new ("POST", @"/v2/Settings/RateLimit/([a-zA-Z]+)", PostV2SettingsRateLimitType),
new ("GET", @"/v2/Settings/AprilFoolsMode", GetV2SettingsAprilFoolsMode),
new ("POST", @"/v2/Settings/AprilFoolsMode", PostV2SettingsAprilFoolsMode),
new ("GET", @"/v2/Settings/CompressImages", GetV2SettingsCompressImages),
new ("POST", @"/v2/Settings/CompressImages", PostV2SettingsCompressImages),
new ("GET", @"/v2/Settings/BWImages", GetV2SettingsBwImages),
new ("POST", @"/v2/Settings/BWImages", PostV2SettingsBwImages),
new ("POST", @"/v2/Settings/DownloadLocation", PostV2SettingsDownloadLocation),
new ("GET", @"/v2/LibraryConnector", GetV2LibraryConnector),
new ("GET", @"/v2/LibraryConnector/Types", GetV2LibraryConnectorTypes),
new ("GET", @"/v2/LibraryConnector/([a-zA-Z]+)", GetV2LibraryConnectorType),
new ("POST", @"/v2/LibraryConnector/([a-zA-Z]+)", PostV2LibraryConnectorType),
new ("POST", @"/v2/LibraryConnector/([a-zA-Z]+)/Test", PostV2LibraryConnectorTypeTest),
new ("DELETE", @"/v2/LibraryConnector/([a-zA-Z]+)", DeleteV2LibraryConnectorType),
new ("GET", @"/v2/NotificationConnector", GetV2NotificationConnector),
new ("GET", @"/v2/NotificationConnector/Types", GetV2NotificationConnectorTypes),
new ("GET", @"/v2/NotificationConnector/([a-zA-Z]+)", GetV2NotificationConnectorType),
new ("POST", @"/v2/NotificationConnector/([a-zA-Z]+)", PostV2NotificationConnectorType),
new ("POST", @"/v2/NotificationConnector/([a-zA-Z]+)/Test", PostV2NotificationConnectorTypeTest),
new ("DELETE", @"/v2/NotificationConnector/([a-zA-Z]+)", DeleteV2NotificationConnectorType),
new ("GET", @"/v2/LogFile", GetV2LogFile),
new ("GET", @"/v2/Ping", GetV2Ping),
new ("POST", @"/v2/Ping", PostV2Ping)
};
this._parent = parent;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
this._listener.Prefixes.Add($"http://*:{TrangaSettings.apiPortNumber}/");
else
this._listener.Prefixes.Add($"http://localhost:{TrangaSettings.apiPortNumber}/");
Thread listenThread = new(Listen);
listenThread.Start();
while(_parent.keepRunning && _running)
Thread.Sleep(100);
this.Dispose();
}
private void Listen()
{
this._listener.Start();
foreach (string prefix in this._listener.Prefixes)
Log($"Listening on {prefix}");
while (this._listener.IsListening && _parent.keepRunning)
{
try
{
HttpListenerContext context = this._listener.GetContext();
//Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}");
Task t = new(() =>
{
HandleRequest(context);
});
t.Start();
}
catch (HttpListenerException)
{
}
}
}
private void HandleRequest(HttpListenerContext context)
{
HttpListenerRequest request = context.Request;
HttpListenerResponse response = context.Response;
if (request.HttpMethod == "OPTIONS")
{
SendResponse(HttpStatusCode.NoContent, response);//Response always contains all valid Request-Methods
return;
}
if (request.Url!.LocalPath.Contains("favicon"))
{
SendResponse(HttpStatusCode.NoContent, response);
return;
}
string path = Regex.Match(request.Url.LocalPath, @"\/[a-zA-Z0-9\.+/=-]+(\/[a-zA-Z0-9\.+/=-]+)*").Value; //Local Path
if (!Regex.IsMatch(path, "/v2(/.*)?")) //Use only v2 API
{
SendResponse(HttpStatusCode.NotFound, response, "Use Version 2 API");
return;
}
Dictionary<string, string> requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI
Dictionary<string, string> requestBody = GetRequestBody(request); //Variables in the JSON body
Dictionary<string, string> requestParams = requestVariables.UnionBy(requestBody, v => v.Key)
.ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API
ValueTuple<HttpStatusCode, object?> responseMessage; //Used to respond to the HttpRequest
if (_apiRequestPaths.Any(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length)) //Check if Request-Path is valid
{
RequestPath requestPath =
_apiRequestPaths.First(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length);
responseMessage =
requestPath.Method.Invoke(Regex.Match(path, requestPath.RegexStr).Groups, requestParams); //Get HttpResponse content
}
else
responseMessage = new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, "Unknown Request Path");
SendResponse(responseMessage.Item1, response, responseMessage.Item2);
}
private Dictionary<string, string> GetRequestVariables(string query)
{
Dictionary<string, string> ret = new();
Regex queryRex = new(@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*");
if (!queryRex.IsMatch(query))
return ret;
query = query.Substring(1);
foreach (string keyValuePair in query.Split('&').Where(str => str.Length >= 3))
{
string var = keyValuePair.Split('=')[0];
string val = Regex.Replace(keyValuePair.Substring(var.Length + 1), "%20", " ");
val = Regex.Replace(val, "%[0-9]{2}", "");
ret.Add(var, val);
}
return ret;
}
private Dictionary<string, string> GetRequestBody(HttpListenerRequest request)
{
if (!request.HasEntityBody)
{
//Nospam Log("No request body");
return new Dictionary<string, string>();
}
Stream body = request.InputStream;
Encoding encoding = request.ContentEncoding;
using StreamReader streamReader = new (body, encoding);
try
{
Dictionary<string, string> requestBody =
JsonConvert.DeserializeObject<Dictionary<string, string>>(streamReader.ReadToEnd())
?? new();
return requestBody;
}
catch (JsonException e)
{
Log(e.Message);
}
return new Dictionary<string, string>();
}
private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
{
//Log($"Response: {statusCode} {content}");
response.StatusCode = (int)statusCode;
response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
response.AddHeader("Access-Control-Max-Age", "1728000");
response.AddHeader("Access-Control-Allow-Origin", "*");
response.AddHeader("Content-Encoding", "zstd");
using CompressionStream compressor = new (response.OutputStream, 5);
try
{
if (content is Stream stream)
{
response.ContentType = "text/plain";
response.AddHeader("Cache-Control", "private, no-store");
stream.CopyTo(compressor);
stream.Close();
}else if (content is Image image)
{
response.ContentType = image.Metadata.DecodedImageFormat?.DefaultMimeType ?? PngFormat.Instance.DefaultMimeType;
response.AddHeader("Cache-Control", "public, max-age=3600");
response.AddHeader("Expires", $"{DateTime.Now.AddHours(1):ddd\\,\\ dd\\ MMM\\ yyyy\\ HH\\:mm\\:ss} GMT");
string lastModifiedStr = "";
if (image.Metadata.IptcProfile is not null)
{
DateTime date = DateTime.ParseExact(image.Metadata.IptcProfile.GetValues(IptcTag.CreatedDate).First().Value, "yyyyMMdd",null);
DateTime time = DateTime.ParseExact(image.Metadata.IptcProfile.GetValues(IptcTag.CreatedTime).First().Value, "HHmmssK",null);
lastModifiedStr = $"{date:ddd\\,\\ dd\\ MMM\\ yyyy} {time:HH\\:mm\\:ss} GMT";
}else if (image.Metadata.ExifProfile is not null)
{
DateTime datetime = DateTime.ParseExact(image.Metadata.ExifProfile.Values.FirstOrDefault(value => value.Tag == ExifTag.DateTime)?.ToString() ?? "2000:01:01 01:01:01", "yyyy:MM:dd HH:mm:ss", null);
lastModifiedStr = $"{datetime:ddd\\,\\ dd\\ MMM\\ yyyy\\ HH\\:mm\\:ss} GMT";
}
if(lastModifiedStr.Length>0)
response.AddHeader("Last-Modified", lastModifiedStr);
image.Save(compressor, image.Metadata.DecodedImageFormat ?? PngFormat.Instance);
image.Dispose();
}
else
{
response.ContentType = "application/json";
response.AddHeader("Cache-Control", "private, no-store");
if(content is not null)
new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))).CopyTo(compressor);
else
compressor.Write(Array.Empty<byte>());
}
compressor.Flush();
response.OutputStream.Close();
}
catch (HttpListenerException e)
{
Log(e.ToString());
}
}
public void Dispose()
{
_running = false;
((IDisposable)_listener).Dispose();
}
}

View File

@ -0,0 +1,31 @@
using System.Net;
using System.Text.RegularExpressions;
using Tranga.MangaConnectors;
namespace Tranga.Server;
public partial class Server
{
private ValueTuple<HttpStatusCode, object?> GetV2ConnectorTypes(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Accepted, _parent.GetConnectors());
}
private ValueTuple<HttpStatusCode, object?> GetV2ConnectorConnectorNameGetManga(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(groups.Count < 1 ||
!_parent.GetConnectors().Any(mangaConnector => mangaConnector.name == groups[1].Value)||
!_parent.TryGetConnector(groups[1].Value, out MangaConnector? connector) ||
connector is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, $"Connector '{groups[1].Value}' does not exist.");
if (requestParameters.TryGetValue("title", out string? title))
{
return (HttpStatusCode.OK, connector.GetManga(title));
}else if (requestParameters.TryGetValue("url", out string? url))
{
return (HttpStatusCode.OK, connector.GetMangaFromUrl(url));
}else
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Parameter 'title' or 'url' has to be set.");
}
}

176
Tranga/Server/v2Jobs.cs Normal file
View File

@ -0,0 +1,176 @@
using System.Net;
using System.Text.RegularExpressions;
using Tranga.Jobs;
using Tranga.MangaConnectors;
namespace Tranga.Server;
public partial class Server
{
private ValueTuple<HttpStatusCode, object?> GetV2Jobs(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, _parent.jobBoss.jobs.Select(job => job.id));
}
private ValueTuple<HttpStatusCode, object?> GetV2JobsRunning(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, _parent.jobBoss.jobs
.Where(job => job.progressToken.state is ProgressToken.State.Running)
.Select(job => job.id));
}
private ValueTuple<HttpStatusCode, object?> GetV2JobsWaiting(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, _parent.jobBoss.jobs
.Where(job => job.progressToken.state is ProgressToken.State.Waiting)
.Select(job => job.id));
}
private ValueTuple<HttpStatusCode, object?> GetV2JobsStandby(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, _parent.jobBoss.jobs
.Where(job => job.progressToken.state is ProgressToken.State.Standby)
.Select(job => job.id));
}
private ValueTuple<HttpStatusCode, object?> GetV2JobsMonitoring(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, _parent.jobBoss.jobs
.Where(job => job.jobType is Job.JobType.DownloadNewChaptersJob)
.Select(job => job.id));
}
private ValueTuple<HttpStatusCode, object?> GetV2JobTypes(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK,
Enum.GetValues<Job.JobType>().ToDictionary(b => (byte)b, b => Enum.GetName(b)));
}
private ValueTuple<HttpStatusCode, object?> PostV2JobCreateType(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!Enum.TryParse(groups[1].Value, true, out Job.JobType jobType))
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"JobType {groups[1].Value} does not exist.");
}
string? mangaId;
Manga? manga;
switch (jobType)
{
case Job.JobType.MonitorManga:
if(!requestParameters.TryGetValue("internalId", out mangaId) ||
!_parent.TryGetPublicationById(mangaId, out manga) ||
manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "'internalId' Parameter missing, or is not a valid ID.");
if(!requestParameters.TryGetValue("interval", out string? intervalStr) ||
!TimeSpan.TryParse(intervalStr, out TimeSpan interval))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "'interval' Parameter missing, or is not in correct format.");
requestParameters.TryGetValue("language", out string? language);
if (requestParameters.TryGetValue("customFolder", out string? folder))
manga.Value.MovePublicationFolder(TrangaSettings.downloadLocation, folder);
if (requestParameters.TryGetValue("startChapter", out string? startChapterStr) &&
float.TryParse(startChapterStr, out float startChapter))
{
Manga manga1 = manga.Value;
manga1.ignoreChaptersBelow = startChapter;
}
return _parent.jobBoss.AddJob(new DownloadNewChapters(this, ((Manga)manga).mangaConnector,
((Manga)manga).internalId, true, interval, language)) switch
{
true => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null),
false => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Conflict, "Job already exists."),
};
case Job.JobType.UpdateMetaDataJob:
if(!requestParameters.TryGetValue("internalId", out mangaId) ||
!_parent.TryGetPublicationById(mangaId, out manga) ||
manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID.");
return _parent.jobBoss.AddJob(new UpdateMetadata(this, ((Manga)manga).internalId)) switch
{
true => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null),
false => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Conflict, "Job already exists."),
};
case Job.JobType.DownloadNewChaptersJob: //TODO
case Job.JobType.DownloadChapterJob: //TODO
default: return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"JobType {Enum.GetName(jobType)} is not supported.");
}
}
private ValueTuple<HttpStatusCode, object?> GetV2JobJobId(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
job is null)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
}
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, job);
}
private ValueTuple<HttpStatusCode, object?> DeleteV2JobJobId(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
job is null)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
}
_parent.jobBoss.RemoveJob(job);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
private ValueTuple<HttpStatusCode, object?> GetV2JobJobIdProgress(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
job is null)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, $"Job with ID: '{groups[1].Value}' does not exist.");
}
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, job.progressToken);
}
private ValueTuple<HttpStatusCode, object?> PostV2JobJobIdStartNow(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
job is null)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
}
_parent.jobBoss.AddJobs(job.ExecuteReturnSubTasks(_parent.jobBoss));
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
private ValueTuple<HttpStatusCode, object?> PostV2JobJobIdCancel(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) ||
job is null)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist.");
}
job.Cancel();
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
private ValueTuple<HttpStatusCode, object?> GetV2Job(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(!requestParameters.TryGetValue("jobIds", out string? jobIdListStr))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Missing parameter 'jobIds'.");
string[] jobIdList = jobIdListStr.Split(',');
List<Job> ret = new();
foreach (string jobId in jobIdList)
{
if(!_parent.jobBoss.TryGetJobById(jobId, out Job? job) || job is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Job with id '{jobId}' not found.");
ret.Add(job);
}
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, ret);
}
}

View File

@ -0,0 +1,116 @@
using System.Net;
using System.Text.RegularExpressions;
using Tranga.LibraryConnectors;
namespace Tranga.Server;
public partial class Server
{
private ValueTuple<HttpStatusCode, object?> GetV2LibraryConnector(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, libraryConnectors);
}
private ValueTuple<HttpStatusCode, object?> GetV2LibraryConnectorTypes(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK,
Enum.GetValues<LibraryConnector.LibraryType>().ToDictionary(b => (byte)b, b => Enum.GetName(b)));
}
private ValueTuple<HttpStatusCode, object?> GetV2LibraryConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
}
if(libraryConnectors.All(lc => lc.libraryType != libraryType))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {Enum.GetName(libraryType)} not configured.");
else
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, libraryConnectors.First(lc => lc.libraryType == libraryType));
}
private ValueTuple<HttpStatusCode, object?> PostV2LibraryConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
}
if(!requestParameters.TryGetValue("url", out string? url))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
if(!requestParameters.TryGetValue("username", out string? username))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'username' missing.");
if(!requestParameters.TryGetValue("password", out string? password))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'password' missing.");
switch (libraryType)
{
case LibraryConnector.LibraryType.Kavita:
Kavita kavita = new (this, url, username, password);
libraryConnectors.RemoveWhere(lc => lc.libraryType == LibraryConnector.LibraryType.Kavita);
libraryConnectors.Add(kavita);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, kavita);
case LibraryConnector.LibraryType.Komga:
Komga komga = new (this, url, username, password);
libraryConnectors.RemoveWhere(lc => lc.libraryType == LibraryConnector.LibraryType.Komga);
libraryConnectors.Add(komga);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, komga);
default: return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"LibraryType {Enum.GetName(libraryType)} is not supported.");
}
}
private ValueTuple<HttpStatusCode, object?> PostV2LibraryConnectorTypeTest(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
}
if(!requestParameters.TryGetValue("url", out string? url))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
if(!requestParameters.TryGetValue("username", out string? username))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'username' missing.");
if(!requestParameters.TryGetValue("password", out string? password))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'password' missing.");
switch (libraryType)
{
case LibraryConnector.LibraryType.Kavita:
Kavita kavita = new (this, url, username, password);
return kavita.Test() switch
{
true => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, kavita),
_ => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.FailedDependency, kavita)
};
case LibraryConnector.LibraryType.Komga:
Komga komga = new (this, url, username, password);
return komga.Test() switch
{
true => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, komga),
_ => new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.FailedDependency, komga)
};
default: return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"LibraryType {Enum.GetName(libraryType)} is not supported.");
}
}
private ValueTuple<HttpStatusCode, object?> DeleteV2LibraryConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType))
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist.");
}
if(libraryConnectors.All(lc => lc.libraryType != libraryType))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"LibraryType {Enum.GetName(libraryType)} not configured.");
else
{
libraryConnectors.Remove(libraryConnectors.First(lc => lc.libraryType == libraryType));
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
}
}

166
Tranga/Server/v2Manga.cs Normal file
View File

@ -0,0 +1,166 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using System.Net;
using System.Text.RegularExpressions;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using Tranga.Jobs;
using Tranga.MangaConnectors;
namespace Tranga.Server;
public partial class Server
{
private ValueTuple<HttpStatusCode, object?> GetV2Mangas(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, GetAllCachedManga().Select(m => m.internalId));
}
private ValueTuple<HttpStatusCode, object?> GetV2Manga(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(!requestParameters.TryGetValue("mangaIds", out string? mangaIdListStr))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Missing parameter 'mangaIds'.");
string[] mangaIdList = mangaIdListStr.Split(',').Distinct().ToArray();
List<Manga> ret = new();
foreach (string mangaId in mangaIdList)
{
if(!_parent.TryGetPublicationById(mangaId, out Manga? manga) || manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with id '{mangaId}' not found.");
ret.Add(manga.Value);
}
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, ret);
}
private ValueTuple<HttpStatusCode, object?> GetV2MangaSearch(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(!requestParameters.TryGetValue("title", out string? title))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Missing parameter 'title'.");
List<Manga> ret = new();
List<Thread> threads = new();
foreach (MangaConnector mangaConnector in _connectors)
{
Thread t = new (() =>
{
ret.AddRange(mangaConnector.GetManga(title));
});
t.Start();
threads.Add(t);
}
while(threads.Any(t => t.ThreadState is ThreadState.Running or ThreadState.WaitSleepJoin))
Thread.Sleep(10);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, ret);
}
private ValueTuple<HttpStatusCode, object?> GetV2MangaInternalId(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(groups.Count < 1 ||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, manga);
}
private ValueTuple<HttpStatusCode, object?> DeleteV2MangaInternalId(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(groups.Count < 1 ||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
Job[] jobs = _parent.jobBoss.GetJobsLike(publication: manga).ToArray();
_parent.jobBoss.RemoveJobs(jobs);
RemoveMangaFromCache(groups[1].Value);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
private ValueTuple<HttpStatusCode, object?> GetV2MangaInternalIdCover(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(groups.Count < 1 ||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
string filePath = manga.Value.coverFileNameInCache!;
if(!File.Exists(filePath))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "Cover-File not found.");
Image image = Image.Load(filePath);
if (requestParameters.TryGetValue("dimensions", out string? dimensionsStr))
{
Regex dimensionsRex = new(@"([0-9]+)x([0-9]+)");
if(!dimensionsRex.IsMatch(dimensionsStr))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Requested dimensions not in required format.");
Match m = dimensionsRex.Match(dimensionsStr);
int width = int.Parse(m.Groups[1].Value);
int height = int.Parse(m.Groups[2].Value);
double aspectRequested = (double)width / (double)height;
double aspectCover = (double)image.Width / (double)image.Height;
Size newSize = aspectRequested > aspectCover
? new Size(width, (width / image.Width) * image.Height)
: new Size((height / image.Height) * image.Width, height);
image.Mutate(x => x.Resize(newSize, CubicResampler.Robidoux, true));
}
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, image);
}
private ValueTuple<HttpStatusCode, object?> GetV2MangaInternalIdChapters(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(groups.Count < 1 ||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
Chapter[] chapters = requestParameters.TryGetValue("language", out string? parameter) switch
{
true => manga.Value.mangaConnector.GetChapters((Manga)manga, parameter),
false => manga.Value.mangaConnector.GetChapters((Manga)manga)
};
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, chapters);
}
private ValueTuple<HttpStatusCode, object?> GetV2MangaInternalIdChaptersLatest(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(groups.Count < 1 ||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
float latest = requestParameters.TryGetValue("language", out string? parameter) switch
{
true => float.Parse(manga.Value.mangaConnector.GetChapters(manga.Value, parameter).Max().chapterNumber),
false => float.Parse(manga.Value.mangaConnector.GetChapters(manga.Value).Max().chapterNumber)
};
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, latest);
}
private ValueTuple<HttpStatusCode, object?> PostV2MangaInternalIdIgnoreChaptersBelow(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(groups.Count < 1 ||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
if (requestParameters.TryGetValue("startChapter", out string? startChapterStr) &&
float.TryParse(startChapterStr, out float startChapter))
{
Manga manga1 = manga.Value;
manga1.ignoreChaptersBelow = startChapter;
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}else
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Parameter 'startChapter' missing, or failed to parse.");
}
private ValueTuple<HttpStatusCode, object?> PostV2MangaInternalIdMoveFolder(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(groups.Count < 1 ||
!_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) ||
manga is null)
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'");
if(!requestParameters.TryGetValue("location", out string? newFolder))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.BadRequest, "Parameter 'location' missing.");
manga.Value.MovePublicationFolder(TrangaSettings.downloadLocation, newFolder);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
}

View File

@ -0,0 +1,33 @@
using System.Net;
using System.Text.RegularExpressions;
namespace Tranga.Server;
public partial class Server
{
private ValueTuple<HttpStatusCode, object?> GetV2LogFile(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (logger is null || !File.Exists(logger?.logFilePath))
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "Missing Logfile");
}
FileStream logFile = new (logger.logFilePath, FileMode.Open, FileAccess.Read);
FileStream content = new(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 0, FileOptions.DeleteOnClose);
logFile.Position = 0;
logFile.CopyTo(content);
content.Position = 0;
logFile.Dispose();
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, content);
}
private ValueTuple<HttpStatusCode, object?> GetV2Ping(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Accepted, "Pong!");
}
private ValueTuple<HttpStatusCode, object?> PostV2Ping(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Accepted, "Pong!");
}
}

View File

@ -0,0 +1,136 @@
using System.Net;
using System.Text.RegularExpressions;
using Tranga.NotificationConnectors;
namespace Tranga.Server;
public partial class Server
{
private ValueTuple<HttpStatusCode, object?> GetV2NotificationConnector(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, notificationConnectors);
}
private ValueTuple<HttpStatusCode, object?> GetV2NotificationConnectorTypes(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK,
Enum.GetValues<NotificationConnectors.NotificationConnector.NotificationConnectorType>().ToDictionary(b => (byte)b, b => Enum.GetName(b)));
}
private ValueTuple<HttpStatusCode, object?> GetV2NotificationConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType))
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist.");
}
if(notificationConnectors.All(nc => nc.notificationConnectorType != notificationConnectorType))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {Enum.GetName(notificationConnectorType)} not configured.");
else
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, notificationConnectors.First(nc => nc.notificationConnectorType != notificationConnectorType));
}
private ValueTuple<HttpStatusCode, object?> PostV2NotificationConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType))
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist.");
}
string? url;
switch (notificationConnectorType)
{
case NotificationConnector.NotificationConnectorType.Gotify:
if(!requestParameters.TryGetValue("url", out url))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
if(!requestParameters.TryGetValue("appToken", out string? appToken))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'appToken' missing.");
Gotify gotify = new (this, url, appToken);
this.notificationConnectors.RemoveWhere(nc =>
nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.Gotify);
this.notificationConnectors.Add(gotify);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, gotify);
case NotificationConnector.NotificationConnectorType.LunaSea:
if(!requestParameters.TryGetValue("webhook", out string? webhook))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'webhook' missing.");
LunaSea lunaSea = new (this, webhook);
this.notificationConnectors.RemoveWhere(nc =>
nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.LunaSea);
this.notificationConnectors.Add(lunaSea);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, lunaSea);
case NotificationConnector.NotificationConnectorType.Ntfy:
if(!requestParameters.TryGetValue("url", out url))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
if(!requestParameters.TryGetValue("username", out string? username))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'username' missing.");
if(!requestParameters.TryGetValue("password", out string? password))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'password' missing.");
Ntfy ntfy = new(this, url, username, password, null);
this.notificationConnectors.RemoveWhere(nc =>
nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.Ntfy);
this.notificationConnectors.Add(ntfy);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, ntfy);
default:
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"NotificationType {Enum.GetName(notificationConnectorType)} is not supported.");
}
}
private ValueTuple<HttpStatusCode, object?> PostV2NotificationConnectorTypeTest(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType))
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist.");
}
string? url;
switch (notificationConnectorType)
{
case NotificationConnector.NotificationConnectorType.Gotify:
if(!requestParameters.TryGetValue("url", out url))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
if(!requestParameters.TryGetValue("appToken", out string? appToken))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'appToken' missing.");
Gotify gotify = new (this, url, appToken);
gotify.SendNotification("Tranga Test", "It was successful :3");
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, gotify);
case NotificationConnector.NotificationConnectorType.LunaSea:
if(!requestParameters.TryGetValue("webhook", out string? webhook))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'webhook' missing.");
LunaSea lunaSea = new (this, webhook);
lunaSea.SendNotification("Tranga Test", "It was successful :3");
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, lunaSea);
case NotificationConnector.NotificationConnectorType.Ntfy:
if(!requestParameters.TryGetValue("url", out url))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'url' missing.");
if(!requestParameters.TryGetValue("username", out string? username))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'username' missing.");
if(!requestParameters.TryGetValue("password", out string? password))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotAcceptable, "Parameter 'password' missing.");
Ntfy ntfy = new(this, url, username, password, null);
ntfy.SendNotification("Tranga Test", "It was successful :3");
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, ntfy);
default:
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.MethodNotAllowed, $"NotificationType {Enum.GetName(notificationConnectorType)} is not supported.");
}
}
private ValueTuple<HttpStatusCode, object?> DeleteV2NotificationConnectorType(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (groups.Count < 1 ||
!Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType))
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist.");
}
if(notificationConnectors.All(nc => nc.notificationConnectorType != notificationConnectorType))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"NotificationType {Enum.GetName(notificationConnectorType)} not configured.");
else
{
notificationConnectors.Remove(notificationConnectors.First(nc => nc.notificationConnectorType != notificationConnectorType));
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
}
}

137
Tranga/Server/v2Settings.cs Normal file
View File

@ -0,0 +1,137 @@
using System.Net;
using System.Text.RegularExpressions;
using Tranga.MangaConnectors;
namespace Tranga.Server;
public partial class Server
{
private ValueTuple<HttpStatusCode, object?> GetV2Settings(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.AsJObject());
}
private ValueTuple<HttpStatusCode, object?> GetV2SettingsUserAgent(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.userAgent);
}
private ValueTuple<HttpStatusCode, object?> PostV2SettingsUserAgent(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (!requestParameters.TryGetValue("value", out string? userAgent))
{
TrangaSettings.UpdateUserAgent(null);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.Accepted, null);
}
else
{
TrangaSettings.UpdateUserAgent(userAgent);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
}
private ValueTuple<HttpStatusCode, object?> GetV2SettingsRateLimitTypes(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, Enum.GetValues<RequestType>().ToDictionary(b =>(byte)b, b => Enum.GetName(b)) );
}
private ValueTuple<HttpStatusCode, object?> GetV2SettingsRateLimit(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.requestLimits);
}
private ValueTuple<HttpStatusCode, object?> PostV2SettingsRateLimit(GroupCollection groups, Dictionary<string, string> requestParameters)
{
foreach (KeyValuePair<string, string> kv in requestParameters)
{
if(!Enum.TryParse(kv.Key, out RequestType requestType) ||
!int.TryParse(kv.Value, out int requestsPerMinute))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, null);
TrangaSettings.UpdateRateLimit(requestType, requestsPerMinute);
}
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.requestLimits);
}
private ValueTuple<HttpStatusCode, object?> GetV2SettingsRateLimitType(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(groups.Count < 1 ||
!Enum.TryParse(groups[1].Value, out RequestType requestType))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"RequestType {groups[1].Value}");
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.requestLimits[requestType]);
}
private ValueTuple<HttpStatusCode, object?> PostV2SettingsRateLimitType(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if(groups.Count < 1 ||
!Enum.TryParse(groups[1].Value, out RequestType requestType))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, $"RequestType {groups[1].Value}");
if (!requestParameters.TryGetValue("value", out string? requestsPerMinuteStr) ||
!int.TryParse(requestsPerMinuteStr, out int requestsPerMinute))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Errors parsing requestsPerMinute");
TrangaSettings.UpdateRateLimit(requestType, requestsPerMinute);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
private ValueTuple<HttpStatusCode, object?> GetV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.aprilFoolsMode);
}
private ValueTuple<HttpStatusCode, object?> PostV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (!requestParameters.TryGetValue("value", out string? trueFalseStr) ||
!bool.TryParse(trueFalseStr, out bool trueFalse))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Errors parsing 'value'");
TrangaSettings.UpdateAprilFoolsMode(trueFalse);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
private ValueTuple<HttpStatusCode, object?> GetV2SettingsCompressImages(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.compression);
}
private ValueTuple<HttpStatusCode, object?> PostV2SettingsCompressImages(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (!requestParameters.TryGetValue("value", out string? valueStr) ||
!int.TryParse(valueStr, out int value)
|| value != int.Clamp(value, 1, 100))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Errors parsing 'value'");
TrangaSettings.UpdateCompressImages(value);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
private ValueTuple<HttpStatusCode, object?> GetV2SettingsBwImages(GroupCollection groups, Dictionary<string, string> requestParameters)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, TrangaSettings.bwImages);
}
private ValueTuple<HttpStatusCode, object?> PostV2SettingsBwImages(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (!requestParameters.TryGetValue("value", out string? trueFalseStr) ||
!bool.TryParse(trueFalseStr, out bool trueFalse))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Errors parsing 'value'");
TrangaSettings.UpdateBwImages(trueFalse);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
private ValueTuple<HttpStatusCode, object?> PostV2SettingsDownloadLocation(GroupCollection groups, Dictionary<string, string> requestParameters)
{
if (!requestParameters.TryGetValue("location", out string? folderPath))
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.NotFound, "Missing Parameter 'location'");
try
{
bool moveFiles = requestParameters.TryGetValue("moveFiles", out string? moveFilesStr) switch
{
false => true,
true => bool.Parse(moveFilesStr!)
};
TrangaSettings.UpdateDownloadLocation(folderPath, moveFiles);
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.OK, null);
}
catch (FormatException)
{
return new ValueTuple<HttpStatusCode, object?>(HttpStatusCode.InternalServerError, "Error Parsing Parameter 'moveFiles'");
}
}
}

View File

@ -8,8 +8,7 @@ public partial class Tranga : GlobalBase
{
public bool keepRunning;
public JobBoss jobBoss;
private Server _server;
private HashSet<MangaConnector> _connectors;
private Server.Server _server;
public Tranga(Logger? logger) : base(logger)
{
@ -26,12 +25,13 @@ public partial class Tranga : GlobalBase
new MangaLife(this),
new ManhuaPlus(this),
new MangaHere(this),
new AsuraToon(this),
};
foreach(DirectoryInfo dir in new DirectoryInfo(Path.GetTempPath()).GetDirectories("trangatemp"))//Cleanup old temp folders
dir.Delete();
jobBoss = new(this, this._connectors);
StartJobBoss();
this._server = new Server(this);
this._server = new Server.Server(this);
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=)"};
SendNotifications("Tranga Started", emojis[Random.Shared.Next(0,emojis.Length-1)]);
Log(TrangaSettings.AsJObject().ToString());
@ -51,9 +51,9 @@ public partial class Tranga : GlobalBase
return connector is not null;
}
public IEnumerable<MangaConnector> GetConnectors()
public List<MangaConnector> GetConnectors()
{
return _connectors;
return _connectors.ToList();
}
public Manga? GetPublicationById(string internalId) => GetCachedManga(internalId);

View File

@ -13,7 +13,10 @@
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="PuppeteerSharp" Version="10.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="2.1.301" />
<PackageReference Include="System.Drawing.Common" Version="9.0.0-preview.7.24405.4" />
<PackageReference Include="ZstdSharp.Port" Version="0.8.1" />
</ItemGroup>
<ItemGroup>

View File

@ -17,11 +17,14 @@ public static class TrangaSettings
public static string userAgent { get; private set; } = DefaultUserAgent;
public static bool bufferLibraryUpdates { get; private set; } = false;
public static bool bufferNotifications { get; private set; } = false;
public static int compression{ get; private set; } = 40;
public static bool bwImages { get; private set; } = false;
[JsonIgnore] public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
[JsonIgnore] public static string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json");
[JsonIgnore] public static string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json");
[JsonIgnore] public static string jobsFolderPath => Path.Join(workingDirectory, "jobs");
[JsonIgnore] public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
[JsonIgnore] public static string mangaCacheFolderPath => Path.Join(workingDirectory, "mangaCache");
public static ushort? version { get; } = 2;
public static bool aprilFoolsMode { get; private set; } = true;
[JsonIgnore]internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
@ -48,7 +51,9 @@ public static class TrangaSettings
ExportSettings();
}
public static void CreateOrUpdate(string? downloadDirectory = null, string? pWorkingDirectory = null, int? pApiPortNumber = null, string? pUserAgent = null, bool? pAprilFoolsMode = null, bool? pBufferLibraryUpdates = null, bool? pBufferNotifications = null)
public static void CreateOrUpdate(string? downloadDirectory = null, string? pWorkingDirectory = null,
int? pApiPortNumber = null, string? pUserAgent = null, bool? pAprilFoolsMode = null,
bool? pBufferLibraryUpdates = null, bool? pBufferNotifications = null, int? pCompression = null, bool? pbwImages = null)
{
if(pWorkingDirectory is null && File.Exists(settingsFilePath))
LoadFromWorkingDirectory(workingDirectory);
@ -59,6 +64,8 @@ public static class TrangaSettings
aprilFoolsMode = pAprilFoolsMode ?? aprilFoolsMode;
bufferLibraryUpdates = pBufferLibraryUpdates ?? bufferLibraryUpdates;
bufferNotifications = pBufferNotifications ?? bufferNotifications;
compression = pCompression ?? compression;
bwImages = pbwImages ?? bwImages;
Directory.CreateDirectory(downloadLocation);
Directory.CreateDirectory(workingDirectory);
ExportSettings();
@ -98,33 +105,67 @@ public static class TrangaSettings
ExportSettings();
}
public static void UpdateCompressImages(int value)
{
compression = int.Clamp(value, 1, 100);
ExportSettings();
}
public static void UpdateBwImages(bool enabled)
{
bwImages = enabled;
ExportSettings();
}
public static void UpdateDownloadLocation(string newPath, bool moveFiles = true)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
Directory.CreateDirectory(newPath,
GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite);
Directory.CreateDirectory(newPath, GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite);
else
Directory.CreateDirectory(newPath);
if (moveFiles && Directory.Exists(downloadLocation))
Directory.Move(downloadLocation, newPath);
downloadLocation = newPath;
if (moveFiles)
MoveContentsOfDirectoryTo(TrangaSettings.downloadLocation, newPath);
TrangaSettings.downloadLocation = newPath;
ExportSettings();
}
public static void UpdateWorkingDirectory(string newPath)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
Directory.CreateDirectory(newPath,
GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite);
Directory.CreateDirectory(newPath, GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite);
else
Directory.CreateDirectory(newPath);
Directory.Move(workingDirectory, newPath);
workingDirectory = newPath;
MoveContentsOfDirectoryTo(TrangaSettings.workingDirectory, newPath);
TrangaSettings.workingDirectory = newPath;
ExportSettings();
}
private static void MoveContentsOfDirectoryTo(string oldDir, string newDir)
{
string[] directoryPaths = Directory.GetDirectories(oldDir);
string[] filePaths = Directory.GetFiles(oldDir);
foreach (string file in filePaths)
{
string newPath = Path.Join(newDir, Path.GetFileName(file));
File.Move(file, newPath, true);
}
foreach(string directory in directoryPaths)
{
string? dirName = Path.GetDirectoryName(directory);
if(dirName is null)
continue;
string newPath = Path.Join(newDir, dirName);
if(Directory.Exists(newPath))
MoveContentsOfDirectoryTo(directory, newPath);
else
Directory.Move(directory, newPath);
}
}
public static void UpdateUserAgent(string? customUserAgent)
{
userAgent = customUserAgent ?? DefaultUserAgent;
@ -167,6 +208,8 @@ public static class TrangaSettings
jobj.Add("requestLimits", JToken.FromObject(requestLimits));
jobj.Add("bufferLibraryUpdates", JToken.FromObject(bufferLibraryUpdates));
jobj.Add("bufferNotifications", JToken.FromObject(bufferNotifications));
jobj.Add("compression", JToken.FromObject(compression));
jobj.Add("bwImages", JToken.FromObject(bwImages));
return jobj;
}
@ -191,5 +234,9 @@ public static class TrangaSettings
bufferLibraryUpdates = blu.Value<bool>()!;
if (jobj.TryGetValue("bufferNotifications", out JToken? bn))
bufferNotifications = bn.Value<bool>()!;
if (jobj.TryGetValue("compression", out JToken? ci))
compression = ci.Value<int>()!;
if (jobj.TryGetValue("bwImages", out JToken? bwi))
bwImages = bwi.Value<bool>()!;
}
}

383
docs/API_Calls.md Normal file
View File

@ -0,0 +1,383 @@
## Tranga API Calls
This document serves to outline all of the different HTTP API calls that Tranga accepts. Tranga expects specific HTTP methods for its calls and therefore careful attention must be paid when making them.
In the examples below, `{apiUri}` refers to your `http(s)://TRANGA.FRONTEND.URI/api`. Parameters are included in the HTTP request URI and the request body is in JSON format. Tranga responses are always
in the JSON format within the Response Body.
#### [GET] /Connectors
Retrieves the available manga sites (connectors) that Tranga is currently able to download manga from.
- Parameters:
None
- Request Body:
None
#### [GET] /Jobs
Retrieves all jobs that Tranga is keeping track of, includes Running Jobs, Waiting Jobs, Manga Tracking (Monitoring) Jobs.
- Parameters:
None
- Request Body:
None
#### [DELETE] /Jobs
Removes the specified job given by the job ID
- Request Variables:
- None
- Request Body:
```
{
jobId: ${Tranga Job ID}
}
```
#### [POST] /Jobs/Cancel
Cancels a running job or prevents a queued job from running.
- Parameters:
None
- Request Body:
```
{
jobId: ${Tranga Job ID}
}
```
#### [POST] /Jobs/DownloadNewChapters
Manually adds a Job to Tranga's queue to check for and download new chapters for a specified manga
- Parameters:
None
- Request Body:
```
{
connector: ${Manga Connector to Download From}
internalId: ${Tranga Manga ID}
translatedLanguage: ${Manga Language}
}
```
#### [GET] /Jobs/Running
Retrieves all currently running jobs.
- Parameters:
None
- Request Body:
None
#### [POST] /Jobs/StartNow
Manually starts a configured job
- Parameters:
None
- Request Body:
```
{
jobId: ${Tranga Job ID}
}
```
#### [GET]/Jobs/Waiting
Retrieves all currently queued jobs.
- Parameters:
None
- Request Body:
None
#### [GET] /Jobs/MonitorJobs
Retrieves all jobs for Mangas that Tranga is currently tracking.
- Parameters:
None
- Request Body:
None
#### [POST] /Jobs/MonitorManga
Adds a new manga for Tranga to monitor
- Parameters:
None
- Request Body:
```
{
connector: ${Manga Connector to download from}
internalId: ${Tranga Manga ID}
interval: ${Interval at which to run job, in the HH:MM:SS format}
translatedLanguage: ${Supported language code}
ignoreBelowChapterNum: ${Chapter number to start downloading from}
customFolderName: ${Folder Name to save Manga to}
}
```
#### [GET] /Jobs/Progress
Retrieves the current completion progress of a running or waiting job. Tranga's ID for the Job is returned with each of the `GET /Job/` API calls.
- Parameters:
- `{jobId}`: Tranga Job ID
- Request Body:
None
#### [POST] /Jobs/UpdateMetadata
Updates the metadata for all monitored mangas
- Parameters:
None
- Request Body:
None
#### [GET] /LibraryConnectors
Retrieves the currently configured library servers
- Parameters:
None
- Request Body:
None
#### [DELETE] /LibraryConnectors/Reset
Resets or clears a configured library connector
- Parameters:
None
- Request Body:
```
{
libraryConnector: Komga/Kavita
}
```
#### [POST] /LibraryConnectors/Test
Verifies the behavior of a library connector before saving it. The connector must be checked to verify that the connection is active.
- Parameters:
None
- Request Body:
```
{
libraryConnector: Komga/Kavita
libraryURL: ${Library URL}
komgaAuth: Only for when libraryConnector = Komga
kavitaUsername: Only for when libraryConnector = Kavita
kavitaPassword: Only for when libraryConnector = Kavita
}
```
#### [GET] /LibraryConnectors/Types
Retrives Key-Value pairs for all of Tranga's currently supported library servers.
- Parameters:
None
- Request Body:
None
#### [POST] /LibraryConnectors/Update
Updates or Adds a Library Connector to Tranga
- Parameters: None
- Request Body:
```
{
libraryConnector: Komga/Kavita
libraryURL: ${Library URL}
komgaAuth: Only for when libraryConnector = Komga
kavitaUsername: Only for when libraryConnector = Kavita
kavitaPassword: Only for when libraryConnector = Kavita
}
```
#### [GET] /LogFile
Retrieves the log file from the running Tranga instance
- Parameters:
None
- Request Body:
None
#### [GET] /Manga/FromConnector
Retrieves the details about a specified manga from a specific connector. If the manga title returned by Tranga is a URL (determined by the presence of `http` in the title, the API call should use the second
call with the `url` rather than the `title`.
- Parameters:
- `{connector}`: Manga Connector
- `{url/title}`: Manga URL/Title
- Request Body:
None
#### [GET] /Manga/Chapters
Retrieves the currently available chapters for a specified manga from a connector. The `{internalId}` is how Tranga uniquely recognizes and distinguishes different Manga.
- Parameters:
- `{connector}`: Manga Connector
- `{internalId}`: Tranga Manga ID
- `{translatedLanguage}`: Translated Language
- Request Body:
None
#### [GET] /Manga/Cover
Retrives the URL of the cover image for a specific manga that Tranga is tracking.
- Parameters:
- `{internalId}`: Tranga Manga ID
- Request Body:
None
#### [GET] /NotificationConnectors
Retrieves the currently configured notification providers
- Parameters:
None
- Request Body:
None
#### [DELETE] /NotificationConnectors/Reset
Resets or clears a configured notification connector
- Parameters:
None
- Request Body:
```
{
notificationConnector: Gotify/Ntfy/LunaSea
}
```
#### [POST] /NotificationConnectors/Test
Tests a notification connector with the currently input settings. The connector behavior must be checked to verify that the input settings are correct.
- Parameters:
None
- Request Body:
```
{
notificationConnector: Gotify/Ntfy/LunaSea
gotifyUrl:
gotifyAppToken:
lunaseaWebhook:
ntfyUrl:
ntfyAuth:
}
```
#### [POST] /NotificationConnectors/Update
Updates or Adds a notification connector to Tranga
- Parameters:
None
- Request Body:
```
{
notificationConnector: Gotify/Ntfy/LunaSea
gotifyUrl:
gotifyAppToken:
lunaseaWebhook:
ntfyUrl:
ntfyAuth:
}
```
#### [GET] /NotificationConnectors/Types
Retrives Key-Value pairs for all of Tranga's currently supported notification providers.
- Parameters:
None
- Request Body:
None
#### [GET] /Ping
This call is used periodically by the web frontend to establish that connection to the server is active.
- Parameters:
None
- Request Body:
None
#### [GET] /Settings
Retrieves the content of Tranga's `settings.json`
- Parameters:
None
- Request Body:
None
#### [GET] /Settings/customRequestLimit
Retrieves the configured rate limits for different types of manga connector requests.
- Parameters:
None
- Request Body:
None
#### [POST] /Settings/customRequestLimit
Sets the rate limits for different types of manga connector requests.
- Parameters:
None
- Request Body:
```
{
requestType: {Request Byte}
requestsPerMinute: {Rate Limit in Requests Per Minute}
}
```
#### [POST] /Settings/UpdateDownloadLocation
Updates the root directory of where Tranga downloads manga
- Parameters:
None
- Request Body:
```
{
downloadLocation: {New Root Directory}
moveFiles: "true"/"false"
}
```
#### [POST] /Settings/userAgent
Updates the user agent that Tranga uses when scraping the web
- Parameters
- Request Body:
```
{
userAgent: {User Agent String}
}
```

1104
docs/API_Calls_v2.md Normal file

File diff suppressed because it is too large Load Diff

173
docs/Types.md Normal file
View File

@ -0,0 +1,173 @@
## Connector
```
{
"name": string,
"SupportedLanguages": string[],
"BaseUris": string[]
}
```
## Manga
```
{
"sortName": string,
"authors": string[],
"altTitles": string[][],
"description": string,
"tags": string[],
"coverUrl": string,
"coverFileNameInCache": string,
"links": string[][],
"year": int,
"originalLanguage": string,
"releaseStatus": ReleaseStatus, see ReleaseStatus
"folderName": string,
"publicationId": string,
"internalId": string,
"ignoreChaptersBelow": number,
"latestChapterDownloaded": number,
"latestChapterAvailable": number,
"websiteUrl": string,
"mangaConnector": Connector
}
```
## Chapter
```
{
"parentManga": IManga,
"name": string | undefined,
"volumeNumber": string,
"chapterNumber": string,
"url": string,
"fileName": string,
"id": string?
}
```
### ReleaseStatus
```
{
Continuing = 0,
Completed = 1,
OnHiatus = 2,
Cancelled = 3,
Unreleased = 4
}
```
## Job
```
{
"progressToken": IProgressToken,
"recurring": boolean,
"recurrenceTime": string,
"lastExecution": Date,
"nextExecution": Date,
"id": string,
"jobType": number, //see JobType
"parentJobId": string | null,
"mangaConnector": IMangaConnector,
"mangaInternalId": string | undefined, //only on DownloadNewChapters
"translatedLanguage": string | undefined, //only on DownloadNewChapters
"chapter": IChapter | undefined, //only on DownloadChapter
}
```
### JobType
```
{
DownloadChapterJob = 0,
DownloadNewChaptersJob = 1,
UpdateMetaDataJob = 2,
MonitorManga = 3
}
```
## ProgressToken
```
{
"cancellationRequested": boolean,
"increments": number,
"incrementsCompleted": number,
"progress": number,
"lastUpdate": Date,
"executionStarted": Date,
"timeRemaining": Date,
"state": number //see ProgressState
}
```
### ProgressState
```
{
Running = 0,
Complete = 1,
Standby = 2,
Cancelled = 3,
Waiting = 4
}
```
## Settings
```
{
"downloadLocation": string,
"workingDirectory": string,
"apiPortNumber": number,
"userAgent": string,
"bufferLibraryUpdates": boolean,
"bufferNotifications": boolean,
"version": number,
"aprilFoolsMode": boolean,
"compressImages": boolean,
"bwImages": boolean,
"requestLimits": {
"MangaInfo": number,
"MangaDexFeed": number,
"MangaDexImage": number,
"MangaImage": number,
"MangaCover": number,
"Default": number
}
}
```
## LibraryConnector
```
{
"libraryType": number, //see LibraryType
"baseUrl": string,
"auth": string
}
```
### LibraryType
```
{
Komga = 0,
Kavita = 1
}
```
## NotificationConnector
```
{
"notificationConnectorType": number, //see NotificationConnectorType
"endpoint": string, //only on Ntfy, Gotify
"appToken": string, //only on Gotify
"auth": string, //only on Ntfy
"topic": string, //only on Ntfy
"id": string, //only on LunaSea
}
```
### NotificationConnectorType
```
{
Gotify = 0,
LunaSea = 1,
Ntfy = 2
}
```