1668 Commits

Author SHA1 Message Date
7f9bea00a4 Fix BaseWorker unnecessary nesting of Tasks
Some checks failed
Docker Image CI / build (push) Has been cancelled
Fix MangaDex Oneshots have no Chapternumber
2025-09-08 18:52:41 +02:00
1360b7afc5 Update Readme and License
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-09-06 17:27:07 +02:00
3a8a2d2c86 Remove obsolete files 2025-09-06 17:03:28 +02:00
af01e4a6a4 NotificationConnector DTO and RequestRecords 2025-09-06 17:02:15 +02:00
d4ea40a875 Descriptions for Records and DTOs 2025-09-06 16:40:19 +02:00
09e6b07186 LibrarConnector DTO and CreateLibraryConnectorRecord 2025-09-06 16:31:16 +02:00
26987c983f Add FileLibrary DTO and CreateLibraryRecord 2025-09-06 16:26:17 +02:00
b3df3bf144 Server v2 is cuttingedge 2025-09-06 16:10:10 +02:00
757d7a8ab0 Fix crashes if task is not finished yet. 2025-09-05 20:52:06 +02:00
ddcab0c6ed Fix caching etag comparison 2025-09-05 20:42:02 +02:00
f1a61a7b90 Fix wrong value being returned 2025-09-05 20:36:24 +02:00
78e7e8fc06 Resize Covers on download 2025-09-05 20:28:50 +02:00
d1b2f0ab19 Include="Log4Net.config.xml" CopyToOutputDirectory="Always" CopyToPublishDirectory="Always" 2025-09-05 17:31:49 +02:00
8439b8b2ab Configure log4net loggers 2025-09-05 15:31:06 +02:00
62090d1677 Use Tracked entity 2025-09-05 03:00:17 +02:00
e0735eea0f Mark ConnectorId to not use again if there is no image-urls 2025-09-05 02:36:26 +02:00
65418c0495 Max Worker Concurrency 2025-09-05 01:13:31 +02:00
35e36a557c Fix asynchronous chapter checking 2025-09-05 00:08:48 +02:00
ee3789fdfd Fix Chapter already exists check 2025-09-05 00:05:14 +02:00
aae5c88ec9 Order the chapters 2025-09-04 23:46:31 +02:00
0cb239a4e6 Maximum concurrent downloads (too many crashes the application) 2025-09-04 23:33:42 +02:00
b24308021b Fix request limits for UserAgents not being set correctly 2025-09-04 23:14:32 +02:00
2d662c269b Fix new Workers not getting added. 2025-09-04 23:02:24 +02:00
c8be88d645 Add new MangaConnectorId<Chapter> for existing Chapters to database 2025-09-04 22:50:17 +02:00
48914985d2 If Manga is marked for Download from Connector, mark the new Chapters as UseForDownload 2025-09-04 22:20:42 +02:00
f94513a4ae Fix Manga ChangeLibrary Endpoint to be faster if library is the same.
Fix MoveMangaLibraryWorker.cs setting off new Library
2025-09-04 22:15:29 +02:00
b76f460778 Include Library in Manga-Response 2025-09-04 21:52:19 +02:00
f9e017ec11 Fix RetrieveMangaChaptersFromMangaconnector Chapters not getting added to Manga 2025-09-04 21:46:23 +02:00
ec54cad58c Add icon to global connector 2025-09-04 19:17:41 +02:00
a5c82b2505 Fix search not adding manga to database 2025-09-04 19:11:41 +02:00
35bbbc521e Change DefaultRequestLimits 2025-09-04 18:02:02 +02:00
2cb898d8eb fix healthcheck for local testing 2025-09-04 18:00:25 +02:00
c23d29436f MangaConnector DTO 2025-09-03 17:55:26 +02:00
cb14a7c31f Use DTOs to return API requests instead of Database Schema types.
Make use of IHttpStatusCodeResults
2025-09-02 22:26:50 +02:00
55f04710a7 Improved CancellationToken usage, Added more logging to Workers, Added Code-comments (please future me, be thankful) 2025-09-02 19:36:06 +02:00
6fa166363a Fix task finished before Finish-Action could be set. 2025-09-02 01:30:41 +02:00
1b0aa7d543 Remove armv6 failed to build: failed to solve: no match for platform in manifest: not found 2025-09-02 00:57:09 +02:00
0c94e6e5fe workflow more platforms 2025-09-02 00:50:01 +02:00
ffe1f4fd2c Search use MinimalManga 2025-09-02 00:42:07 +02:00
e96b2585e0 Return new DTO "MinimalManga" for endpoints that return a lot of Manga-data 2025-09-01 23:43:59 +02:00
3b8570cf57 Allow requests to be cancelled.
Make workers have a CancellationTokenSource
2025-09-01 23:26:49 +02:00
6c61869e66 remove --platform=$TARGETPLATFORM 2025-09-01 21:51:30 +02:00
1c6398414d Add key length annotation to Identifiable 2025-09-01 21:50:00 +02:00
29d21f06e5 Move endpoint to get downloading manga 2025-09-01 21:26:34 +02:00
b4a0ce68c3 Return all Manga by default, add endpoint to return Mangakeys 2025-09-01 21:22:29 +02:00
94a8dfc90a Add endpoint to return worker keys 2025-09-01 21:21:13 +02:00
81463de409 Return Workers directly instead of keys 2025-09-01 21:18:56 +02:00
e31ecbd66b specify user for postgres healthcheck 2025-09-01 20:35:00 +02:00
c81a3633de Update packages, remove puppeteer 2025-09-01 20:35:00 +02:00
5883ed6426 Fix rebase 2025-09-01 20:35:00 +02:00
7e70288662 Fix MangaConnectorId Chapters Cascade 2025-09-01 20:35:00 +02:00
24299a955a Add Maintenance Controller
- CleanupNoDownloadManga
2025-09-01 20:35:00 +02:00
49856657ca Add Query to get similar Manga by name 2025-09-01 20:35:00 +02:00
16ef2fc9f1 Fix MangaDex null 2025-09-01 20:35:00 +02:00
32f7a6642a Fix merging of Manga
Fix ComickIo empty lists
2025-09-01 20:35:00 +02:00
4d5c95b119 Additional Query to get only downloading Manga 2025-09-01 20:35:00 +02:00
6da116acb3 Fix Merge of Chapters 2025-09-01 20:35:00 +02:00
34b7d0c2a3 Fix Merge of Manga 2025-09-01 20:35:00 +02:00
cb06cbbb61 Add Queries for MangaConnectorIds 2025-09-01 20:35:00 +02:00
a0eb2be4bd Fix DownloadChapterFromMangaconnectorWorker trying to download even if Library is not set 2025-09-01 20:35:00 +02:00
ce9195272d Fix return of GET MangaConnector 2025-09-01 20:35:00 +02:00
d6bb9fca21 Fix CancellationToken Source crashing all Workers after 10 Minutes of runtime 2025-09-01 20:35:00 +02:00
4f6053172d Fix NotificationConnectors 2025-09-01 20:35:00 +02:00
bbaf3c46b3 NamedSwaggerGenOptions.cs 2025-09-01 20:35:00 +02:00
ccaed156bb More Logging 2025-09-01 20:35:00 +02:00
dcf8ada486 Do not use a Thread to Periodically check for Due workers.
Each Periodic Worker has it's own Thread that waits for execution.
2025-09-01 20:35:00 +02:00
9d560692dc Fix Concurrency of DownloadClient LastExecutedRateLimit 2025-09-01 20:35:00 +02:00
9d2dd2eabb Add UpdateCoversWorker 2025-09-01 20:35:00 +02:00
16c0cde526 Download Cover when adding Manga 2025-09-01 20:35:00 +02:00
7189dccd89 Remove non-periodic workers after they finish 2025-09-01 20:35:00 +02:00
044553abcb Search for Manga on different MangaConnector 2025-09-01 20:35:00 +02:00
f2bc48bc52 Context Load Navigations and Collections 2025-09-01 20:35:00 +02:00
93d3878b07 GET Workers return IDs 2025-09-01 20:35:00 +02:00
cae8cde53f Disable LazyLoading
Remove MangaConnectors from Database
2025-09-01 20:35:00 +02:00
394944e11a Indent TrangaSettings 2025-09-01 20:35:00 +02:00
fb004657eb Move Configuration of Workers to separate method 2025-09-01 20:35:00 +02:00
9d84716278 Rename methods for workers from old Job 2025-09-01 20:35:00 +02:00
461272d02a StartNewChapterDownloadsWorker interval 1 minute 2025-09-01 20:35:00 +02:00
7a8ae9e175 Fix request path 2025-09-01 20:35:00 +02:00
31a22416ee Fix Scope/Context for Workers 2025-09-01 20:35:00 +02:00
f08e77102f Fix Worker-Cycle:
Periodic set last execution,
Print Running Worker-Names when done
2025-09-01 20:35:00 +02:00
6604c1b412 Fix TrangaBaseContext.Sync 2025-09-01 20:35:00 +02:00
2ab1ae04c1 Fix RemoveOldNotificationsWorker.cs: RemoveRange 2025-09-01 20:35:00 +02:00
902ebc48ff Add Migrations
Add RemoveOldNotificationsWorker.cs
2025-09-01 20:35:00 +02:00
54efc5fd5b Add default Tranga-Workers 2025-09-01 20:35:00 +02:00
6f3ccda0ed Enable Manga Downloading 2025-09-01 20:35:00 +02:00
04da402847 SettingsController set download language 2025-09-01 20:35:00 +02:00
d8c8310a48 Tranga WorkerCycle 2025-09-01 20:35:00 +02:00
fd508b7e6c UpdateMetadataWorker.cs 2025-09-01 20:35:00 +02:00
3c4f1c16db ToString overrides 2025-09-01 20:35:00 +02:00
fa29c21dfb StartNewChapterDownloadsWorker.cs 2025-09-01 20:35:00 +02:00
15f38f009f SendNotificationsWorker, CleanupMangaCoversWorker, UpdateChaptersDownloadedWorker add optional interval parameter 2025-09-01 20:35:00 +02:00
b34aa5e73a CheckForNewChaptersWorker 2025-09-01 20:35:00 +02:00
d63733bb5a Move AddMangaToContext to Tranga.cs 2025-09-01 20:35:00 +02:00
d9144e3708 SendNotificationsWorker.cs 2025-09-01 20:35:00 +02:00
072a5c3210 TrangaSettings as static field in Tranga instead of Static class 2025-09-01 20:35:00 +02:00
ed2dc72c75 Tranga CheckRunning Workers 2025-09-01 20:35:00 +02:00
56397dd0d2 BaseWorker Logging 2025-09-01 20:35:00 +02:00
9deac27f11 DbContext never null 2025-09-01 20:35:00 +02:00
637ad2e215 BaseWorker, BaseWorkerWithContext DoWork, call: Scope setting
TrangaBaseContext Sync return with success state and exception message
2025-09-01 20:35:00 +02:00
650cbffc8a IPeriodic non-generic 2025-09-01 20:35:00 +02:00
c8fcde656e Refactor Controllers
SettingsController.cs

SearchController.cs

QueryController.cs

NotificationConnectorController.cs

MetadataFetcherController.cs

MangaConnectorController.cs

FileLibraryController

LibraryConnectors

WorkerController
2025-09-01 20:35:00 +02:00
10d0a65637 WIP 2025-09-01 20:35:00 +02:00
35053bbc85 Create TrangaBaseContext for common OnConfiguring implementation of Contexts 2025-09-01 20:35:00 +02:00
d7cbafba50 Notifications-Identifiable 2025-09-01 20:35:00 +02:00
e9052de0e6 Add TODO to remove migrations after some time 2025-09-01 20:35:00 +02:00
e9231691fd Metadata-Site Search (Interactive linking) 2025-09-01 20:35:00 +02:00
191d805afa Change PrimaryKey of MetadataEntry to Fetcher + Identifier 2025-09-01 20:35:00 +02:00
826b0e4b98 MetadataFetching:
- Jikan (MAL) linking, fetching/updating
2025-09-01 20:35:00 +02:00
1abee5149c Manga and Chapters are shared across Connectors 2025-09-01 20:35:00 +02:00
ed148ce3d6 WIP: Manga can be linked to multiple Connectors
- PgsqlContext Adjustment
2025-09-01 20:35:00 +02:00
ddf009293a WIP: Manga can be linked to multiple Connectors 2025-09-01 20:35:00 +02:00
c6bb949224 Job is IComparable<Job> 2025-09-01 20:35:00 +02:00
2ff163067a Merge branch 'master' into postgres-Server-V2
# Conflicts:
#	README.md
#	Tranga/MangaConnectors/Webtoons.cs
#	Tranga/MangaConnectors/WeebCentral.cs
2025-09-01 20:27:10 +02:00
88ecf8d20b Create testing branch
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-09-01 19:47:48 +02:00
96b2e8d97a Merge pull request #422 from C9Glax/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-08-22 20:59:39 +02:00
29cdaa382e Merge pull request #424 from PBXg33k/bugfix/jsonexception
[V2] BUGFIX: JsonException not caught if response is not JSON
2025-08-22 20:59:14 +02:00
PBX_g33k
1ff3673adb BUGFIX: JsonException not caught if response is not JSON 2025-08-22 17:41:32 +02:00
dependabot[bot]
f54d12b729 Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 14:53:02 +00:00
e063cf1fd9 Debug: MatchJobsRunningAndWaiting
Some checks failed
Docker Image CI / build (push) Has been cancelled
UpdateCoverJobs not starting.
2025-06-28 23:15:51 +02:00
8170e1d762 JobCycle Info-Debug list jobs started/running
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-06-28 20:35:10 +02:00
254383b006 Include Description in ComicInfo.xml 2025-06-28 20:28:28 +02:00
df431e533a Add POST Jobs/Cleaup Endpoint:
Removes failed and completed Jobs (that are not recurring)
2025-06-28 20:18:28 +02:00
9a4cc0cbaf Only log Error on image-processing if we dont know what Exception was thrown 2025-06-28 20:13:09 +02:00
861cf7e166 Fix Image-Processing:
Format is not supported by Imagesharp, throwing exception causing Job to fail.
2025-06-28 20:00:01 +02:00
7e34b3b91e Update readme to contain information on how to test locally 2025-06-28 19:48:47 +02:00
29d36484f9 include logging driver in docker-compose
Remove parameters from start-CMD in Dockerfile
2025-06-28 19:39:19 +02:00
ba1ebcd6ba Merge pull request #407 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.11.1
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/setup-buildx-action from 3.11.0 to 3.11.1
2025-06-20 14:21:48 +02:00
dependabot[bot]
cc655b0acd Bump docker/setup-buildx-action from 3.11.0 to 3.11.1
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.11.0 to 3.11.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.11.0...v3.11.1)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 3.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-19 05:58:29 +00:00
2c6e8e4d16 Default startNewJobTimeoutMs set to 20s
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-06-18 02:11:03 +02:00
fab2886684 ComickIo Stop double work for retrieving chapters:
We can build the canonical url from the hids
2025-06-18 01:55:19 +02:00
d9ccf71b21 DownloadSingleChapterJob add check if chapter is already downloaded before re-downloading 2025-06-18 01:18:06 +02:00
f36f34f212 We dont need to actually load the MangaConnector to know if two names match.
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-06-18 00:23:33 +02:00
ff10432c79 Fix FilterJobsWithoutDownloading: Dont check if a job has a connector, that takes forever 2025-06-18 00:11:05 +02:00
776e1e4890 ...use what we coded... 2025-06-17 20:18:10 +02:00
db0643fa19 More Debug 2025-06-17 20:09:49 +02:00
3eeb563ca1 Add Debug Statement to find slow operations in Job-Cycle 2025-06-17 19:55:54 +02:00
7a88b1f7ee Increase default request Limits 2025-06-17 19:55:31 +02:00
b5411e9c6c Better Debugging for HttpDownloadClient 2025-06-17 18:52:27 +02:00
07b260dea6 GC Cleanup 2025-06-17 18:52:14 +02:00
71ad32de31 Fix FlareSolverr IsJson-Check 2025-06-17 18:51:29 +02:00
ecd2c2722f Fix FlareSolverr, Flaresolverrsharp is broken 2025-06-17 18:28:18 +02:00
ff1e467ada Add caching header to Covers
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-06-17 16:23:58 +02:00
3f5c9d0ca1 Merge pull request #403 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.11.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/setup-buildx-action from 3.10.0 to 3.11.0
2025-06-17 11:41:35 +02:00
dependabot[bot]
538825f0ef Bump docker/setup-buildx-action from 3.10.0 to 3.11.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.10.0 to 3.11.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.10.0...v3.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-17 06:03:00 +00:00
24f68b4a8e SearchController GetFromUrl StatusCode 404 instead of 400 if URL does not yield a Manga
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-06-17 00:25:09 +02:00
e51e90aabc FlareSolverr by FlareSolverrSharp
#372
2025-06-17 00:25:08 +02:00
dc2c27f4bd Merge pull request #402 from catumin/docker-compose
Some checks failed
Docker Image CI / build (push) Has been cancelled
Wait for Postgres healthcheck before attempting to continue
2025-06-16 09:52:11 +02:00
Cat Aulucya
406d8eef51 Wait for Postgres healthcheck before attempting to continue
Signed-off-by: Cat Aulucya <cat@aulucya.gay>
2025-06-15 21:17:24 -07:00
1fba599c79 Fix UserAgent formatting
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-06-16 01:31:58 +02:00
a668a16035 Use TrangaSettings.userAgent 2025-06-16 01:14:05 +02:00
f89b8e1977 Fix UserAgent RequestHeader:
UserAgent should not be added after it already existed
2025-06-16 01:11:38 +02:00
11290062c0 Fix setting of version policy 2025-06-16 00:58:54 +02:00
f46910fac6 Formatting 2025-06-16 00:52:10 +02:00
f974c5ddd1 header formatting (debug) HttpDownloadClient.cs 2025-06-16 00:49:27 +02:00
a01963a125 HttpVersionPolicy.RequestVersionOrHigher 2025-06-16 00:47:26 +02:00
8a877ee465 Extend debug for requests 2025-06-16 00:34:03 +02:00
c370e656f1 HttpDownloadClient add a Debug statement if the request fails with status code and content 2025-06-16 00:10:59 +02:00
58ed976737 HttpDownloadClient Check if original uri is equal to final uri 2025-06-16 00:10:28 +02:00
1b6af73a0c MangaDex nullvalue checks and allow null-fields in response
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-06-15 23:55:23 +02:00
70fe23857b Update UserAgent-String to Version 2.0 2025-06-15 23:26:30 +02:00
0027af2d36 Fix: First startup coverImageCache does not exist (on stale check) 2025-06-15 23:07:34 +02:00
1a8f70f501 Cleanup code for HttpDownloadClient and error-log 2025-06-15 23:00:01 +02:00
f0de0a29da Merge pull request #400 from C9Glax/dependabot/github_actions/docker/build-push-action-6.18.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/build-push-action from 6.17.0 to 6.18.0
2025-05-28 15:50:47 +02:00
dependabot[bot]
d4227f2b8f Bump docker/build-push-action from 6.17.0 to 6.18.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.17.0 to 6.18.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.17.0...v6.18.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-28 05:59:24 +00:00
aa67c11050 Start-Job endpoint: Add option to start Jobs that our job is dependent on
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-05-19 19:57:51 +02:00
7b38d0aa2b Add Debug-output for when next job is due if not job was started 2025-05-19 19:57:27 +02:00
64e31fad54 Job-Cycle match JobTypes and MangaConnectors on running and waiting Jobs 2025-05-19 17:36:32 +02:00
49a70e2341 startNewJobTimeoutMs set to 5000 2025-05-19 17:36:07 +02:00
9659f2a68a MangaDex.cs year may be null
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-05-18 22:44:32 +02:00
d474868116 Fix missing Permissions for covers 2025-05-18 22:14:51 +02:00
b1312c4164 Remove UpdateSingleChapterDownloadedJob.cs 2025-05-18 20:39:24 +02:00
33856f9927 Fix infinity joby (because we did not create new Scope on every cycle) 2025-05-18 20:31:46 +02:00
02ab3d8cae UpdatecoverJob Migrations
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-05-18 18:17:41 +02:00
7974c58fd5 Fix PgsqlContext Discriminator UpdateCoverJob 2025-05-18 18:16:17 +02:00
503d9dfb5f Fix Name UpdateCoverJob 2025-05-18 17:55:37 +02:00
62b035f6c5 GET Mangaconnector endpoint 2025-05-18 17:20:53 +02:00
40c70fbf19 Update readme 2025-05-18 17:15:53 +02:00
49bd66ccab Add UpdateCoverJob.cs
Covers get updated on every pull
If a Manga has no DownloadAvailableChaptersJob, Cover is removed
Add endpoint POST Settings/CleanupCovers that removed covers not associated to any Manga
2025-05-18 17:05:01 +02:00
9b251169a5 Remove old covers from ImageCache 2025-05-18 16:54:53 +02:00
aa29c45094 Do not regenerate JobIds in EF Constructor
(and pass down recurrenceTime regardless of usage)
2025-05-18 16:53:42 +02:00
bd60fda05a Chapters now have IdOnConnector-Site 2025-05-18 16:30:03 +02:00
8ecbdb91b2 Let Job update itself in its own context 2025-05-18 16:06:52 +02:00
cb1c68f295 Remove Job.DependenciesFulfilled 2025-05-18 16:06:39 +02:00
421a25ec31 Delete duplicate IsRequired Statements 2025-05-18 16:06:16 +02:00
2d122a918f Create a Context per cycle
Load each Job in a separate context per Job.
2025-05-18 16:06:00 +02:00
100cb06ba0 SearchController.cs Local-Search endpoint 2025-05-18 15:32:58 +02:00
6125b036bf SearchController.cs Local-Search endpoint 2025-05-18 15:31:11 +02:00
3fe3fc09b0 JobContext per Job 2025-05-18 15:21:59 +02:00
96d5b09391 Ony load necessary References and Collections 2025-05-18 15:16:55 +02:00
84aecda916 NoTrackingWithIdentityResolution 2025-05-18 15:00:33 +02:00
0803a92a66 UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) 2025-05-18 14:52:06 +02:00
7f55aaf85d test 2025-05-18 14:41:43 +02:00
3853e2daa2 Chapter.cs remove comparison again and instead check chapterids in RetrieveChaptersJob.cs 2025-05-18 14:28:07 +02:00
852fbf5ae8 Chapter.cs Compare Ids for Collection-Comparisons 2025-05-18 14:05:03 +02:00
4e7a725fee Load entry references and collections 2025-05-18 13:53:23 +02:00
698d138642 Load ParentManga.Library for Chapter
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-05-17 23:40:15 +02:00
8efb60652b RetrieveChaptersJob.cs distinct Chapters 2025-05-17 23:25:00 +02:00
fe60b98cb8 MangaDex fix crash if "en" tag was missing 2025-05-17 23:05:48 +02:00
63442e9af6 MangaDex fix crash if "en" tag was missing 2025-05-17 22:51:57 +02:00
703e32a30e Check if directorypath is null 2025-05-17 22:38:12 +02:00
4ddfe4a54c ComicInfoXML filter null values 2025-05-17 22:33:22 +02:00
fb2b4d6920 Merge pull request #396
Some checks failed
Docker Image CI / build (push) Has been cancelled
Backend-Logic-Update
2025-05-17 19:26:59 +02:00
496b49ccb3 ParentJob can be updated, DownloadAvailableJobs Endpoint automatically sets ParentJob to the DownloadAvailableChaptersJob (ondeleteCascade) 2025-05-17 19:24:24 +02:00
b3efcf19d9 Manga GetCover, GetLatestDownloaded, GetLatestAvailable: Check if Jobs are running to fulfill request 2025-05-17 19:07:01 +02:00
0903ec606b MangaController.cs use Manga.Chapters for Navigation instead of new context-Request 2025-05-17 18:54:24 +02:00
6cfa29e3dd Append Headers instead of Adding MangaController.cs 2025-05-17 18:17:51 +02:00
0519ed26de Remove Lunasea 2025-05-17 18:00:46 +02:00
aacdb72d6a Remove LunaseaRecord 2025-05-17 17:42:07 +02:00
3283dd7290 DownloadAvailableChaptersJob.cs only create DownloadSingleChapterJobs for Chapters that have not been downloaded 2025-05-16 22:00:17 +02:00
937c5cb7a7 Create a UpdateChaptersDownloadedJob with creation of DownloadAvailableChaptersJob 2025-05-16 21:59:53 +02:00
225b7f02ad Lazy Load Jobs.DependsOnJobs, Manga.Chapters 2025-05-16 21:53:59 +02:00
6258e07f20 Remove unnecessary Attachments 2025-05-16 21:36:24 +02:00
622198a09e Changes to Job.cs:
- Nest try-catch for DBUpdateException and other Exceptions
- More Logging for Jobs
2025-05-16 21:32:42 +02:00
49b382fe1f Logging for Job-Cycle 2025-05-16 21:27:02 +02:00
5a6dc5a5b2 Logging for Jobs-Filtering 2025-05-16 21:25:08 +02:00
4bc70eca68 Distinct Jobs 2025-05-16 21:23:47 +02:00
63fee081e6 Catch all Exceptions in Job 2025-05-16 21:18:46 +02:00
e45b72dcf9 Include Spaces in Directory-Path 2025-05-16 21:18:37 +02:00
021ad5e804 Include FullArchivePath in Chapter-Response 2025-05-16 21:18:19 +02:00
8e0c964883 Update Jobs on each cycle (since it is super fast now) 2025-05-16 21:09:43 +02:00
d6e945741a Fix Manga-Chapter loading 2025-05-16 21:09:32 +02:00
3a3306240f Use LazyLoading 2025-05-16 21:05:55 +02:00
110dd36166 Do not update context.Jobs on every cycle 2025-05-16 20:16:11 +02:00
065cac62af Move TRANGA message 2025-05-16 20:16:02 +02:00
563afa1e6f Split UpdateFilesDownloadedJob.cs to UpdateChaptersDownloadedJob.cs and split into UpdateSingleChapterDownloadedJob.cs 2025-05-16 20:13:39 +02:00
be2adff57d DownloadSingleChapterJob.cs load Jobs 2025-05-16 20:01:50 +02:00
adc7ee606e RetrieveChaptersJob.cs do not use context to access Chapters 2025-05-16 20:00:53 +02:00
a764f381c9 Logging for DBContexts 2025-05-16 19:46:14 +02:00
590ccdd09a Use GlobalConnector for Url Search requests 2025-05-16 19:27:34 +02:00
0f0a49f74f Change Search with name to GET Request 2025-05-16 19:27:22 +02:00
a1a5028858 Ordering of DownloadChapterJobs (start at first chapter and work up) 2025-05-16 19:18:07 +02:00
1792952039 Fix RequestTypes for ComickIo 2025-05-16 19:17:45 +02:00
9e62eb53cb Log-Output for DownloadClient improved 2025-05-16 19:17:29 +02:00
f3c4b012b0 ToString Override for RequestResult 2025-05-16 19:17:13 +02:00
cd00d35f22 Merge pull request #395 from C9Glax/dependabot/github_actions/docker/build-push-action-6.17.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/build-push-action from 6.16.0 to 6.17.0
2025-05-16 16:15:40 +02:00
7e1c65b470 Optimize requests for JobStarter 2025-05-16 15:15:34 +02:00
4247ae7740 Do not autoinclude chapters for Manga 2025-05-16 15:15:11 +02:00
a5954ed5c8 DownloadSingleChapterJob only spawn one Job per Queue for Manga 2025-05-16 14:46:36 +02:00
d08544b892 Sending notifications for -> Debug instead of Info 2025-05-16 14:38:47 +02:00
f6f86deb7f Add Debug output 2025-05-16 14:36:48 +02:00
16f5817a31 Fix ComickIo Chapter-Loading 2025-05-16 14:36:29 +02:00
d5d9f44a5f Add Comick.Io
https://github.com/C9Glax/tranga/issues/253
2025-05-16 14:24:18 +02:00
83bc3b418b Manga Year is not required (nullable) 2025-05-16 14:23:33 +02:00
205f0a1629 MangaAltTitle change Id to random 2025-05-16 14:23:20 +02:00
a1c2942208 SearchAddMangaToContext fix 2025-05-16 14:21:14 +02:00
dependabot[bot]
4ef3e877ce Bump docker/build-push-action from 6.16.0 to 6.17.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.16.0 to 6.17.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.16.0...v6.17.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-16 05:31:47 +00:00
4b4e24c6a0 Migrations update 2025-05-15 15:14:09 +02:00
475a29b10d Attach Entities to Jobs 2025-05-15 15:13:53 +02:00
694b88d200 Reload Jobs loaded in context 2025-05-09 12:31:40 +02:00
0f6c060026 Remove unnecessary default value for EF Constructors 2025-05-09 12:30:30 +02:00
b49b11828c Add ToString Overriddes 2025-05-09 12:22:32 +02:00
2d69b30e83 Fix missing Entity-Relation for UpdateFilesDownloadedJob 2025-05-09 12:03:18 +02:00
53d9be5656 Add ToString Overrides for Chapter and Manga 2025-05-09 12:03:01 +02:00
7d4a6be569 MangaConnectors do not have to return an Object with 6 Parameters.
Job-Start Logic readable and optimized
More robust Database design
2025-05-09 06:28:44 +02:00
7477f4d04d Do no replace spaces with %20
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-05-08 05:28:31 +02:00
30a8162777 Shorten RequestUrl for MangaDex MangaInfo
Lower MaxRequestLimits
2025-05-08 04:39:22 +02:00
57baad3d2c Make LastRequest (LastExecutedRateLimit) static 2025-05-08 03:57:45 +02:00
3c5f51e495 fix time formatting 2025-05-08 03:48:57 +02:00
397d3c93df More logging 2025-05-08 03:36:32 +02:00
1b49b171f4 Remove Manganato (for now) 2025-05-08 03:22:31 +02:00
ec5d048df5 Add more Logging 2025-05-08 03:03:44 +02:00
7dba2518f9 Merge pull request #388 from C9Glax/dependabot/github_actions/docker/build-push-action-6.16.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/build-push-action from 6.15.0 to 6.16.0
2025-04-25 09:01:00 +02:00
dependabot[bot]
7506a0201e Bump docker/build-push-action from 6.15.0 to 6.16.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.15.0 to 6.16.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.15.0...v6.16.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-25 05:51:29 +00:00
a490e233d7 Jobs remove redundant fields (context tracking)
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-04-02 02:16:55 +02:00
f085c5cf8e Fix RetrieveChaptersJob 2025-04-02 02:09:05 +02:00
31beeeffae Fix Job has ParentJob -> ParentJob has Job 2025-04-02 01:58:17 +02:00
99a3f2614d Catch Stream closed
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-04-01 18:24:25 +02:00
15ced9aed8 Longer Var-Chars in Manga 2025-04-01 18:21:24 +02:00
64b17aea7a weebcentral no commas in tags 2025-04-01 03:54:58 +02:00
c696c38983 Fix Weebcentral
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-04-01 02:14:46 +02:00
dbbac1ad59 Fix Weebcentral 2025-04-01 02:14:13 +02:00
b955d41530 Fix Weebcentral 2025-04-01 02:09:28 +02:00
91e033a2ec Logging 2025-03-31 19:08:35 +02:00
4dd31dfe18 Dependency Updates 2025-03-31 18:14:34 +02:00
91fb815153 Update README.md
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-29 21:17:22 +01:00
66fcdca7e7 Pushover Notifications https://github.com/C9Glax/tranga/issues/246
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-29 20:55:24 +01:00
9350de0ae9 Custom Chapter-Naming scheme
https://github.com/C9Glax/tranga/issues/92
2025-03-29 20:33:58 +01:00
c94c55300c https://github.com/C9Glax/tranga/issues/361 Chromium Close Pages that errored.
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-27 20:30:51 +01:00
721f932fac Fix MangaConnector enablestate endpoint 2025-03-27 19:49:44 +01:00
f691529591 "Global"-Connector https://github.com/C9Glax/tranga-website/issues/50 2025-03-27 19:35:49 +01:00
d75262a8f3 DBUpdate Exception on Jobs update 2025-03-27 19:03:06 +01:00
9521f66bac Fix compression level check on endpoint
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-19 23:55:39 +01:00
3981a41303 Fix Settings Endpoints (FromBody tags) 2025-03-19 23:49:24 +01:00
f2961711cf Return TrangaSettings as proper Json instead of string 2025-03-19 23:13:56 +01:00
93ad691971 Endpoint to change all information of locallibrary 2025-03-19 22:44:31 +01:00
cef3b24efd Do not delay LatestChapterAvailable/Downloaded Requests if we have a number (which will still be updated)
Some checks are pending
Docker Image CI / build (push) Waiting to run
2025-03-19 00:46:43 +01:00
f10c478cab Add 503 response for covers and requests that take longer 2025-03-19 00:16:37 +01:00
01ba927491 Fix Chapter FileName 2025-03-19 00:16:11 +01:00
90ce1395b8 Add Middleware to add Startofrequest time to all requests 2025-03-19 00:16:00 +01:00
892ef6c9d6 Fix: Add MoveMangaLibrary job to queue 2025-03-18 20:19:21 +01:00
6faf8bc733 Merge pull request #376 from C9Glax/cuttingedge
Some checks failed
Docker Image CI / build (push) Has been cancelled
Weebcentral fixxes
2025-03-18 18:13:47 +01:00
bdff5b7aec Merge pull request #375 from TheyCallMeTravis/webtoons-search_regex_fix
Some checks failed
Docker Image CI / build (push) Has been cancelled
webtoons - fix search regex
2025-03-18 18:12:35 +01:00
5af8060d7b Merge pull request #374 from TheyCallMeTravis/weebcentral-fixsearch
Weebcentral - Fix Search Results Parse
2025-03-18 18:12:23 +01:00
TheyCallMeTravis
6ed8ff1d52 webtoons - fix search regex parsing 2025-03-18 10:12:42 -05:00
TheyCallMeTravis
3324ed6e4a Weebcentral - Fix Search Results Parse 2025-03-17 14:29:09 -05:00
31c039d71e Change Endpoint to move Manga
Some checks are pending
Docker Image CI / build (push) Waiting to run
2025-03-17 20:16:39 +01:00
5b03befbf1 #168 Multiple Base-Paths (Libraries) Support
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-16 16:48:19 +01:00
67fd9d284b Merge pull request #369 from TheyCallMeTravis/WeebCentral-add_referrer
Some checks failed
Docker Image CI / build (push) Has been cancelled
WeebCentral - add referer to DownloadChapterImages
2025-03-15 10:24:28 +01:00
TheyCallMeTravis
08f26dd21d add referer to DownloadChapterImages 2025-03-14 21:18:51 -05:00
5012bbb2eb Only update jobs that still exist
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-14 00:43:46 +01:00
45a8f7a038 Logging 2025-03-14 00:39:52 +01:00
9b4baa1334 Do not try and add duplicate chapters. 2025-03-13 23:39:31 +01:00
657ab571f9 TrangaSettings add timeout to start new jobs field 2025-03-13 23:39:03 +01:00
1480aa0a03 Fix MaxURL length in db (2048) 2025-03-13 23:26:58 +01:00
232fe6406a Fix weebcentral baseuri-usage 2025-03-13 23:15:37 +01:00
a2bc14d54a Query Endpoints documentation 2025-03-13 22:47:50 +01:00
d278a25f16 Cover Endpoint wait for DownloadMangaCoverJob to finish if it exists 2025-03-13 22:29:59 +01:00
5f42a2d5ae OnDelete cascade behaviour for all manga-related items 2025-03-13 22:28:33 +01:00
60d84d1186 weebcentral remove http from baseuri 2025-03-13 22:28:11 +01:00
7ee4d32c07 Add BadRequest Check for width and height on cover endpoint 2025-03-13 21:30:05 +01:00
0e68d64f75 Annotation for jobs 2025-03-13 20:51:05 +01:00
434e30dc47 DownloadSingleChapterJob creates a scan for downloaded files 2025-03-13 20:06:59 +01:00
19ff3f578a Move adding jobs to context to Job.Run (from Tranga.cs) 2025-03-13 20:06:46 +01:00
cc03b6fa9c Fix referrer and statuscode when fetching covers 2025-03-13 19:58:29 +01:00
1f59ef66cd Fix Request-Method for cover (set to GET) 2025-03-13 19:16:06 +01:00
d7f21550cd Fix annotations 2025-03-13 19:15:55 +01:00
d93c8fdb94 Fix pgsqlcontext for Manganato 2025-03-13 18:47:15 +01:00
80320fd44d Fix pgsqlcontext for Manganato 2025-03-13 18:46:33 +01:00
6449132dbf Move name on Endpoint Search by controller body 2025-03-13 17:47:04 +01:00
0df3381c8c Fix Base-Endpoint-Path for MangaConnectorController.cs 2025-03-13 17:29:17 +01:00
2ca43e6f5d Fix Endpoint ignorechaptersbefore 2025-03-13 17:25:34 +01:00
3ba1261f31 Add Endpoints to retrieve all (not-)Downloaded Chapters 2025-03-13 17:23:52 +01:00
be72d4ba97 Add Endpoint to return Jobs with State and Type combined 2025-03-13 16:52:40 +01:00
f237d82cac Add Endpoints to retrieve Database-Entries for Author, Link, and MangaAltTitle from Id 2025-03-13 16:44:34 +01:00
a43901564b Manga Cover request retrieve sizing from query instead of body 2025-03-13 16:35:29 +01:00
34ec185125 Rename Manga.ConnectorId to IdOnConnectorSite
Do not include Manga.CoverUrl and CoverFileNameInCache in API-request-responses
2025-03-13 16:07:07 +01:00
cd08d9d78b Update TrangaSettings request limits to private set 2025-03-13 15:40:13 +01:00
339f40b61b More readme 2025-03-13 15:34:29 +01:00
8ee8f33f33 Update docker-compose.local.yaml to not spin up a website-instance
Some checks are pending
Docker Image CI / build (push) Waiting to run
2025-03-13 15:25:09 +01:00
6721bd863f Update Readme 2025-03-13 15:20:22 +01:00
ee820cfa27 Merge branch 'master' into postgres-Server-V2 2025-03-13 14:00:42 +01:00
24361d5a43 Update packages 2025-03-13 14:00:38 +01:00
6687ab4b3b Port Manganato 2025-03-08 19:22:23 +01:00
290324f9d9 Merge branch 'cuttingedge-merge-ServerV2' into postgres-Server-V2
# Conflicts:
#	Tranga/Chapter.cs
#	Tranga/MangaConnectors/Manganato.cs
#	Tranga/MangaConnectors/WeebCentral.cs
2025-03-08 19:04:36 +01:00
89ed500751 Update actions for Server-V2
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-08 19:02:14 +01:00
b00b0ee030 Merge branch 'master' into cuttingedge-merge-ServerV2
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-08 19:00:42 +01:00
e47c52ad48 Merge pull request #367 from C9Glax/cuttingedge
Cuttingedge merge
2025-03-08 18:58:40 +01:00
ef87e02d0b Add Endpoints to change single requestlimit 2025-03-08 18:46:35 +01:00
0af83f2fd0 Update Swagger Documenatation for 501 NotImplemented 2025-03-08 18:36:57 +01:00
f3854ab594 Implement MangaController/MoveFolder 2025-03-08 18:36:30 +01:00
207604a437 Return JobIds and NotificationConnector-IDs on creation 2025-03-08 18:36:10 +01:00
832ddf1442 Implement MoveFileOrFolderJob.cs 2025-03-08 18:35:41 +01:00
313225a1a1 Migrations 2025-03-08 18:09:54 +01:00
ffc0e7555a Add NewtonsoftJson to Swagger
Add RetrieveChaptersJob.cs
Add UpdateFilesDownloadedJob.cs
Remove DownloadNewChaptersJob.cs and instead use DownloadAvailableChaptersJob.cs
2025-03-08 18:09:41 +01:00
ecfc8f349b Back to working 2025-03-08 16:53:27 +01:00
94678e744f Replace occurences of "ID" with more meaningful names in documentation 2025-03-08 13:26:07 +01:00
73eb02e7cb Fix Route for new Endpoint
Adjust Routes for Search
2025-03-08 13:20:28 +01:00
c3ebd6acac Database Migrations 2025-03-08 13:10:39 +01:00
a694b9f5ab Add endpoint to add Manga by URL 2025-03-08 13:09:19 +01:00
b24d2e12fc Provide Method to validate URL for GetMangaFromUrl 2025-03-08 12:54:29 +01:00
6909c367e5 Types return only Ids of related Entities 2025-03-08 12:54:07 +01:00
293f0af8e3 Merge pull request #366 from merlinmarijn/manganato-domain-switch
Some checks are pending
Docker Image CI / build (push) Waiting to run
Manganato connector search fix
2025-03-08 07:40:11 +01:00
merlinmarijn
ebfa34e386 Update Manganato.cs 2025-03-07 22:33:24 +01:00
merlinmarijn
14524407f9 Update Manganato.cs 2025-03-07 22:29:40 +01:00
d56f0b383a Merge pull request #365 from merlinmarijn/manganato-domain-switch
Some checks are pending
Docker Image CI / build (push) Waiting to run
Manganato fix chapter naming format in CBZ files (i am sorry)
2025-03-07 21:54:06 +01:00
merlinmarijn
70391c83c1 Update Manganato.cs
i found out, i am stupid
2025-03-07 21:39:17 +01:00
dc7696ee26 Merge pull request #364 from merlinmarijn/manganato-domain-switch
Some checks are pending
Docker Image CI / build (push) Waiting to run
Enforce correct referrer check for access to Manganato
2025-03-07 20:19:52 +01:00
merlinmarijn
49dab9a670 Referrer policy changed
- Updated: image hosting platform seem to have changed a policy requiring now to send the referrer from the actual site instead of just allowing any connecting regardless of the referrer address
2025-03-07 19:57:27 +01:00
4cb48dd1b4 Remove TrangaSettings.apiportnumber 2025-03-07 18:56:01 +01:00
d43ae881b5 #362 add links to MangaConnectors for Icons 2025-03-07 18:53:34 +01:00
3583e45071 Only use enabled connectors for global search 2025-03-07 18:28:43 +01:00
c4adba6357 Update Metadata of all Manga on startup 2025-03-07 18:25:28 +01:00
ed74975312 Filter and order jobs by type and chapter 2025-03-07 18:13:39 +01:00
043f0b9593 #74 Endpoints for latest chapter downloaded and latest chapter available 2025-03-07 16:37:32 +01:00
022ebe2bcc Arbitrary Webhook for NotificationConnectors
Backend only deals in REST Webhooks
The API has custom Endpoints for Ntfy, Gotify, Lunasea that create pre-formatted Webhooks
#297 #259
2025-03-07 16:30:32 +01:00
3a8b400851 Update XML-Documentation with Mediatype 2025-03-07 14:46:15 +01:00
6c5bc3685e #229 Resize cover-images on request before transfer 2025-03-07 14:29:25 +01:00
c679d7c677 Fix DownloadMangaCover, Implement Manga/Cover endpoint 2025-03-07 14:07:59 +01:00
4075adfe6b Searching adds DownloadMangaCoverJob 2025-03-07 14:07:22 +01:00
72abc90af3 Implement Manga/Cover 2025-03-07 13:38:46 +01:00
894f105786 DownloadSingleChapter narrow down FileStream options 2025-03-07 13:38:32 +01:00
d314559361 UpdateMetadataJob add check if archives for chapters exist on disk 2025-03-07 13:26:49 +01:00
5ab0bd0b78 Chapter short Documentation update 2025-03-07 13:26:25 +01:00
5c0ace291b DownloadSingleChapterJob update coverfilename in cache in database 2025-03-07 13:26:07 +01:00
0d931bc835 MangaConnector shorten Endpoint paths 2025-03-07 13:11:41 +01:00
e1e5a45960 Implement SettingsController 2025-03-07 13:06:01 +01:00
3ecbc1a805 XML-Documentation 2025-03-07 13:05:51 +01:00
3305519307 JobController XML-Documentation 2025-03-07 12:31:10 +01:00
9fca2d81ab DeleteJob also deletes children 2025-03-07 12:25:02 +01:00
949c0cc16d #180 Option to disable/enable MangaConnectors 2025-03-07 11:45:45 +01:00
5921e524a9 Option to disable/enable jobs 2025-03-07 11:32:59 +01:00
bf332717a5 #114 Modify Job Intervals 2025-03-07 11:27:40 +01:00
bb31a94eea Dependency Updates 2025-03-07 10:36:57 +01:00
c9bc79fbd5 Update new_connector.yml
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-07 10:19:08 +01:00
83ce315f87 Merge pull request #357 from merlinmarijn/manganato-domain-switch
Some checks are pending
Docker Image CI / build (push) Waiting to run
Update Connector for Manganato connector: Migrate from .com to .gg & Adjust HTML Parsing
#358 @merlinmarijn
2025-03-07 10:06:44 +01:00
merlinmarijn
59511056d0 added try around getting urls 2025-03-03 23:43:35 +01:00
merlinmarijn
ed3ca5dba8 removed leftover comment 2025-03-03 23:04:43 +01:00
merlinmarijn
8df05d7e8a fixed image referrer 2025-03-03 22:59:25 +01:00
merlinmarijn
95d1e37b47 Update Manganato.cs 2025-03-03 22:27:37 +01:00
1d2ca4d76a Dependency Updates and Migrations 2025-03-03 21:13:18 +01:00
e2ff2c76ed Jobs:
Fix multiple jobs running on the same connector
Fix state-updates
Add more Documentation to JobController
Only allow non-running jobs to be started
2025-03-03 21:13:03 +01:00
8a0829ef69 JobState set enum to bytes with fixed values 2025-03-03 21:11:39 +01:00
d257095885 Fix MangaDex Downloads: Get Chapter-Id from URL 2025-03-03 21:11:21 +01:00
68cc23e158 Mangaconnector-Update:
Remove MangaNato
Update Weebcentral
2025-03-03 15:03:16 +01:00
b6494ab7f9 Merge pull request #354 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.6.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/setup-qemu-action from 3.5.0 to 3.6.0
2025-03-03 13:54:59 +01:00
dependabot[bot]
1d1d01b6e5 Bump docker/setup-qemu-action from 3.5.0 to 3.6.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3.5.0...v3.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 05:09:43 +00:00
5bb4977876 Merge pull request #353 from ale-ben/cuttingedge
Some checks failed
Docker Image CI / build (push) Has been cancelled
Weebcentral: File name also depends on original chapter name
2025-03-02 16:20:07 +01:00
Alessandro Benetton
c6bb1c9180 [cuttingedge] fix(Chapter): Minor logic change to account for all chapterName cases 2025-03-02 16:06:21 +01:00
Alessandro Benetton
9a066e7ac7 [cuttingedge] fix(Weebcentral): Updated CheckChapterIsDownloaded logic to also consider chapter name if present 2025-03-02 15:57:09 +01:00
Alessandro Benetton
4bafffded4 [cuttingedge] feat(Weebcentra): When ordering chapters, order name desc to put special chapters first 2025-03-02 15:56:12 +01:00
235183cd7f Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into postgres-Server-V2
# Conflicts:
#	API/Schema/MangaConnectors/Webtoons.cs
#	Tranga/Server.cs
2025-03-02 10:07:33 +01:00
942b43da67 Merge branch 'refs/heads/cuttingedge' into cuttingedge-merge-ServerV2 2025-03-02 10:06:48 +01:00
ce5538b352 Merge pull request #341 from Makhuta/cuttingedge
Some checks are pending
Docker Image CI / build (push) Waiting to run
Added to HandleGet support for connector languages
2025-03-02 09:51:11 +01:00
0cfdf17bd4 Merge pull request #350 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.10.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/setup-buildx-action from 3.9.0 to 3.10.0
2025-03-02 09:50:21 +01:00
0c48c1e020 Merge pull request #351 from C9Glax/dependabot/github_actions/docker/build-push-action-6.15.0
Bump docker/build-push-action from 6.14.0 to 6.15.0
2025-03-02 09:50:17 +01:00
0638e75ed6 Merge pull request #349 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.5.0
Bump docker/setup-qemu-action from 3.4.0 to 3.5.0
2025-03-02 09:49:16 +01:00
Alessandro Benetton
5a4bc1c6de [cuttingedge] fix(Weebcentral): Handle case of chapter name with multiple number parts 2025-03-01 12:06:51 +01:00
Alessandro Benetton
71f663ca2f [cuttingedge] fix(Weebcentral): File name also depends on original chapter name 2025-03-01 11:39:12 +01:00
dependabot[bot]
1b61a16061 Bump docker/build-push-action from 6.14.0 to 6.15.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.14.0 to 6.15.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.14.0...v6.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-27 05:25:23 +00:00
dependabot[bot]
db81fdce39 Bump docker/setup-buildx-action from 3.9.0 to 3.10.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.9.0...v3.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-27 05:25:21 +00:00
dependabot[bot]
fdb5451162 Bump docker/setup-qemu-action from 3.4.0 to 3.5.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3.4.0...v3.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-27 05:25:19 +00:00
6b7632b071 Merge pull request #344 from C9Glax/dependabot/github_actions/docker/build-push-action-6.14.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/build-push-action from 6.13.0 to 6.14.0
2025-02-20 16:49:19 +01:00
dependabot[bot]
06c080dfce Bump docker/build-push-action from 6.13.0 to 6.14.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.13.0 to 6.14.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.13.0...v6.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-20 05:50:25 +00:00
8130e11a9c Merge pull request #342 from C9Glax/cuttingedge
Some checks failed
Docker Image CI / build (push) Has been cancelled
Cuttingedge master merge
2025-02-14 15:51:42 +01:00
Makhuta
659a42d370 Add
- ability to get supported languages of connector for use in monitoring languages selector
2025-02-12 14:07:13 +01:00
9cef068785 Merge pull request #340 from Makhuta/cuttingedge
Some checks failed
Docker Image CI / build (push) Has been cancelled
Fix the Webtoons connector getting few chapters multiple times
2025-02-11 21:24:14 +01:00
Makhuta
4ad3149523 Fix
- fixed when parsing chapters the pages was incorrectly parsed resulting into adding the chapters from the last page multiple times (was still downloading OK but it would try to download the chapters from last page multiple times)
2025-02-11 20:16:30 +01:00
dccc9fdbef Merge branch 'cuttingedge-merge-ServerV2' into postgres-Server-V2
# Conflicts:
#	API/MangaDownloadClients/ChromiumDownloadClient.cs
#	Tranga/Jobs/JobBoss.cs
#	Tranga/Tranga.csproj
2025-02-09 18:38:46 +01:00
e6d40a7b36 Remove unused code from Weebcentral
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-02-09 18:37:55 +01:00
a95cb90561 Update Nuget packages 2025-02-09 18:37:41 +01:00
603e1b41d9 Add Chromium referer header 2025-02-09 18:37:33 +01:00
bb8a514830 Do not create .duplicate files anymore.
Just warn in log and delete (or attempt to delete)
2025-02-09 17:57:08 +01:00
9928abb674 Webtoons 2025-02-09 17:53:22 +01:00
ebb034e0c7 Merge branch 'cuttingedge-merge-ServerV2' into postgres-Server-V2
# Conflicts:
#	API/Schema/MangaConnectors/Webtoons.cs
#	Tranga/MangaConnectors/MangaConnector.cs
#	Tranga/MangaConnectors/MangaConnectorJsonConverter.cs
#	Tranga/MangaConnectors/WeebCentral.cs
#	Tranga/Tranga.cs
2025-02-09 17:41:29 +01:00
edacaaba8a Update Readme 2025-02-09 17:38:54 +01:00
d97da26994 spelling error 2025-02-09 17:37:53 +01:00
8b923d73c4 Merge pull request #337 from Makhuta/cuttingedge
Add Manga Connector
2025-02-09 17:34:08 +01:00
1dca7ec569 Remove Manga4Life 2025-02-09 17:30:30 +01:00
7229fad6c5 Merge branch 'cuttingedge-merge-ServerV2' into postgres-Server-V2
# Conflicts:
#	API/Schema/MangaConnectors/MangaLife.cs
#	Tranga/MangaConnectors/MangaConnectorJsonConverter.cs
#	Tranga/Tranga.cs
2025-02-09 17:28:47 +01:00
814efd3528 Merge remote-tracking branch 'origin/cuttingedge' into cuttingedge 2025-02-09 17:28:03 +01:00
2cd5d8bc4f Merge branch 'cuttingedge-merge-ServerV2' into cuttingedge 2025-02-09 17:27:42 +01:00
5a864ab9b7 Remove Manga4Life 2025-02-09 17:27:35 +01:00
663e2e2ca0 Merge pull request #329 from ale-ben/postgres-Server-V2
[Server V2] Changed chapterNumber to string and fixed job spawning error
2025-02-09 17:20:19 +01:00
c700974693 Merge pull request #339 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.4.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/setup-qemu-action from 3.3.0 to 3.4.0
2025-02-09 17:18:42 +01:00
553b5558d3 Merge pull request #338 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.9.0
Bump docker/setup-buildx-action from 3.8.0 to 3.9.0
2025-02-09 17:18:25 +01:00
c9bbfee26b Merge pull request #331 from C9Glax/dependabot/github_actions/docker/build-push-action-6.13.0
Bump docker/build-push-action from 6.12.0 to 6.13.0
2025-02-09 17:18:10 +01:00
dependabot[bot]
6e869eeb0d Bump docker/setup-qemu-action from 3.3.0 to 3.4.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3.3.0...v3.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-07 05:13:18 +00:00
dependabot[bot]
be7da69dbd Bump docker/setup-buildx-action from 3.8.0 to 3.9.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.8.0 to 3.9.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.8.0...v3.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-07 05:13:16 +00:00
Makhuta
7f13d9b1e6 Fix
- forgotten comma
2025-02-06 15:39:06 +01:00
Makhuta
0c9e3205c2 Add Manga Connector
- added [Webtoon](https://www.webtoons.com) manga connector
- modified/added support for saving covers with refferer
2025-02-06 15:37:30 +01:00
Alessandro Benetton
6315940cd6 [postgres-Server-V2] fix: pk conflict error in chapter download (still not working though) 2025-02-02 00:47:46 +01:00
Alessandro Benetton
ef7ebf022d [postgres-Server-V2] Update Weebcentral with changes from V1 2025-02-02 00:18:38 +01:00
Alessandro Benetton
725813c2f3 [postgres-Server-V2] feat(Jobs): Instead of passing context to each job, pass service provider and let the job create its own context 2025-02-01 23:08:13 +01:00
Alessandro Benetton
a69e12179b [postgres-Server-V2] fix(Manga): Was using properties before initialization 2025-02-01 22:16:12 +01:00
Alessandro Benetton
45ca2695eb [postgres-Server-V2] feat: Convert DateTime.Now calls to UtcNow 2025-02-01 21:58:50 +01:00
Alessandro Benetton
bd9e79d026 [postgres-Server-V2] Use MangaId to check if manga exists instead of connector id 2025-02-01 21:53:16 +01:00
Alessandro Benetton
6bbd09072b [postgres-Server-V2] fix(Chapter): Use tryParse instead of parse when comparing chapters 2025-02-01 21:52:34 +01:00
Alessandro Benetton
d6018b60ae [postgres-Server-V2] Add parameter array to token gen 2025-01-31 21:24:35 +01:00
dependabot[bot]
8c3b70b32e Bump docker/build-push-action from 6.12.0 to 6.13.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.12.0 to 6.13.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.12.0...v6.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 06:02:55 +00:00
4f7031ecfc Merge pull request #328 from ale-ben/cuttingedge
Some checks failed
Docker Image CI / build (push) Has been cancelled
fix: Add escape to Weebcentral regex
2025-01-25 23:26:48 +01:00
Alessandro Benetton
a713a006dd [postgres-Server-V2] fix: Updated Job logic to spawn a job only if a job with the same id is not already in the RunningJobs list 2025-01-25 12:10:32 +01:00
Alessandro Benetton
ebe7e145aa [postgres-Server-V2] feat: Convert chapterNumeber to string 2025-01-25 12:06:56 +01:00
Alessandro Benetton
f7a285aabd [cuttingedge] fix: Add escape to Weebcentral regex 2025-01-25 11:40:00 +01:00
786482398c Merge pull request #327 from ale-ben/cuttingedge
Some checks are pending
Docker Image CI / build (push) Waiting to run
Fix bug that prevented download of chapters 0
2025-01-24 22:09:53 +01:00
Alessandro Benetton
7921dcb1cb [cuttingedge] fix: Change condition for newChapters. Should solve #323 2025-01-24 21:52:52 +01:00
d0c9313279 Merge pull request #322 from C9Glax/dependabot/github_actions/docker/build-push-action-6.12.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/build-push-action from 6.11.0 to 6.12.0
2025-01-16 17:00:36 +01:00
dependabot[bot]
58cf4cf4e0 Bump docker/build-push-action from 6.11.0 to 6.12.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.11.0 to 6.12.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.11.0...v6.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-16 05:52:33 +00:00
be6b3da1be Merge branch 'cuttingedge-merge-ServerV2' into postgres-Server-V2
# Conflicts:
#	API/MangaDownloadClients/ChromiumDownloadClient.cs
#	Tranga/TrangaSettings.cs
2025-01-15 23:14:51 +01:00
280d715a7c Merge branch 'cuttingedge-merge-ServerV2' into cuttingedge
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-01-15 23:14:20 +01:00
d0b775444d Change Chromium back to WaitUntilNavigation.Networkidle0 2025-01-15 23:14:15 +01:00
b4edcccafe Merge branch 'cuttingedge-merge-ServerV2' into cuttingedge 2025-01-15 23:02:55 +01:00
268441a47d Trangasettings Json 2025-01-15 23:02:48 +01:00
b818f63f2a Merge branch 'cuttingedge-merge-ServerV2' into postgres-Server-V2
# Conflicts:
#	API/MangaDownloadClients/ChromiumDownloadClient.cs
#	Tranga/TrangaSettings.cs
2025-01-15 22:56:25 +01:00
1701881f4b Merge branch 'cuttingedge-merge-ServerV2' into cuttingedge 2025-01-15 22:53:47 +01:00
78a9322036 ChromiumDownloadClient Change WaitUnitlNavigation to Load instead of NetworkIdle 2025-01-15 22:53:39 +01:00
e5be5703f8 Merge branch 'cuttingedge-merge-ServerV2' into cuttingedge 2025-01-15 22:25:16 +01:00
cc32b3dfae TrangaSettings Chromium Timeouts 2025-01-15 22:24:55 +01:00
110a0bf481 Remove Mangasee 2025-01-15 22:19:24 +01:00
fdbe585aa0 Merge branch 'cuttingedge-merge-ServerV2' into postgres-Server-V2
# Conflicts:
#	API/Schema/MangaConnectors/Mangasee.cs
#	Tranga/Jobs/JobBoss.cs
#	Tranga/MangaConnectors/MangaConnectorJsonConverter.cs
#	Tranga/Tranga.cs
2025-01-15 22:18:52 +01:00
ce217aae4f Merge remote-tracking branch 'origin/cuttingedge' into cuttingedge 2025-01-15 22:18:11 +01:00
123a8b06b2 jobloading errormessage 2025-01-15 22:15:33 +01:00
2350c5a04b Remove Mangasee 2025-01-15 22:13:58 +01:00
f532e2ff76 JobBoss LoadJobsList change:
Fix Directory.Exists jobsFolderPath to create new Directory
Fix Loading Job fails leading to crash.
2025-01-15 22:13:50 +01:00
6a8df2f5f8 TokenGen CreateTokenHash from array of strings. 2025-01-12 19:09:37 +01:00
3abf7224d0 Merge pull request #316 from ale-ben/cuttingedge
Some checks failed
Docker Image CI / build (push) Has been cancelled
Fixed regex to capture chapters with decimal (1.5, ..)
2025-01-11 00:41:20 +01:00
Alessandro Benetton
b39dbd5671 [cuttingedge] fix(weebcentral): Fixed regex to capture chapters with decimal (1.5, ..) 2025-01-10 22:10:34 +01:00
375fad0c21 Merge pull request #314 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.3.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/setup-qemu-action from 3.2.0 to 3.3.0
2025-01-10 11:02:46 +01:00
ee0d17c24f Merge pull request #315 from C9Glax/dependabot/github_actions/docker/build-push-action-6.11.0
Bump docker/build-push-action from 6.9.0 to 6.11.0
2025-01-10 11:02:26 +01:00
dependabot[bot]
36ab3c3fdb Bump docker/build-push-action from 6.9.0 to 6.11.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.9.0 to 6.11.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.9.0...v6.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-10 05:46:13 +00:00
dependabot[bot]
c3d60c6586 Bump docker/setup-qemu-action from 3.2.0 to 3.3.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.2.0 to 3.3.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3.2.0...v3.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-10 05:46:10 +00:00
524596ad85 Merge branch 'cuttingedge-merge-ServerV2' into postgres-Server-V2
# Conflicts:
#	API/Schema/MangaConnectors/Mangaworld.cs
2025-01-09 01:51:07 +01:00
6aa8413c40 Fix #311 MangaWorld now requires Javascript
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-01-09 01:48:13 +01:00
94adefa8e6 Fix some Jobs 2025-01-09 01:34:03 +01:00
7cf7eb85d2 Fix #307 Chapternumbers
ChapterNumbers now can be sub-decimal, like version-numbers (x.y.z.a...)
2025-01-09 01:33:30 +01:00
55c0e2c4e7 Manga latest downloaded and available via SQL Queries 2024-12-18 16:42:59 +01:00
5494f2b754 Merge branch 'master' into postgres-Server-V2 2024-12-18 00:45:32 +01:00
b96ae4a2d2 Merge pull request #304 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.8.0
Bump docker/setup-buildx-action from 3.7.1 to 3.8.0
2024-12-17 17:38:07 +01:00
80190e1286 Job Run pass context to add new Data 2024-12-17 17:24:25 +01:00
dependabot[bot]
3a25c0b221 Bump docker/setup-buildx-action from 3.7.1 to 3.8.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.7.1 to 3.8.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.7.1...v3.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-17 05:59:19 +00:00
16dd1ffa97 Job Run pass context to add new Data 2024-12-16 23:29:57 +01:00
9cb5f636dd Fix MangaDex wrong Id in requests for new chapters 2024-12-16 23:29:42 +01:00
df319e9afb Jobs change RunningJobs to Dictionary of Thread and Job instead of just List of threads 2024-12-16 23:21:13 +01:00
84388a469a Remove unncessary nullable in Job 2024-12-16 23:20:46 +01:00
d322445550 Add "GetAll" Paths to Manga and Jobcontroller 2024-12-16 23:20:31 +01:00
81d22bc022 Newtonsoft Enum Json Converter 2024-12-16 23:20:17 +01:00
6b0cefbc7c Simplify SearchPaths 2024-12-16 23:19:59 +01:00
519030861d Move GetConnectors to new MiscController 2024-12-16 23:19:47 +01:00
6940e6c64d Include Manga and Chapter in jobs 2024-12-16 23:05:12 +01:00
e66ab49e7d ConnectorController.cs -> SearchController.cs 2024-12-16 22:59:52 +01:00
67a15cec7f Path for SearchManga 2024-12-16 22:58:44 +01:00
ae11c31b9d ConnectorController.AddMangaToContext existing also check for connector 2024-12-16 22:55:52 +01:00
60b128fc30 Fix Arrays shall not be added to context 2024-12-16 22:54:23 +01:00
729f018712 Fix relation Manga -> Tags and Manga -> Authors with many-many 2024-12-16 21:52:35 +01:00
03e89913e3 Scoped PGSql Contexts for Threads 2024-12-16 21:24:00 +01:00
c4fc2f436b Notification-Thread Implemented 2024-12-16 21:02:55 +01:00
ebc30c85bf Notification Date notnull 2024-12-16 20:35:25 +01:00
d6b0e3a366 Notification Date notnull 2024-12-16 20:34:20 +01:00
e1bfdd675b NotificationConnector SendNotification public 2024-12-16 20:33:59 +01:00
e6f8853b49 Add NotificationUrgency.cs 2024-12-16 20:14:28 +01:00
99ddb06d6d TrangaSettings remove old properties 2024-12-16 20:08:23 +01:00
62876498d0 Notification add Date 2024-12-16 20:03:45 +01:00
1044821147 Fix relation Manga->Chapter (latest) 2024-12-16 20:03:38 +01:00
7f946da1c3 Remove docs/ we have SWAGger 2024-12-16 19:50:56 +01:00
79e7941dda Remove docs/ we have SWAGger 2024-12-16 19:49:56 +01:00
faa235783c remove leftover .dockerignore 2024-12-16 19:49:17 +01:00
87c5ad001d AutoInclude Navigation on Manga and Chapter 2024-12-16 19:49:03 +01:00
3b58e0498b Fix redundant keys, MangaSearch 2024-12-16 19:25:22 +01:00
87274aca19 Remove APISerializable and APIJsonSerializer 2024-12-16 18:55:52 +01:00
77c5903cf1 Fixup Docker files 2024-12-16 18:29:57 +01:00
0d32f15ee9 PGSqlContext add MangaConnector Discriminator
API use Newtonsoft Json,
2024-12-16 18:28:58 +01:00
a0774841bc PrimaryKey Notification 2024-12-16 18:02:48 +01:00
3ee3a07565 Urgency 2024-12-16 18:02:14 +01:00
b9eecd3afd Remove excess 2024-12-16 17:47:12 +01:00
6534341fd5 Library Connector Constructors 2024-12-16 17:45:58 +01:00
6737be4a20 Notification Connector Methods 2024-12-16 17:42:16 +01:00
84833acdeb Schema add Notifications 2024-12-16 17:35:36 +01:00
538e6fa60b MangaConnectors in API 2024-12-15 23:00:35 +01:00
8c5bcd2665 Weebcentral 2024-12-14 22:02:32 +01:00
50dfd92c91 Merge branch 'Server-V2' into postgres-Server-V2 2024-12-14 21:57:47 +01:00
bf9fe517b0 Merge branch 'cuttingedge-merge-ServerV2' into Server-V2 2024-12-14 21:57:34 +01:00
e1f1a05724 Merge pull request #302 from ale-ben/feature/weebcentral_build_error
Fix build error in Weebcentral
2024-12-14 21:54:28 +01:00
1008da7ee8 Add API 2024-12-14 21:53:29 +01:00
Alessandro Benetton
72d9bda0e8 [feature/weebcentral_build_error] fix type in equality check 2024-12-14 20:44:43 +01:00
a40a9c84df Merge pull request #298 from ale-ben/feature/weebcentral
Weebcentral implementation
2024-12-14 18:42:47 +01:00
ec884f888f Merge branch 'Server-V2' into postgres-Server-V2 2024-12-14 18:07:27 +01:00
57df419d65 Merge branch 'cuttingedge-merge-ServerV2' into Server-V2 2024-12-14 18:07:22 +01:00
825b945ad1 AsuraToon Crash on no Artists or Authors
Fix #296
2024-12-14 18:02:41 +01:00
b8c624f3ea AsuraToon crash when there is no search-results #296 2024-12-14 17:55:20 +01:00
93cfdddd19 Possible fix #300 chromium statup "Failed to launch browser! chrome_crashpad_handler: --database is required" 2024-12-14 17:51:22 +01:00
Alessandro Benetton
4c8d9bfaf2 [feature/weebcentral] Added Weebcentral to readme 2024-12-14 16:29:43 +01:00
Alessandro Benetton
dd988658c0 [feature/weebcentral] Added Weebcentral to connectors 2024-12-14 16:18:15 +01:00
Alessandro Benetton
cf4c84a47f [feature/weebcentral] Working download logic 2024-12-14 00:58:52 +01:00
Alessandro Benetton
5d9bfc3adf [feature/weebcentral] Get chapters 2024-12-14 00:45:10 +01:00
Alessandro Benetton
5a770c8e9f [feature/weebcentral] Working search 2024-12-13 23:42:35 +01:00
395619acd3 Merge branch 'Server-V2' into postgres-Server-V2 2024-12-13 18:54:16 +01:00
502821c246 Merge branch 'cuttingedge-merge-ServerV2' into postgres-Server-V2 2024-12-13 18:54:02 +01:00
9d6a8ed686 Merge branch 'cuttingedge-merge-ServerV2' into Server-V2 2024-12-13 18:53:52 +01:00
e3bd7620aa Fix #296 AsuraToon
AsuraComic does not use Static sites, use Chromium instead.
Make Puppeteer spam less logs
2024-12-13 18:53:25 +01:00
afcc2cacaf merge 2024-12-12 22:48:46 +01:00
4040b5845c Merge branch 'cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Jobs/DownloadNewChapters.cs
#	Tranga/Jobs/JobBoss.cs
#	Tranga/Jobs/UpdateMetadata.cs
2024-12-12 22:47:47 +01:00
428d6e13d1 Fix UpdateJobFile with oldFile:
oldFilePath was fullname, not relative
2024-12-12 22:41:28 +01:00
1e6a65c0fd Chapter volume and chapternumber as float instead of string.
Possible fix #293
2024-12-12 22:33:13 +01:00
025d43b752 Fix duplicate job check.
We were still adding duplicate jobs if not *every* field in the Manga matched.
We now only compare publicationId.
2024-12-12 22:18:06 +01:00
113c0abba7 Merge pull request #294 from C9Glax/cuttingedge
Merge cuttingedge into master
2024-12-12 22:07:13 +01:00
7daebcb1c4 Merge branch 'cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Tranga.csproj
2024-12-12 22:05:52 +01:00
747df0bde5 Add Puppeteer Logger 2024-12-12 21:42:21 +01:00
463f360808 Dependency updates 2024-12-12 21:28:58 +01:00
44ff158c66 Merge branch 'cuttingedge-merge-ServerV2' into Server-V2 2024-12-04 19:58:48 +01:00
85d7c07b13 Mangaworld add decimal-chapters (686.5) to regex
#289
2024-12-04 19:55:31 +01:00
b5b45d0801 Merge branch 'cuttingedge-merge-ServerV2' into Server-V2 2024-12-04 19:49:56 +01:00
553f56ecaf Longer ExceptionMessage when Chapter comparison fails
#289
2024-12-04 19:49:38 +01:00
9cc4f8c090 Merge pull request #283 from C9Glax/cuttingedge-merge-candiate
AsuraToon merge
2024-11-28 21:41:19 +01:00
29f3f1a16e Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-11-28 21:35:48 +01:00
204fb7614d Fix #281 Manganato errors when there is no chapters uploaded 2024-11-28 21:35:29 +01:00
d6e73ffcdf Merge pull request #276 from C9Glax/cuttingedge-merge-candiate
Cuttingedge merge candidate
2024-11-28 21:23:56 +01:00
5a8202f872 More logging 2024-11-11 17:59:48 +01:00
1bd914571c Asuratoon Server-V2 2024-11-11 17:09:19 +01:00
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
5186ae66c9 Merge pull request #270 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.7.1
Bump docker/setup-buildx-action from 3.6.1 to 3.7.1
2024-10-23 16:11:06 +02:00
c35e1ef517 Merge pull request #269 from C9Glax/dependabot/github_actions/docker/build-push-action-6.9.0
Bump docker/build-push-action from 6.7.0 to 6.9.0
2024-10-23 16:10:52 +02:00
dependabot[bot]
8f6891142b Bump docker/setup-buildx-action from 3.6.1 to 3.7.1
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.6.1 to 3.7.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.6.1...v3.7.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-23 05:49:09 +00:00
dependabot[bot]
b52e6d4908 Bump docker/build-push-action from 6.7.0 to 6.9.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.7.0 to 6.9.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.7.0...v6.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-23 05:49:07 +00: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
30c44760e7 Merge pull request #256 from C9Glax/cuttingedge-merge-candidate
Cuttingedge merge candidate
2024-09-29 01:13:56 +02:00
a3ae3c320d Merge branch 'refs/heads/cuttingedge' into cuttingedge-merge-candidate 2024-09-29 01:07:59 +02:00
4871bc801d Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-29 01:07:51 +02:00
ea262889e6 Its late. Set TARGETPLATFORM in base 2024-09-29 01:02:50 +02:00
445542b653 Set --platform to BUILDPLATFORM for dotnet 2024-09-29 00:58:24 +02:00
b7718220ef Merge branch 'refs/heads/cuttingedge' into cuttingedge-merge-candidate 2024-09-29 00:54:28 +02:00
34c62e8658 Remove cache step from cuttingedge workflow, set --platform to TARGETPLATFORM instead 2024-09-29 00:50:53 +02:00
a9fcc93670 Merge pull request #257 from C9Glax/master
Update docker-image-cuttingedge.yml
2024-09-29 00:44:17 +02:00
68d7ef258f Update docker-image-cuttingedge.yml
Clear Cache on build
2024-09-29 00:40:59 +02:00
fdea4f5ea5 Merge branch 'cuttingedge-merge-ServerV2' into cuttingedge 2024-09-27 17:09:19 +02:00
ac3039e587 Add Star-Graph to README 2024-09-27 17:08:59 +02:00
1c5f105a4d Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-27 15:04:03 +02:00
3829a1cf26 Merge branch 'refs/heads/cuttingedge' into cuttingedge-merge-candidate 2024-09-27 15:03:51 +02:00
c3daa0b751 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-27 15:03:44 +02:00
3a072beea3 Update Readme:
* Fix dotnet Version
* Link directly to new issue for new Connectors
* Add Ntfy as Notification Connector
* Remove Roadmap
2024-09-27 15:03:06 +02:00
8e6f2798a9 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge-merge-candidate 2024-09-27 14:58:07 +02:00
26a07f4a2f Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-27 14:58:02 +02:00
9cbde9a6b4 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-27 14:57:57 +02:00
0870aa9fdb Merge branch 'refs/heads/master' into cuttingedge-merge-ServerV2 2024-09-27 14:57:36 +02:00
172650e644 Merge pull request #254 from C9Glax/cuttingedge-merge-candidate
Cuttingedge merge candidate
2024-09-27 14:53:24 +02:00
48ab44c28d Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-27 14:51:20 +02:00
52ff2e54a8 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-27 14:51:11 +02:00
61d80a93cf Fix #255 MangaKatana sanitization. 2024-09-27 14:50:57 +02:00
32ecdcda76 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-23 15:41:02 +02:00
7be3ee52e9 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-23 15:40:53 +02:00
981eb0fd9f Fix notification batching:
Do not resend old notifications.
2024-09-23 15:40:43 +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
47f3044a6d Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-22 00:15:59 +02:00
6d03cc5f8d Fix incorrect setting check for notificationsbuffer 2024-09-22 00:15:50 +02:00
290c405f52 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-22 00:09:54 +02:00
fcdbd32872 Include amount of notifications of type in title 2024-09-22 00:09:45 +02:00
eb6c37cc53 Output settings.json on startup 2024-09-22 00:05:09 +02:00
d922842186 Add NotificationBuffer, so Notification are not spammed on every chapter. 2024-09-22 00:02:43 +02:00
69323d6d60 Add LibraryBuffer, so Libraries are not spammed with scans on every download. 2024-09-21 21:02:55 +02:00
46a0fb8c48 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-21 20:34:57 +02:00
ec8eb40941 Allow Versions to lose their volume number, if site no longer lists it. 2024-09-21 20:30:55 +02:00
d2074fae35 Readable CheckChapterIsDownloaded check 2024-09-21 20:23:21 +02:00
713bbc230f Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-18 18:56:09 +02:00
32ab9a552f Also delete files on UpdateJobFile if we dont provide a filepath 2024-09-18 18:56:01 +02:00
7b6724ad38 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-18 18:47:28 +02:00
c11c68d6d7 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-18 18:46:02 +02:00
09fdb6e5f1 Fix #250 old jobs getting re-exported. 2024-09-18 18:45:55 +02:00
be68ddc9b7 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-17 00:52:06 +02:00
e86ad03b1e Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-17 00:51:30 +02:00
9dfbe89e87 include --platform=$BUILDPLATFORM in Dockerfile 2024-09-17 00:51:22 +02:00
96e2845a5b Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-17 00:23:49 +02:00
98e75af486 Merge branch 'cuttingedge' of ssh://git.bernloehr.eu:222/glax/Tranga into cuttingedge 2024-09-16 23:21:13 +02:00
e2f5c3badc Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-16 23:18:57 +02:00
cda07bb9aa Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-16 23:09:43 +02:00
7c18466e95 Fix NETSDK1194 on build 2024-09-16 23:09:34 +02:00
c36204c7a8 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-16 22:51:57 +02:00
ce1c4d3f65 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-16 22:48:06 +02:00
52d0489a1b Fix duplicate mangas on startup 2024-09-16 22:47:55 +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
f89aea6ac8 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-16 21:19:27 +02:00
5f05ba1049 Make SupportedLanguages public. 2024-09-16 21:19:19 +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
a20ee01cfa Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-16 21:17:18 +02:00
cf5cbba9a8 #247 Add supported languages to Mangaconnectors 2024-09-16 21:17:07 +02:00
600b56033d Upgrade to Dotnet 8.0 LangVer 12 2024-09-16 21:11:50 +02:00
fdea3659f1 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-16 20:38:19 +02:00
7f3754fb64 Fix startup issue/issue with existing chapters: ProgressToken would not complete 2024-09-16 20:36:40 +02:00
2dac5db4da Create single Chromium Instance that is shared between all Connectors.
Fix pages staying open when page could not be loaded.
2024-09-16 20:30:23 +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
3456fc6564 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-16 19:52:39 +02:00
35f2625f05 Fix #249 Manhuaplus where author/tags are not set. 2024-09-16 19:52:25 +02:00
0b9948e367 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-16 18:32:45 +02:00
96f3dbce65 Throw more readable exceptions if deserialization fails for Mangaconnectors.
#249
2024-09-16 18:32:34 +02:00
895128a462 Merge remote-tracking branch 'origin/cuttingedge-merge-ServerV2' into cuttingedge-merge-ServerV2 2024-09-16 18:24:39 +02:00
3b9d4a6735 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-09-11 14:41:49 +02:00
a94186455b Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-09-11 14:41:35 +02:00
7d3deee74c Remove unused constant 2024-09-11 14:40:28 +02:00
5980b64caa Readable Chapter comparison 2024-09-11 14:40:03 +02:00
cbecb257ef Remove unused constant 2024-09-11 14:39:16 +02:00
8316ed08a7 Merge pull request #245 from C9Glax/cuttingedge
Prod didn't break, nice
2024-09-09 10:10:36 +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
7ff9ac53ee Build all docker images with new workflow #233 2024-09-09 09:42:52 +02:00
6faaaf4139 Fix #243 Moving Publication folders, overwrite files, merge folders 2024-09-09 09:23:25 +02:00
9b8b80cd24 Fix response closed on OPTIONS request 2024-09-07 20:44:15 +02:00
15f3e2b8ec Use current time as internalId for Manga instead of BASE64 string of title
#232
Fix #237
2024-09-07 20:33:03 +02:00
2be29e4019 MangaDex only download single release for chapter.
Fix #219
2024-09-07 20:16:05 +02:00
e8dbf7a718 Merge pull request #233 from vonProteus/arm64
Added support for ARM
2024-08-31 20:57:44 +02:00
vonProteus
a968f4328d Added support for ARM 2024-08-31 20:38:10 +02:00
398b6fff05 Merge pull request #230 from C9Glax/cuttingedge-merge-candidate
Cuttingedge merge candidate
2024-08-31 20:25:33 +02:00
f5da2f8526 Merge pull request #231 from C9Glax/dependabot/github_actions/docker/build-push-action-6.7.0
Bump docker/build-push-action from 6.6.1 to 6.7.0
2024-08-31 20:24:43 +02:00
dependabot[bot]
73093ab86c Bump docker/build-push-action from 6.6.1 to 6.7.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.6.1 to 6.7.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.6.1...v6.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 05:55:58 +00:00
75eea8c761 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-08-26 20:47:19 +02:00
fccaf9fcbe Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-26 20:47:06 +02:00
3122aa32e8 fix #223 wrong selector 2024-08-26 20:46:50 +02:00
06cdbbd283 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-08-26 20:28:57 +02:00
02fad2dd44 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-26 20:28:51 +02:00
e0a7d1a187 Fix #220 Mangaworld Chapter number parsing 2024-08-26 20:28:40 +02:00
054c88712e Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-08-26 20:18:51 +02:00
d0f9a4102c Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-26 20:18:44 +02:00
9f178821b6 Fix #223 Manganato chapter relative dates. 2024-08-26 20:18:35 +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
682fd0bc2a Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-26 13:22:09 +02:00
dfa8e66f34 Fix try-block in Server.cs 2024-08-26 13:21:54 +02:00
8f51d22303 Fix try-block in Server.cs 2024-08-26 13:21:34 +02:00
d41de84262 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge
# Conflicts:
#	Tranga/Server.cs
2024-08-26 13:21:05 +02:00
1bd20791b8 Add Cache-Control headers 2024-08-26 13:18:48 +02:00
03aeab44cd Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-26 13:11:41 +02:00
6d723b6355 Fix Settings not returning as JSON 2024-08-26 13:11:00 +02:00
7b91bb699f Fix Settings not loading on reload 2024-08-26 13:10:47 +02:00
14e33cc496 Fix Settings not loading on reload 2024-08-26 13:09:33 +02:00
6f3bba99b0 Fix Settings not returning as JSON 2024-08-26 12:59:19 +02:00
2d848843d0 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-26 12:37:03 +02:00
63b493fa9c Rework TrangaSettings 2024-08-26 12:36:35 +02:00
001a37b8ef Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-26 11:18:12 +02:00
69d6884517 #227 Fix wrong filtering, only return top 10 results 2024-08-26 11:17:59 +02:00
db73af3bdd Fix crash when outputstream closes before response could be sent.
#227
2024-08-26 10:38:45 +02:00
59547efab2 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-26 10:35:37 +02:00
f4336f9777 #227 Mangasee Return results that have similarity over 95% or at least top ten results 2024-08-26 10:35:16 +02:00
bec3ac52a9 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-20 20:53:09 +02:00
ea37e81ece Fix last commit 2024-08-20 20:53:03 +02:00
6a20783d48 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-20 20:47:21 +02:00
21af75f410 Faster download for images-urls.
#224
2024-08-20 20:47:13 +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
a629792818 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-08 21:09:26 +02:00
34dd78810d Update README.md 2024-08-08 21:09:08 +02:00
e1c504226c Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-08 21:04:09 +02:00
200a22228f add log output for Mangahere
https://github.com/C9Glax/tranga/issues/69
2024-08-08 21:02:13 +02:00
bc10136331 MangaHere image download sucks, you have to iterate all over all images one by one. Have some extra traffic then, idc.
https://github.com/C9Glax/tranga/issues/69
2024-08-08 21:00:37 +02:00
06df6e0767 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-08-08 19:00:26 +02:00
ba029b71f5 Merge branch 'refs/heads/manhuaplus' into cuttingedge-merge-ServerV2 2024-08-08 19:00:20 +02:00
082802ddbe Merge branch 'refs/heads/master' into cuttingedge-merge-ServerV2 2024-08-08 19:00:09 +02:00
d5f1df0400 Merge pull request #216 from C9Glax/dependabot/github_actions/docker/build-push-action-6.6.1
Bump docker/build-push-action from 6.5.0 to 6.6.1
2024-08-08 18:59:46 +02:00
d00881e611 Add Connector ManhuaPlus
https://github.com/C9Glax/tranga/issues/213
2024-08-08 18:58:40 +02:00
dependabot[bot]
72bc7ec07b Bump docker/build-push-action from 6.5.0 to 6.6.1
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.5.0 to 6.6.1.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.5.0...v6.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-08 05:08:32 +00:00
89b5aa266e Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-07-31 19:25:03 +02:00
926c0d5833 fix #214 foldernames 2024-07-31 19:24:59 +02:00
80e2568113 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-07-31 17:48:21 +02:00
3b6417eff2 Fix #214 HTML encoded Characters 2024-07-31 17:48:15 +02:00
6b9ddca711 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-07-31 17:44:42 +02:00
2812a6dff1 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-07-31 17:44:37 +02:00
1991862a42 Merge remote-tracking branch 'refs/remotes/github/master' into cuttingedge-merge-ServerV2 2024-07-31 17:44:22 +02:00
40e4d5c203 Merge pull request #215 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.6.1
Bump docker/setup-buildx-action from 3.4.0 to 3.6.1
2024-07-31 17:44:05 +02:00
49e9731184 Merge pull request #212 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.2.0
Bump docker/setup-qemu-action from 3.1.0 to 3.2.0
2024-07-31 17:43:57 +02:00
a4e85f254f Merge pull request #210 from C9Glax/dependabot/github_actions/docker/build-push-action-6.5.0
Bump docker/build-push-action from 6.3.0 to 6.5.0
2024-07-31 17:43:48 +02:00
dependabot[bot]
4f47aeadcf Bump docker/setup-buildx-action from 3.4.0 to 3.6.1
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.4.0 to 3.6.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.4.0...v3.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-30 05:45:04 +00:00
dependabot[bot]
e0c1356fea Bump docker/setup-qemu-action from 3.1.0 to 3.2.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.1.0 to 3.2.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3.1.0...v3.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-23 06:02:31 +00:00
dependabot[bot]
0d9b3d2499 Bump docker/build-push-action from 6.3.0 to 6.5.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.3.0 to 6.5.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.3.0...v6.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-23 06:02:27 +00:00
d73bf70868 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2
# Conflicts:
#	Tranga/Server.cs
2024-07-11 15:46:35 +02:00
8e5d15ead9 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-07-11 15:46:27 +02:00
b8c28e6d21 Merge pull request #207 from C9Glax/master
Update active dev branch with changes to master
2024-07-11 15:45:33 +02:00
9ea5e436fe Merge pull request #204 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.4.0
Bump docker/setup-buildx-action from 3.3.0 to 3.4.0
2024-07-11 15:44:39 +02:00
b4c310638a Merge pull request #205 from C9Glax/dependabot/github_actions/docker/build-push-action-6.3.0
Bump docker/build-push-action from 6.1.0 to 6.3.0
2024-07-11 15:44:17 +02:00
159341ff3c Merge pull request #206 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.1.0
Bump docker/setup-qemu-action from 2.2.0 to 3.1.0
2024-07-11 15:43:58 +02:00
dependabot[bot]
29338b9b17 Bump docker/setup-qemu-action from 2.2.0 to 3.1.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2.2.0 to 3.1.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2.2.0...v3.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-10 05:46:20 +00:00
dependabot[bot]
0eda8913b0 Bump docker/build-push-action from 6.1.0 to 6.3.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.1.0 to 6.3.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.1.0...v6.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-10 05:46:17 +00:00
dependabot[bot]
5ca50630e4 Bump docker/setup-buildx-action from 3.3.0 to 3.4.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.3.0...v3.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-10 05:46:15 +00:00
d0bfb262bf Merge remote-tracking branch 'refs/remotes/github/master' into cuttingedge-merge-ServerV2 2024-07-09 11:22:05 +02:00
4f14f15ade Merge pull request #200 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.1.0
Bump docker/setup-qemu-action from 2.2.0 to 3.1.0
2024-07-09 11:20:29 +02:00
d89a24fd11 Merge pull request #201 from C9Glax/dependabot/github_actions/docker/build-push-action-6.3.0
Bump docker/build-push-action from 6.1.0 to 6.3.0
2024-07-09 11:20:14 +02:00
a5859e3c82 Merge pull request #203 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.4.0
Bump docker/setup-buildx-action from 3.3.0 to 3.4.0
2024-07-09 11:19:55 +02:00
dd2fa3fbd7 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-07-09 11:17:58 +02:00
33e5d65785 fix Kavita GetLibraries 2024-07-09 11:17:50 +02:00
dependabot[bot]
d60ed77dbe Bump docker/setup-buildx-action from 3.3.0 to 3.4.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.3.0...v3.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-05 05:11:09 +00:00
dependabot[bot]
e15c6816b5 Bump docker/build-push-action from 6.1.0 to 6.3.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.1.0 to 6.3.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.1.0...v6.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-04 05:56:47 +00:00
dependabot[bot]
4a4fe4b40d Bump docker/setup-qemu-action from 2.2.0 to 3.1.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2.2.0 to 3.1.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2.2.0...v3.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-04 05:56:42 +00:00
d221532e0d Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-06-29 22:50:14 +02:00
4881789970 Merge branch 'refs/heads/cuttingedge' 2024-06-29 22:50:07 +02:00
be1e6fe988 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-06-29 22:49:56 +02:00
f61e51e506 Fix crash when moving files, now overwrites. 2024-06-29 22:49:39 +02:00
eba511749b Merge pull request #199 from C9Glax/cuttingedge
Merge cuttingedge to latest.
2024-06-29 19:49:06 +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
de4c57a0cd Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-06-29 19:37:09 +02:00
e368c3c98a Fix https://github.com/C9Glax/tranga/issues/193
Mangaworld Volume and Chapter number Parsing.
2024-06-29 19:37:02 +02:00
f3e0959be8 Merge pull request #198 from C9Glax/master
Merge Github Actions
2024-06-29 19:23:37 +02:00
d17ca1d97a Merge pull request #197 from C9Glax/master
Merge Github Actions
2024-06-29 19:22:59 +02:00
e9376e3782 Merge pull request #196 from C9Glax/master
Merge Github Actions
2024-06-29 19:21:41 +02:00
7c217a7e33 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-06-29 19:20:16 +02:00
a437fcbca1 Possible fix https://github.com/C9Glax/tranga/issues/185
Mangaworld publication id had invalid path characters.
2024-06-29 19:20:04 +02:00
1dcfecd66f Create CoverImageCache when saving coverimages. 2024-06-29 19:14:37 +02:00
6db4646336 Move/rename archives if volume number gets updated. 2024-06-29 19:11:18 +02:00
8a6298e3fd Merge pull request #157 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.3.0
Bump docker/setup-buildx-action from 3.1.0 to 3.3.0
2024-06-27 00:08:31 +02:00
194705c124 Merge pull request #194 from C9Glax/dependabot/github_actions/docker/build-push-action-6.1.0
Bump docker/build-push-action from 5.3.0 to 6.1.0
2024-06-27 00:06:28 +02:00
dependabot[bot]
f4d5969003 Bump docker/build-push-action from 5.3.0 to 6.1.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.3.0 to 6.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5.3.0...v6.1.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 05:58:28 +00:00
8607bd2c89 #187 NTFY JsonConverter 2024-06-15 21:40:28 +02:00
9d92069a4b #187 NTFY JsonConverter 2024-06-15 21:39:53 +02:00
5614729eab #187 Server v1 NTFY username password 2024-06-15 21:33:42 +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
d52ec8d36f NTFY username and password usage instead of auth. 2024-06-15 21:24:28 +02:00
790e77b00c Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-06-02 01:05:29 +02:00
37dfb4df02 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-06-02 01:05:20 +02:00
42feea3ad5 Fix covers returning wrong fileLocation if cover already exists. 2024-06-02 01:05:08 +02:00
4f14903538 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-06-02 00:23:29 +02:00
bbc750d731 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-06-02 00:23:23 +02:00
08dd01942f #183 Fix NTFY not exporting topic to notificationConnectors.json 2024-06-02 00:23:16 +02:00
6ae3918679 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-06-02 00:11:33 +02:00
351144e763 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-06-02 00:09:18 +02:00
aea4c0c61b Add GlaxArguments to fetch Runtime-Args 2024-06-02 00:09:03 +02:00
7b9e935db7 Commented optional second level only domains for cover-image-names 2024-06-01 22:10:09 +02:00
048b165d76 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-06-01 22:09:18 +02:00
ebe3012c69 NTFY check endpoint URI and add optional custom topic #183 2024-06-01 22:09:08 +02:00
8ccb6c0cb5 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-05-26 23:04:35 +02:00
a5dbed9525 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-05-26 23:04:27 +02:00
811ddd903f fix missing minus-sign from domain namers in coverimages 2024-05-26 23:04:16 +02:00
beb455308f Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-05-26 22:52:05 +02:00
f948809bcd Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-05-26 22:51:59 +02:00
7ceb9cd4cb #182 Changed filename to instead of remote filename have the format server-internalId.fileFormat 2024-05-26 22:51:46 +02:00
57f1e037ef Corrected check for if cover exists 2024-05-26 22:45:39 +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
6ca8d58e43 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-05-26 18:46:58 +02:00
e3211b95e2 #182 Remove covers that have no asssociated Manga 2024-05-26 18:46:40 +02:00
b5e9e03f64 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-05-26 18:34:57 +02:00
98bd8a983b Possible Fix #182 2024-05-26 18:34:45 +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
f4996659ef Fix loading file results in "null"-job and crashes. 2024-05-26 18:23:16 +02:00
e05684d5d1 Fix loading file results in "null"-job and crashes. 2024-05-26 18:22:51 +02:00
4a7d23c0d9 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-05-26 18:10:45 +02:00
1d44b6d9c6 Log added Jobs during Startup 2024-05-26 18:10:29 +02:00
2cfc7ac2c5 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into Server-V2 2024-04-27 19:09:31 +02:00
811a183af2 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-04-27 19:09:22 +02:00
fb0755eb89 Use NeedlemanWunsch for string comparison on Mangasee.cs
Resolves #132
#167
2024-04-27 19:09:12 +02:00
2e8b896f3b Fix #178 wrong check on parsing variable aprilfoolsmode 2024-04-27 17:53:08 +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
4692cc297a Fix MangaDex linksNode is null 2024-04-26 00:48:55 +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
3d855020eb Export job files indented. 2024-04-25 21:32:48 +02:00
c6d0168d2f Fix #174 auth not being written to file for ntfy. 2024-04-25 21:29:05 +02:00
d52213002e Delete old jobfiles. 2024-04-25 21:24:29 +02:00
ec9290f41f Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge
# Conflicts:
#	Tranga/Jobs/UpdateMetadata.cs
2024-04-25 21:10:42 +02:00
6b91796e5a Update manga in DownloadNewChapters Jobs 2024-04-25 21:10:26 +02:00
9f9ea569d5 fix bug Manga.WithMetadata coverfilenameincache not being replaced. 2024-04-25 21:03:57 +02:00
4bd1150a0e fix bug Manga.WithMetadata coverfilenameincache not being replaced. 2024-04-25 21:03:44 +02:00
8b62e2c467 Possible fix #175 Export jobs when Manga-Metadata is updated. 2024-04-25 20:57:59 +02:00
7ec262a2e4 Possible fix #175 Export jobs when Manga-Metadata is updated. 2024-04-25 20:57:46 +02:00
d32d5976ee Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-04-25 20:46:32 +02:00
58cff6513a Possible fix #175 2024-04-25 20:46:26 +02:00
783f229a6a Add LibraryConnector.Test to see if requests can be made to endpoint. 2024-04-23 00:58:33 +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
aaf06da8e1 Merge branch 'refs/heads/cuttingedge-merge-ServerV2' into cuttingedge 2024-04-23 00:20:50 +02:00
51a26a3cba Fix https://github.com/C9Glax/tranga/issues/143
ImageCache could never find files, because they were not in the expected location.
2024-04-23 00:20:34 +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
762da4c859 Make cachedPublications private with getter-setter 2024-04-22 22:43:42 +02:00
daba940b45 Make cachePublications a dictionary with internalId as key. 2024-04-22 22:38:23 +02:00
79e61a62c7 Export Jobfiles after execution, update metadata in jobfiles 2024-04-22 22:29:22 +02:00
06fe98323a Fix crashing when comparing old Manga (missing websiteUrl) 2024-04-22 22:09:43 +02:00
5f820c53f5 Update websiteUrl on metadata-refresh https://github.com/C9Glax/tranga-website/issues/60 2024-04-22 22:03:09 +02:00
c69f1f6569 Addresses #170 Manganato authors and genres include "\r\n" 2024-04-22 04:45:49 +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
5bdbd9e2e4 Hack to resolve #60 Website-URL.
Field will have same name, just acquisition will be better.
2024-04-22 02:25:39 +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
f729c44f88 Merge branch 'refs/heads/master' into cuttingedge 2024-04-20 18:49:19 +02:00
8b9769b816 Merge branch 'refs/heads/master' into Server-V2 2024-04-20 18:49:08 +02:00
f4966b0348 Docker Image build 2024-04-20 18:48:51 +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
df2fc4a036 Remove README CLI reference 2024-04-20 18:39:49 +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
0ab2ae03ce unionby isntead of concat 2024-04-19 03:07:46 +02:00
95236daf41 Check if tags and authors are the same on Manga equals.
UpdateManga performs union/concat operation on alttitles, tags and authors
2024-04-19 03:00:31 +02:00
294ce01bc3 Set Manga.releaseStatus to new releaseStatus.
Fix #119
2024-04-19 02:37:17 +02:00
13565d1c7a Fixes #166 MangaDex crash on UpdateMetadata, needed to include cover_art in request 2024-04-19 02:21:20 +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
54b24ac37f Merge remote-tracking branch 'refs/remotes/db-2001/cuttingedge' into cuttingedge 2024-04-19 00:10:14 +02:00
c67e89f1dd null checks 2024-04-19 00:07:34 +02:00
Dity
4ba44d3ac3 Merge branch 'C9Glax:cuttingedge' into cuttingedge 2024-04-18 18:04:07 -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
8631cf6376 Merge pull request #161 from C9Glax/MangaDexRequestLimitChange
MangaDex request limit change
2024-04-18 23:54:44 +02:00
df4d547e2b Fix crash with old settings files 2024-04-18 23:52:52 +02:00
db-2001
006b71b496 Merge remote-tracking branch 'upstream/cuttingedge' into cuttingedge 2024-04-18 17:48:43 -04:00
5f03b0d89c Closes #154 2024-04-18 23:05:04 +02:00
6dc1ea0030 Merge branch 'refs/heads/master' into cuttingedge 2024-04-18 22:52:51 +02:00
ff08754610 Bump docker/setup-buildx-action@v3.3.0
Bump docker/build-push-action@v5.3.0
2024-04-18 22:52:38 +02:00
d1a6c0ad3d Set Chromium Start Timeout to 30 seconds.
Resolves #135 ?
2024-04-18 22:13:10 +02:00
0260868968 Merge pull request #163 from C9Glax/cuttingedge
Connector Bugs, AprilFools Mode
2024-04-18 21:29:40 +02:00
b1f72dcb81 Legacy RateLimit remove 2024-04-18 19:00:28 +02:00
b0f353819b Legacy RateLimit 2024-04-18 18:58:42 +02:00
8f8d019861 Streamlined MangaDex information retrieval 2024-04-18 18:56:34 +02:00
21a7392493 Resolves #160, Rated Manga on Mangadex. 2024-04-18 18:01:02 +02:00
db-2001
0d5db15f87 Merge remote-tracking branch 'upstream/cuttingedge' into cuttingedge 2024-04-16 21:51:58 -04:00
431fde0d76 Wrong April Fools check.
Resolves https://github.com/C9Glax/tranga/issues/159
2024-04-16 04:18:56 +02:00
e022bf3081 Merge branch 'cuttingedge' into dev 2024-04-15 15:02:52 +02:00
c25a4f69ec Cleanup 2024-04-15 14:51:01 +02:00
82bdb248b9 userAgent private set in settings 2024-04-15 14:50:44 +02:00
b27114eaad April Fools Mode
https://github.com/C9Glax/tranga/issues/155
2024-04-15 14:50:03 +02:00
Dity
051eb4a417 Merge pull request #158 from db-2001/cuttingedge
Reimplement Fix for Mangasee
2024-04-14 14:35:06 -04:00
db-2001
482704af2c Merge remote-tracking branch 'upstream/cuttingedge' into cuttingedge 2024-04-14 14:29:30 -04:00
dependabot[bot]
af4229920d Bump docker/setup-buildx-action from 3.1.0 to 3.3.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.1.0 to 3.3.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.1.0...v3.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-09 05:32:25 +00: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
537ad3a5f8 https://github.com/C9Glax/tranga/issues/142
Cleanup old temporary Folders and files
2024-04-01 20:35:47 +02:00
6a8697fc3a Manga4Life fix bug that made it impossible for Manga to be loaded if they did not have a "Load more Chapters" button.
https://github.com/C9Glax/tranga/issues/149
Created a check if the button exists before trying to click it.
2024-04-01 20:12:25 +02:00
94582496ef Mangadex do not try downloading externally linked chapters, or chapters that have no pages.
https://github.com/C9Glax/tranga/issues/153
2024-04-01 20:00:02 +02:00
17ef5eae0f Fix MangaDex request for new Chapter. 2024-03-30 21:53:11 +01:00
db-2001
d5b6d4e8ee Fixes for https://github.com/C9Glax/tranga/issues/138 and bug fix for MDex 2024-03-29 23:59:16 -04:00
db-2001
05190bc9e2 Holy moly a fix for Mangasee 2024-03-26 18:16:41 -04:00
db-2001
d211dd2d01 Added check to prevent creation of empty chapter files 2024-03-18 22:32:26 -04:00
590547e407 Add Logline to print current logfilePath. 2024-03-05 02:55:10 +01:00
2ad04c5c46 Change LogFilePath to LogFolderPath
#139
2024-03-05 02:35:47 +01:00
189569ccdf dev image 2024-02-28 20:38:22 +01:00
2872eeea09 Merge pull request #134 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-3.1.0
Bump docker/setup-buildx-action from 2.10.0 to 3.1.0
2024-02-28 07:03:31 +01:00
dependabot[bot]
c0cfeaa35d Bump docker/setup-buildx-action from 2.10.0 to 3.1.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.10.0 to 3.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.10.0...v3.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-28 06:02:59 +00:00
2fd780996c Dockerfile maddnesssss 2024-02-28 04:03:53 +01:00
b390bb8ea5 LogFilePath 2024-02-28 03:59:09 +01:00
847829e617 Corrected DockerFile Arguments 2024-02-28 03:56:24 +01:00
0f29da00de Merge pull request #122 from C9Glax/tranga-website-41
Website Changes
2024-02-28 03:22:42 +01:00
9b2a6de841 Merge pull request #133 from C9Glax/cuttingedge
RateLimits, FileNames, Volume/Chapter Numbers
2024-02-28 02:49:48 +01:00
17a27c9922 Reset RequestLimits 2024-02-28 02:33:43 +01:00
6c9071b22b Reset UserAgent 2024-02-28 02:32:36 +01:00
abfe42b7c1 Reset UserAgent when Empty 2024-02-28 02:25:46 +01:00
72ae124418 Handle unauthorized kavita 2024-02-28 02:25:17 +01:00
bee6e7ba37 Export settings after updating rateLimits 2024-02-28 02:23:58 +01:00
8079ffc742 GlobalBase static is FileInUse 2024-02-28 02:17:48 +01:00
6d6e33491b Indented Json 2024-02-28 02:15:04 +01:00
a8697a14a3 GlobalBase static is FileInUse 2024-02-28 02:14:58 +01:00
e2adac937a Fix settings not being loaded from settingsfile 2024-02-28 02:13:18 +01:00
b4708c5d10 Encoding 850 issue for jsonconvert 2024-02-28 02:12:23 +01:00
597abde115 Fix wrong chapter (and volume) numbers for chapters 2024-02-27 22:04:14 +01:00
2a824bbb8d Correct "1" ChapterNumbers for Mangasee 2024-02-12 21:04:14 +01:00
9691eb0d08 Correct ChapterNumbers for Mangasee 2024-02-12 21:02:01 +01:00
4888e18fd2 Correct ChapterNumbers for Mangasee 2024-02-12 20:49:33 +01:00
0aa92a7913 Correct VolumeNumbers for Mangasee 2024-02-12 11:22:19 +01:00
db53e2156b API added POST
NotificationConnectors/Reset
LibraryConnectors/Reset
2024-02-11 20:44:27 +01:00
1cce0f204e API added POST
NotificationConnectors/Test
LibraryConnectors/Test
2024-02-11 20:41:55 +01:00
ce41c49a0e Merge branch 'master' into tranga-website-41 2024-02-11 01:11:41 +01:00
b8570e5eef Merge branch 'master' into cuttingedge 2024-02-11 01:11:34 +01:00
1f24a2349d Do not build latest/master on pull 2024-02-11 01:11:23 +01:00
ca95460218 https://github.com/C9Glax/tranga/pull/122
https://github.com/C9Glax/tranga-website/pull/41
LogFile
Enable LogFiles
2024-02-11 01:06:40 +01:00
e801cc4cbf #122 RateLimit GET
https://github.com/C9Glax/tranga-website/pull/41
2024-02-11 00:49:26 +01:00
2c4c8de8b5 Remove StyleSheet from TrangaSettings 2024-02-11 00:39:21 +01:00
0b4461265c #109 Rate Limits
Moved Config for RateLimits to TrangaSettings
Updated API: Settings/customRequestLimit
requestType in RequestType.cs
requestsPerMinute as int
2024-02-11 00:35:33 +01:00
c008d55f26 #103 Regeeeeex 2024-02-08 11:05:44 +01:00
9b990aecea With a passion 2024-02-07 19:40:07 +01:00
299fa6afda I hate Regex 2024-02-07 19:37:35 +01:00
c03e927565 Fix Mangaworld #103 Plurals 2024-02-07 19:23:55 +01:00
bb6c553afa One more Regex... 2024-02-07 19:05:11 +01:00
33d78ed757 https://github.com/C9Glax/tranga/issues/111#issuecomment-1932447848 2024-02-07 18:18:33 +01:00
84272ddd1e https://github.com/C9Glax/tranga/issues/111#issuecomment-1932447848 2024-02-07 18:08:57 +01:00
2f0fbbd3cb #111 Fix renaming of chapters.
Fixed check if Chapter exists
2024-02-07 15:50:26 +01:00
5bc414fd59 #113 old formatting of fileNames 2024-02-07 15:34:20 +01:00
2eaeadb92c #113 whitespaces 2024-02-07 15:29:42 +01:00
d8df6eccb1 Mangasee fix cloudflare 520 2024-02-07 14:53:57 +01:00
db64b717eb Fix regex for parsing publicationId 2024-02-02 19:38:16 +01:00
1afe36a525 add todo 2024-02-02 18:46:09 +01:00
aa692f6978 #108 2024-02-02 18:45:12 +01:00
c706824222 Merge pull request #110 from C9Glax/cuttingedge
Update Master
2024-01-31 19:14:41 +01:00
3ca6245fc2 safe Useragent as string and export settings after changing 2024-01-31 19:00:38 +01:00
2dd82aad13 https://datatracker.ietf.org/doc/html/rfc2616 2024-01-31 18:46:37 +01:00
3c4867a276 #105 2024-01-31 18:39:34 +01:00
bae157cdb4 Cleanup #90 2024-01-31 18:39:34 +01:00
3b818ff1af typo 2024-01-31 18:39:34 +01:00
5d12be2983 Fix crash when Request times out on ChromiumDownloadClient 2024-01-31 18:39:34 +01:00
31a4e693e0 Custom Request Limits #109 2024-01-31 18:39:34 +01:00
e49db9a4cb Change toplevel domain #103 2024-01-25 16:40:04 +01:00
54142e61fe Fix #103 2024-01-20 17:20:56 +01:00
cd5ca0e302 Fix #90 2024-01-20 16:44:22 +01:00
95da900213 Add url to Request-Error Output 2024-01-20 16:33:47 +01:00
b5be4e0dd8 Fixes #97 missing jobs.
Implemented Equals(obj) functions for Chapter, DownloadChapter and DownloadNewChapters to check if jobs already exist.
2024-01-11 20:19:04 +01:00
0c135aa89e Fixes #97 because stupid 2024-01-06 17:12:36 +01:00
e11ee4dafe Fixes #98 VolumeNumber can not be null for comparison 2024-01-04 17:04:08 +01:00
05573f65f9 #96 Added single click to load all chapters. 2024-01-03 18:37:29 +01:00
d986c808e3 Chapter as Comparable 2024-01-03 18:37:12 +01:00
5df63b00c2 Moved Struct RequestResult to own file 2024-01-03 17:31:00 +01:00
903bb5af5e Resolves #97 Manga4Life Volume Numbers 2024-01-03 17:05:33 +01:00
cc8453d4a8 #85 included characters with accents, umlauts, and + 2023-12-24 16:52:24 +01:00
800d4c1ec1 Amend 29f6de2590
Fix #87, manga that return no chapters, crash when updating latest released chapter.
2023-12-24 16:43:49 +01:00
b4f97eefcf Fix comparisons 2023-12-24 16:34:54 +01:00
29f6de2590 Catch parsing error #93 to prevent crashes and restart loops 2023-12-24 16:27:20 +01:00
23e5c4a7b1 Fix #93 2023-12-24 16:20:06 +01:00
e15717cb04 Merge pull request #84 from arxae/mangakatana_input_string_not_correct_format
Fixed input string not being in correct format
2023-11-13 11:54:02 +01:00
Andy Maesen
b995fc568a Requested changes 2023-11-13 06:49:20 +01:00
442d949371 Fix #80 UpdateMetaData failing 2023-11-12 13:03:33 +01:00
263d0e6036 Fix #82 Tranga crashes when cover is missing from imageCache.
Retrying download of cover and copy
2023-11-12 12:39:32 +01:00
Andy Maesen
7c7d43021e Fixed input string not being in correct format 2023-11-12 05:38:06 +01:00
5cdc7d7207 Fix wrong jobtype 2023-11-05 16:14:23 +01:00
1bcbd1517f Addresses #81 2023-11-05 16:14:12 +01:00
b72da45ae9 Add GetMangaFromId for MangaWorld 2023-11-02 15:58:16 +01:00
01041e43ac Fix publicationId for MangaWorld 2023-11-02 15:58:04 +01:00
4c1a659f16 Add API: POST Jobs/UpdateMetadata 2023-11-02 15:48:46 +01:00
2e02f0b237 Exception message. 2023-11-02 15:48:31 +01:00
77f93d87f9 UpdateMetadata now finishes correctly. 2023-11-02 15:48:17 +01:00
45c0f19a9d Added override Manga.Equals 2023-11-02 15:48:03 +01:00
7c09deb143 Remove Manga.WebsiteUrl 2023-11-02 15:47:43 +01:00
449d406eab Add MangaConnector.GetMangaFromId 2023-11-02 15:47:16 +01:00
083ce238d8 Add UpdateMetadata Job to DownloadNewChapters 2023-11-02 15:20:34 +01:00
5f9ffb8aad Improved UpdateMetadata 2023-11-02 15:20:20 +01:00
92bc3d5aa8 Catch HttpRequestException in LibraryConnector 2023-11-02 15:19:56 +01:00
49ab8928b1 Add parameter JobBoss to Job.ExecuteTask (and Internal) 2023-11-02 15:19:36 +01:00
391efcb9bc Add Field jobType to Job 2023-11-02 15:18:41 +01:00
963ad375e8 Add Job UpdateMetadata --> untested! 2023-11-01 14:17:11 +01:00
0a5ded2036 Add field WebsiteUrl to Manga 2023-11-01 14:15:55 +01:00
4843c7f05c Overwrite SeriesInfo.json parameter in SaveSeriesInfoJson. 2023-11-01 14:04:35 +01:00
6adbda2359 #77 Added field releaseStatus to Manga 2023-11-01 13:59:21 +01:00
425cf7e0d6 Re-add forgotten seriesInfo.json to new downloads 2023-11-01 13:36:58 +01:00
8f5dd5aab5 #78 Manganato chapternumber parsing from url 2023-11-01 13:22:33 +01:00
733ae285f1 #76 debug 2023-10-31 16:46:41 +01:00
2e1c8ce34f #75 Reimplemented own search.
At the moment returns too many results, levenshtein distance still too inefficient.
2023-10-31 15:47:39 +01:00
c965bc38d1 https://github.com/C9Glax/tranga-website/issues/19
Wrong regex for URLs with ports
2023-10-30 19:30:51 +01:00
37266ea095 https://github.com/C9Glax/tranga-website/issues/19
Add exception handling if host doesnt exist
2023-10-30 13:48:25 +01:00
8caac538c9 https://github.com/C9Glax/tranga-website/issues/19 Send a badrequest response if not a valid libraryconnector 2023-10-30 13:39:50 +01:00
7c7f711bb4 https://github.com/C9Glax/tranga-website/pull/17 2023-10-28 12:47:13 +02:00
d78897eb74 #74 untested 2023-10-27 14:09:34 +02:00
438c11af4f #73 api side, untested 2023-10-27 13:47:37 +02:00
38df54baff Exception handling on request failed HttpDownloadClient 2023-10-25 18:22:00 +02:00
98d187d133 Possible fix #72
Volume Numbers broke Regex
Now can also parse volume numbers!
2023-10-25 18:16:26 +02:00
5352cca058 Possible fix for #72
RegexMatching was off for last element sometimes on bato
2023-10-23 17:01:26 +02:00
3381909afd Fix #72 Chapternumber Parsing Bato 2023-10-21 15:44:37 +02:00
7219641859 #68 Because XML is sometimes broken, we parse from somewhere else
Also fixed the faulty url completion.
2023-10-20 15:01:55 +02:00
f63851d95d #68 JsonConverter 2023-10-20 14:50:26 +02:00
e72301d062 #68 and other chromium connectors: Wait for page to be fully loaded 2023-10-20 14:49:48 +02:00
2302e1009b Merge branch 'issue_70' into cuttingedge 2023-10-20 14:40:37 +02:00
40fea6cc7f Fix #70 invalid chapter numbers 2023-10-20 14:40:24 +02:00
5458c43f21 Merge branch 'timeout-bug' into cuttingedge 2023-10-19 13:00:03 +02:00
f78bec43d6 Fix an issue where a request-timeout would cause a restartloop. 2023-10-19 12:59:20 +02:00
88876fb8f4 #68 corrected url in GetChapters 2023-10-19 12:09:43 +02:00
c71aec8882 #68 Readme and Name 2023-10-19 12:08:49 +02:00
ddfba0d864 #68 MangaLife untested code, XML on site is broken 2023-10-19 12:06:03 +02:00
ca9c0b22c1 Merge pull request '#67 prevent crash if xml document does not exist' (!60) from cuttingedge into master
Reviewed-on: #60
2023-10-15 12:21:31 +02:00
6844d0a242 #67 prevent crash if xml document does not exist 2023-10-15 12:19:44 +02:00
fd9319de27 Merge pull request 'Fix #66 Mangasee search and parsing' (!59) from cuttingedge into master
Reviewed-on: #59
2023-10-14 13:07:46 +02:00
726be70af3 #66 Mangasee search sanitization 2023-10-14 12:59:35 +02:00
19c9ecb3e7 #66 Mangasee empty search breaks 2023-10-14 12:59:06 +02:00
f01a786e59 Merge pull request 'cuttingedge' (!58) from cuttingedge into master
Reviewed-on: #58
2023-10-12 20:48:15 +02:00
59f9bcc7d0 Merge remote-tracking branch 'origin/cuttingedge' into cuttingedge 2023-10-12 20:47:25 +02:00
2796a2adb5 Merge branch 'master' into cuttingedge 2023-10-12 20:47:16 +02:00
e07b191293 Merge branch 'master' into cuttingedge 2023-10-12 20:46:43 +02:00
9bf650f5fc New Issue Template: New Connector 2023-10-12 20:45:56 +02:00
334795b263 Update readme to reflect new connectors 2023-10-10 22:58:05 +02:00
51a6f216af Remove extraneous covers from imageCache. 2023-10-10 22:51:24 +02:00
238a2775f4 Author formatting bato 2023-10-10 22:45:11 +02:00
fec970d7d6 #64 fix empty search 2023-10-10 22:43:34 +02:00
e642d50c47 #64 Bato
Comment: This website suuuucks to scrape. There is gonna be so many issues
2023-10-10 22:40:44 +02:00
fafcdac00a Fix file-extension on image download 2023-10-10 22:40:07 +02:00
1785aa28ea Change coverCacheFilenames, to avoid conflicts and malformatted filenames 2023-10-10 22:34:47 +02:00
f22c332cab Merge pull request 'cuttingedge' (!57) from cuttingedge into master
Reviewed-on: #57
2023-10-10 21:21:34 +02:00
b3bf523e1e Fix #63 Chapter numbering. 2023-10-09 15:28:37 +02:00
06b2e11164 Add Mangaworld to dict. 2023-10-09 15:15:42 +02:00
7972f07801 housekeeping 2023-10-04 22:09:33 +02:00
d89af7cc5b Fix multiple enumeration 2023-10-04 22:09:27 +02:00
31a0c6ffb2 Fix build warnings 2023-10-04 18:14:46 +02:00
668a3b3a96 MangaDex nullchecking in response 2023-10-04 18:14:12 +02:00
3938c61297 #62 https://github.com/C9Glax/tranga/issues/62#issuecomment-1747064431
Parsing, parsing, parsing
2023-10-04 17:45:13 +02:00
4f3bcd245d #62 fix one bug, create another 2023-10-04 15:44:06 +02:00
129c95f123 Set timeout on chromiumclient
#62
2023-10-04 11:20:14 +02:00
e2cdf27d40 https://github.com/C9Glax/tranga/issues/62#issuecomment-1746422154
#62
ChapterNumber Parsing on Manganato
2023-10-04 11:15:24 +02:00
4156365b18 Improved logic on QueueContainsJob and AddJobTo Queue
Added some documentation
2023-10-04 09:38:40 +02:00
d3ccddd8db Fix multiple enumeration 2023-10-04 09:33:11 +02:00
13075a8704 Improved logic in LoadJobsList 2023-10-04 09:31:03 +02:00
e7d9f53a93 Prevent override of List-jobs in AddJobsQueue-method 2023-10-04 09:30:42 +02:00
dc6dfd4aa1 Renamed method ExportJob(s) to UpdateJobFiles 2023-10-04 09:30:08 +02:00
0fba09b1e8 Logic removed unecessary call 2023-10-04 09:24:21 +02:00
f08b9e85ec Add log message for inactive jobs 2023-10-03 20:46:59 +02:00
95fcc73c74 Cancel Running Jobs if inactive for more than 5 minutes 2023-10-03 20:46:21 +02:00
73492d8102 #62 even more debug logging 2023-10-03 20:38:45 +02:00
c69dd22ecf #62 more debug-logging
Instead of assigning buffer copy directy from result to filestream
2023-10-03 14:07:58 +02:00
17b6c523a2 Print results before downloading covers 2023-09-28 15:53:57 +02:00
6c3f7604fe Better Mangasee search 2023-09-28 15:53:40 +02:00
94f88f08e9 Update bug_report.yml
WHAT
2023-09-26 18:50:56 +02:00
47327524be body can not be empty? 2023-09-26 18:47:02 +02:00
3b96419739 will this work 2023-09-26 18:39:12 +02:00
b7c9b4e9b4 Update issue templates 2023-09-26 18:37:59 +02:00
13adb45444 File.extensions.matter 2023-09-26 18:32:44 +02:00
b8fbee578e Update readme 2023-09-26 18:30:52 +02:00
c1fb42b537 Update docker compose to latest 2023-09-26 18:29:49 +02:00
dcc12ec3ea Merge remote-tracking branch 'github/master' 2023-09-26 18:28:23 +02:00
8c554076b2 Merge branch 'cuttingedge' 2023-09-26 18:28:15 +02:00
a10fbdf3a5 Merge pull request #59 from C9Glax/C9Glax-patch-1
Update issue templates
2023-09-26 18:27:38 +02:00
f246209685 Changed to template 2023-09-26 18:26:42 +02:00
41c561bd1d Update issue templates 2023-09-26 18:18:06 +02:00
fc7d5463c3 Fix #58
Mangaworld: Manga without volumes crash
2023-09-26 18:03:18 +02:00
3c2ce266f6 Changed (fixed?) queuelogic 2023-09-20 21:59:39 +02:00
306cb87d67 Fix Check for subjobs 2023-09-20 21:34:04 +02:00
23cda74487 Fix wrong domain regex 2023-09-20 21:33:53 +02:00
3ceee63dfc Only send notification on successful downloads 2023-09-20 14:40:03 +02:00
4e5a6fe97b Export Library and notification connectors on deletion
Added logging
2023-09-20 14:11:31 +02:00
b3b1971dad Startup notification 2023-09-20 13:58:10 +02:00
2699f35b62 housekeeping 2023-09-20 13:33:13 +02:00
7a14583d6a Moved Regex for baseUrl to Globalbase 2023-09-20 13:30:52 +02:00
660f6a1648 Logmessages for creation of library and notification Connector 2023-09-20 13:28:09 +02:00
482fcb7102 better logging for removing files 2023-09-19 23:24:39 +02:00
b6cdb07e3f Remove filewrites 2023-09-19 23:15:18 +02:00
0875e7ee12 Remove log clutter and filewrites 2023-09-19 23:07:26 +02:00
cb6482ebae Add logmessage on startup for next job 2023-09-19 20:04:25 +02:00
87ea077281 Remove log clutter and filewrites 2023-09-19 20:02:56 +02:00
c1aa4cf6b5 Fi bug with exportjobslist not exporting updated jobs 2023-09-19 19:59:51 +02:00
f5b6b1785f small improvements 2023-09-19 19:57:35 +02:00
2553a150d1 Add log to see wait time 2023-09-19 19:54:26 +02:00
b149d377dc Add log to see wait time 2023-09-19 19:54:00 +02:00
0209159c5c Add log to see wait time 2023-09-19 19:50:39 +02:00
e31820eb00 Export Jobs list when finished. 2023-09-19 19:49:42 +02:00
c4d69c27a4 copy cover 2023-09-19 19:43:58 +02:00
3ee53b7436 copy cover 2023-09-19 19:43:39 +02:00
64ec0963e1 copy cover 2023-09-19 19:42:50 +02:00
27c4ed719c Cancel failed jobs 2023-09-19 19:33:43 +02:00
4f4b0cb3a8 LibraryConnector baseUrl regex 2023-09-19 19:22:49 +02:00
48d312da0b File Permissions 2023-09-19 19:21:37 +02:00
1fe4b75ac7 Folder permissions 2023-09-19 19:04:55 +02:00
c580fafc62 Added user tranga to container and set permissions 2023-09-19 19:00:00 +02:00
58040ecb10 Order of returned API Jobs/MonitorJobs And Jobs/Waiting 2023-09-19 18:06:08 +02:00
2960a9b8f0 Merge branch 'cuttingedge'
# Conflicts:
#	Tranga/Connectors/Mangasee.cs
2023-09-19 16:59:58 +02:00
f52bb8eb89 Get Readme ready for migration to master 2023-09-19 16:54:17 +02:00
ae0dc548ae Changed working directory on linux to /usr/share/tranga-api
Updated docker-compose to include settings-volume
2023-09-19 16:47:49 +02:00
051b85d08b Added contentType to response for images and logs 2023-09-19 16:43:08 +02:00
d89ca0a2ef Changed Jobs ToString 2023-09-19 16:30:55 +02:00
f1f640c1f6 Mangaworld fix volume and chapter numbers 2023-09-19 16:30:44 +02:00
9319aa7d1f Fix Mangaworld empty search-result crash 2023-09-19 16:24:07 +02:00
656e62628e Fix Mangaworld search 2023-09-19 16:23:52 +02:00
ba27adf255 Show startmessage and log settings 2023-09-19 16:08:00 +02:00
88ca75e883 Use lock statement instead of variable to lock logmessages 2023-09-19 15:59:52 +02:00
67c23b357f Add console-output to Dockerfile 2023-09-14 14:55:45 +02:00
4a5271e2a7 Added italian tags to series.json 2023-09-13 23:33:12 +02:00
fec5ad664c Fix possible nullreference 2023-09-13 23:02:36 +02:00
3cea5fb431 #50 Added Mangaworld.bz connector 2023-09-13 23:00:52 +02:00
7fa44fba54 Fix filename for coverimage if url contains parameters 2023-09-13 23:00:27 +02:00
d6b5a29fdc Fix Manganato kaguya-bug: volumenumber, chapternumber, chaptername match 2023-09-13 21:47:50 +02:00
a4a49d40f0 API GET LogMessages new optional parameter count 2023-09-13 14:40:23 +02:00
28fa85f05c #50 Added parameter translatedLanguage POST Jobs/DownloadNewChapters
POST Jobs/MonitorManga
2023-09-13 14:20:10 +02:00
1066e1ca2e #50 translated-language support (if connector supports it)
API GET Manga/Chapters new parameter "translatedLanguage"
2023-09-13 14:09:47 +02:00
39307f4313 Changed jobs.json to instead be a directory with one file per job
#48
2023-09-09 19:15:20 +02:00
a316ee3d48 Changed id creation for Jobs to be more descriptive 2023-09-09 19:14:47 +02:00
569622099d DownloadClient and MangaConnector improvements
DownloadClient is now abstract for HttpDownloadClient and ChromiumDownloadClient
The chromium client will exit the headless browser (on clean exit of the program).
The field "name" of MangaConnector is no longer abstract, instead set through constructor.
2023-09-08 23:27:09 +02:00
017701867d Fixed logic on API GET Jobs/Progress 2023-09-08 19:58:44 +02:00
c3d62bd337 Added ProgressToken timeRemaining 2023-09-08 19:58:29 +02:00
dc9e9e705c Fix FileLogger filePath 2023-09-08 19:28:44 +02:00
9eee6683fa Add API GET Ping 2023-09-08 16:31:38 +02:00
1265c7a072 Added API: GET Manga 2023-09-05 20:26:31 +02:00
c601541249 Added API: GET LogMessages and LogFile
resolves #10
2023-09-05 20:02:24 +02:00
ae1184320f Added API: customFolderName to Jobs/MonitorManga and Jobs/DownloadNewChapters
resolves #30
2023-09-05 19:51:18 +02:00
384e4c4f43 Added parameter "ignoreBelowChapterNum" tp API: Jobs/MonitorManga and Jobs/DownloadNewChapters 2023-09-05 19:44:14 +02:00
76a2b2498a Added numberFormatDecimalPoint to GlobalBase 2023-09-05 19:42:46 +02:00
2ab21b15cf Merge pull request #47 from C9Glax/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2023-09-05 19:33:20 +02:00
dependabot[bot]
7acdf7a19b Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-05 17:31:28 +00:00
af8716fcb1 Possible fix for #20 2023-09-05 19:28:43 +02:00
5f2c66b729 Merge remote-tracking branch 'origin/cuttingedge' into cuttingedge 2023-09-03 18:37:03 +02:00
e030f02431 Merge remote-tracking branch 'origin/cuttingedge' into cuttingedge
# Conflicts:
#	README.md
2023-09-03 18:36:57 +02:00
bdeb75f4e4 Merge remote-tracking branch 'origin/cuttingedge' into cuttingedge
# Conflicts:
#	README.md
2023-09-03 18:36:05 +02:00
4ce114986d Updated Readme to reflect the seperation of tranga (the api) and the website. 2023-09-03 18:35:51 +02:00
8035bf3fcd Updated Readme to reflect the seperation of tranga (the api) and the website. 2023-09-03 18:34:17 +02:00
85bf3ec7e8 Fix MangaSee publicationId 2023-09-02 23:14:31 +02:00
0f17615b10 Fix FileInUse 2023-09-02 23:14:16 +02:00
0c8145803e Possibly related to #20 2023-09-02 22:49:00 +02:00
b2e0c3db97 docker-compose to cuttingedge 2023-09-02 22:43:09 +02:00
ca283fcfff Fix Dockerfile, copy CLI 2023-09-02 22:39:54 +02:00
1d55070daf Merge branch '41_-_trash_everything' into cuttingedge
# Conflicts:
#	Tranga/MangaConnectors/DownloadClient.cs
2023-09-02 22:33:29 +02:00
32fd75bdae Add Manga to cached on parsing 2023-09-02 22:12:49 +02:00
99ad702163 Fixed MangaDex GetMangaFromUrl Regex-Group and resultobject 2023-09-02 22:12:34 +02:00
6e3a9c2a78 Added Lock to MemoryLogger 2023-09-02 21:53:09 +02:00
ad1d4dfe23 Fixed naming errors containing Manga
Added GetMangaFromUrl(url) to Mangaconnector
2023-09-02 21:52:48 +02:00
14ba71005f CheckJobs combined cancelled and completed checks,
added standby check
2023-09-02 16:16:00 +02:00
22c4c0eb2c Fixed GetJobsLike, for empty publication, but existing chapter 2023-09-02 16:15:06 +02:00
44f8d369c3 Added AddJobs to JobBoss 2023-09-02 16:14:36 +02:00
c0e6da144e Changed Job.ExecuteNow to ExecutionEnqueue
Instead of replacing progressToken, change Increments based in completed increments
2023-09-02 16:14:21 +02:00
51a1ae72ca Added parentJobId for deserialization
When creating Jobs with null as recurrence time, set it to zero
Job.NextExecution() removed the recurrence check
2023-09-02 16:12:10 +02:00
79bbc92467 Added lastExecution time on jobs.json parse 2023-09-02 15:05:15 +02:00
ae5be31c89 Fixed Jobs/StartNow 2023-09-02 14:49:31 +02:00
eebe25a378 Added check if jobQueue is empty 2023-09-02 14:46:38 +02:00
0f3da4ec81 Added check to read/write jobs.json if file is in use
Write jobs.json on change
2023-09-02 14:46:13 +02:00
0b77dc1172 Added ProgressToken state Cancelled 2023-09-02 14:45:46 +02:00
37cf47bc17 Reduced CheckJobs timer to 100ms 2023-09-02 14:45:02 +02:00
4cce2e04cb Renamed Job.Reset to ResetProgress 2023-09-02 14:13:30 +02:00
5465ac4e5c Removed DELETE Jobs/DownloadChapter and Jobs/MonitorManga. Can both be reached with DELETE Jobs (jobId)
Added POST Jobs/Cancel
CancelJob and RemoveJob cancels/removes subJobs
2023-09-02 14:13:15 +02:00
dd4d5a81ee Fix JobId variable in API requests 2023-09-02 14:11:44 +02:00
a05e1914e3 Log output changes 2023-09-02 14:11:11 +02:00
ed79ee5d0f Add Manga from Jobs to cachedManga 2023-09-01 23:41:50 +02:00
28e05e549d Added import and export for Jobs
Renamed tasksFilePath -> jobsFilePath and changed to jobs.json
2023-09-01 23:37:50 +02:00
eaab7c5235 Fixed jobs not starting at all 2023-09-01 23:08:31 +02:00
0552b3db82 Fix crash on null Logmessage 2023-09-01 22:53:38 +02:00
c813e1854d Do not add duplicate jobs 2023-09-01 22:39:22 +02:00
32036df057 Added API call to retrieve cover with internalId.
No need to mount imageCache over multiple containers.
2023-09-01 21:40:56 +02:00
394829ee36 Revert "Download Covers only when Downloading Chapters"
This reverts commit e663163d

Covers might be important
2023-09-01 21:17:46 +02:00
2a389f1ede Changed default download and working directories.
ExportSettings() now created folder
2023-08-31 17:07:54 +02:00
3167f6c3e6 Changed default log-folder path, and log-encoding to utf8 2023-08-31 17:07:17 +02:00
89c5f4b820 Added API-call GET Jobs/MonitorJobs 2023-08-31 16:40:08 +02:00
1c1169e5ce Renamed Managers to Connectors 2023-08-31 16:39:39 +02:00
d5d34c5381 Changed return-values of API: NotifcationConnectors/Types and LibraryConnectors/Types 2023-08-31 15:52:47 +02:00
c0efbb22cc Fixed JsonParsing of NotifcationConnector and LibraryConnector with GlobalBase 2023-08-31 15:41:02 +02:00
9f30e52713 Added new API-Calls:
POST: Jobs/StartNow
DELETE: Jobs
2023-08-31 13:12:03 +02:00
1fd36c91d6 Renamed Publication.cs to Manga.cs
Renamed Request-Paths "Tasks" to "Jobs"
2023-08-31 12:16:02 +02:00
e663163de8 Download Covers only when Downloading Chapters 2023-08-31 12:14:03 +02:00
4827b90c3d Merge pull request #45 from C9Glax/dependabot/github_actions/docker/setup-buildx-action-2.10.0
Bump docker/setup-buildx-action from 2.9.1 to 2.10.0
2023-08-29 19:09:40 +02:00
e274c864f9 CLI: Add Status Code to output 2023-08-29 14:11:46 +02:00
f4bc182954 CLI: Prompt directy for HttpMethod, ignore input when exiting log 2023-08-29 14:09:35 +02:00
3365be219c Logger: Logmessage time 2023-08-29 14:08:57 +02:00
10708b3abd Add CLI with basic functionality. 2023-08-29 14:00:55 +02:00
c1e939f1e3 Server correct shutdown/force shutdown 2023-08-29 12:40:10 +02:00
21d53dabec TrangaSettings corrected logic for loading settingsfile, and overwriting settings 2023-08-29 12:39:48 +02:00
a9417dbba6 Trangasettings fix infinite loop on load 2023-08-29 12:39:21 +02:00
dependabot[bot]
4ca7b107eb Bump docker/setup-buildx-action from 2.9.1 to 2.10.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.9.1 to 2.10.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.9.1...v2.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-29 05:58:02 +00:00
61024bcee9 UserDictionary and variable readonly 2023-08-27 01:22:39 +02:00
ea1b8749a6 Removed unnecessary check 2023-08-27 01:22:21 +02:00
2fcab1f1b1 More Logging 2023-08-27 01:22:08 +02:00
bbd716383a Added ToString overrides 2023-08-27 01:21:23 +02:00
6e1a0ab06c Corrected order of constructor (GlobalBase clone) 2023-08-27 01:15:02 +02:00
181942153b Fixed some variables 2023-08-27 01:05:32 +02:00
fe04af4a2b Added most API-functions 2023-08-27 01:01:39 +02:00
4240a1eb6a Added methods to search for jobs, and remove multiple jobs. 2023-08-27 01:01:20 +02:00
32349c1ddf Added methods to Update Downloadlocation and WorkingDirectory 2023-08-27 01:00:42 +02:00
a94d3d6b40 Added method to delete Library/Notification-Connector 2023-08-27 01:00:13 +02:00
f916cda0f1 Corrected order of constructor (GlobalBase clone) 2023-08-27 00:59:54 +02:00
a8f0f1af15 More API Requests 2023-08-26 02:43:24 +02:00
0cf3a95f58 cachedPublications 2023-08-26 02:42:57 +02:00
a89a526fda Default language GetChapters: en 2023-08-26 02:42:31 +02:00
4d1e43e7b3 Job: add Id 2023-08-26 02:40:24 +02:00
4f9749d09e Fix bug with MangaDex, Useragent 2023-08-26 01:51:16 +02:00
7614f9aad3 Add User Agent to MangaConnectors 2023-08-26 01:50:31 +02:00
97c0e42512 Handle first requests, add parameter parser 2023-08-26 01:47:36 +02:00
565bc0775d Add Connectors to Tranga 2023-08-26 01:47:15 +02:00
e6a3fa2899 public GetPublications 2023-08-26 01:46:36 +02:00
2d82279d98 Added startup args, and first http-requesthandler 2023-08-24 13:35:07 +02:00
c5559a4ceb Save api-Portnumber in settings 2023-08-24 13:34:43 +02:00
2572a537ab Job Inherits from GlobalBase 2023-08-24 13:34:23 +02:00
58db049496 Merged MonitorJobs and CheckJobs in JobBoss 2023-08-24 13:34:09 +02:00
8f309fcfd7 Library- and NotificationConnectors in GlobalBase 2023-08-24 13:33:33 +02:00
11461051f3 Fixed missing filelogger crash 2023-08-24 12:13:34 +02:00
a4aa571870 Added Jobs and ProgressToken 2023-08-04 14:51:40 +02:00
e4086a8892 Rename TBaseObject -> GlobalBase
Remove Notification and Library Connectors from GlobalBase
2023-08-01 18:24:19 +02:00
c45e4ddf90 Rename Connectors -> MangaConnectors 2023-08-01 18:22:24 +02:00
675effd317 Trash everything and writing everything from scratch 2023-08-01 18:21:29 +02:00
a4f67c9ab4 Merge pull request 'Fixes for MangaKatana' (!53) from cuttingedge into master
Reviewed-on: #53
2023-07-31 23:09:24 +02:00
2538a29788 MangaKatana fix search result characters 2023-07-31 23:05:29 +02:00
81d5802092 MangaKatana fix bug where empty result in search would crash program 2023-07-31 23:03:46 +02:00
436edfde66 Fix issue where closed connection crashes api 2023-07-31 22:58:41 +02:00
00c1cd56b8 Merge pull request '#31 #40' (!52) from cuttingedge into master
Reviewed-on: #52
2023-07-31 22:50:22 +02:00
a63154b581 Fix new installation startup issue where version would be null on new installs 2023-07-31 22:47:35 +02:00
53fe7ee983 Possible fix for #31
chapter regex
2023-07-31 22:47:14 +02:00
6fb4098c16 Merge pull request 'Missing logger, breaking version in settings.json' (!51) from cuttingedge into master
Reviewed-on: #51
2023-07-31 02:14:06 +02:00
7a024e8733 Add logger to CommonObjects on deserialiazation 2023-07-31 02:11:53 +02:00
835e239be5 Cleanup 2023-07-31 02:07:39 +02:00
df8538c3b4 Merge pull request 'version' (!50) from cuttingedge into master
Reviewed-on: #50
2023-07-31 01:59:42 +02:00
f832fe0de3 version 2023-07-31 01:58:00 +02:00
ebdb38bd57 Merge pull request 'Moving away from API/CLI model, combined into single executable.' (!49) from cuttingedge into master
Reviewed-on: #49
2023-07-31 01:53:50 +02:00
e3201a9b99 Ignore Logger 2023-07-31 01:50:26 +02:00
eb50b84266 Converters 2023-07-31 01:48:40 +02:00
b3d778ff56 accessibility 2023-07-31 01:45:55 +02:00
00861c406a added logging 2023-07-31 01:42:15 +02:00
01c8784bab wrong array 2023-07-31 01:30:32 +02:00
3aa299e48a deserialization of enum 2023-07-31 01:28:32 +02:00
d1ce244135 New Migration to new commonObjects 2023-07-31 01:26:38 +02:00
c91754614b weird env 2023-07-31 00:58:22 +02:00
70b1ae4812 isLinux 2023-07-31 00:52:27 +02:00
336e08aebf If not running cli add back console output 2023-07-31 00:46:14 +02:00
18134cdf01 If not running cli add back console output 2023-07-31 00:43:57 +02:00
5b89cbd042 Only run TaskMode on Windows 2023-07-31 00:41:25 +02:00
74aca86b62 Wrong entrypoint 2023-07-31 00:36:56 +02:00
e5abaa4549 Wrong entrypoint 2023-07-31 00:35:11 +02:00
eb0eb71e86 wrong dockerfile 2023-07-31 00:33:57 +02:00
4e73b0a4cf wrong dockerfile 2023-07-31 00:32:42 +02:00
140074208f Merged API and CLI into one. 2023-07-31 00:31:19 +02:00
fa19d3da14 Fix missing file on loading settings/commonobjects 2023-07-31 00:01:18 +02:00
3d6657b483 Moved libraryManagers, notificationManagers and logger to commonObjects class. 2023-07-30 23:31:25 +02:00
f9b5e05974 Merge pull request #39 from C9Glax/cuttingedge
Move Namespaces, move logger to TrangaSettings, move downloadClient to seperate File, remove deprecated calls
2023-07-30 17:34:06 +02:00
ad4027779f Remove Deprecated CreateUpdateLibraryTask 2023-07-30 17:29:30 +02:00
98ec0b837f Remove Enter input from settings, instead update all settings on click of "Update" Button.
resolves #38
2023-07-30 17:27:47 +02:00
1afa3df316 Cleanup build warnings, ReShaper, Dictionary 2023-07-30 17:25:04 +02:00
d83aa1ef5b deprecated 2023-07-30 17:11:11 +02:00
b610ec734e Chapter readonly struct 2023-07-30 17:09:39 +02:00
abf587377c API: Changed uninstantiated class Program to static 2023-07-30 17:09:30 +02:00
437349bd27 TrangaSettings changed set directive 2023-07-30 17:09:10 +02:00
000539d6a6 Moved logger to Trangasettings 2023-07-30 17:08:43 +02:00
b4bef25a22 Moved downloadclient to separate file 2023-07-30 17:04:43 +02:00
579e400a5d Moved class to appropriate namespaces 2023-07-30 17:01:54 +02:00
8af2b12fc0 Moved class to appropriate namespaces 2023-07-30 16:26:29 +02:00
bad4330330 introduce branch cuttingedge 2023-07-30 16:21:04 +02:00
42596752d3 FIX: null Publications in tasks 2023-07-29 18:55:06 +02:00
16238c590b Remove UpdateLibrariesTask 2023-07-29 18:20:41 +02:00
9f38dc3b6a Revert "Remove UpdateLibrariesTask"
This reverts commit de14ff0b75.
2023-07-29 18:18:02 +02:00
485637d99a Added Min-Chapter-Number to API 2023-07-28 10:47:36 +02:00
de14ff0b75 Remove UpdateLibrariesTask 2023-07-28 10:41:20 +02:00
f947c37bd6 Change website context to revert location to / instead of /Website 2023-07-28 10:30:54 +02:00
77eec0f696 Fix wrong deserialization 2023-07-21 00:32:18 +02:00
18323f9f51 remove debug 2023-07-21 00:22:41 +02:00
2cd2b6842d arch armv7 fails to build 2023-07-21 00:20:13 +02:00
09f815903f arch arm64 fails to build 2023-07-21 00:18:08 +02:00
c108478039 context 2 2023-07-21 00:15:35 +02:00
74289e43b7 context 2023-07-21 00:14:07 +02:00
2779f9ba09 Merge remote-tracking branch 'origin/master' 2023-07-21 00:12:23 +02:00
59a8e556f0 wrong build path 2023-07-21 00:12:09 +02:00
074b137b5c Merge pull request 'dev' (!48) from dev into master
Reviewed-on: #48
2023-07-21 00:10:33 +02:00
3cb2540794 debugging 2023-07-21 00:09:59 +02:00
02c9934896 change context back to API 2023-07-21 00:09:51 +02:00
b2e1c95bca Merge remote-tracking branch 'origin/master' 2023-07-21 00:07:44 +02:00
8c9e3ea6b6 Merge pull request 'split into two actions, dont always build tranga-base' (!47) from dev into master
Reviewed-on: #47
2023-07-21 00:07:16 +02:00
db441607ad Merge branch 'master' into dev 2023-07-21 00:04:24 +02:00
91c56783dc restore absolute path 2023-07-21 00:03:32 +02:00
2c288eeeea Don't rebuild tranga-base every time. 2023-07-20 23:54:30 +02:00
57a1ea91fc Merge pull request 'dev' (!46) from dev into master
Reviewed-on: #46
2023-07-20 23:50:15 +02:00
06138a3927 Workflow change context 2023-07-20 23:49:33 +02:00
84b053e672 Merge remote-tracking branch 'origin/dev' 2023-07-20 23:44:17 +02:00
0fe0cbc4ad Merge pull request #34 from C9Glax/dev
Unsupported arch
2023-07-20 23:42:04 +02:00
62e6ce8363 remove unsupported platforms 2023-07-20 23:38:10 +02:00
a4f3ec6580 Merge pull request #33 from schklom/master
Automatic build of Docker images for many platforms (ARM too)
2023-07-20 23:25:10 +02:00
schklom
8b4e996b7e Create dependabot.yml 2023-07-20 23:10:46 +02:00
schklom
964540d30f Create docker-image.yml 2023-07-20 23:10:15 +02:00
fa69f4488f Removed UpdateLibraryTask (deprecated).
Libraries will be updated on new Chapters downloaded.
Added Migrator, for future file-changes
2023-07-20 18:15:14 +02:00
42c2876188 Mangakatana chapter num fix 2023-07-16 20:22:33 +02:00
715244ff1b Mangasee more logging 2023-07-16 18:15:28 +02:00
2333cd9095 Mangasee more bad words 2023-07-16 18:15:11 +02:00
c8225db4fe #30 #31 2023-07-16 17:47:00 +02:00
6741ca096b Startup Message 2023-07-16 17:38:42 +02:00
a897a7b3a2 Better Logger.
Includes a formatted Console-Log
2023-07-16 17:33:15 +02:00
0f8932e712 Fixed missing logger for notificationManagers on deserialization 2023-07-09 21:38:49 +02:00
78023ef0fd resolves #21 lunasea 2023-07-09 21:35:15 +02:00
Andy Maesen
d171f34e4e Update README.md 2023-07-07 14:23:33 +02:00
Andy Maesen
aa0dc4fa35 Fixes single result redirect 2023-07-06 02:09:56 +02:00
25f48592c0 Added more badwords to filter out when searching mangasee, resolves #26 2023-07-04 22:44:01 +02:00
398ac304d2 Update Komga/Kavita immediately after new chapter is Downloaded 2023-07-03 00:01:08 +02:00
58a62f8272 Mangasee search all title-fields. 2023-07-02 23:54:02 +02:00
86752c9a7e Order of task Execution by due-time 2023-07-02 23:10:16 +02:00
f9a7828d02 Moved notification back to DownloadChapterTask
temp: Dont wait for childTasks to finish to finish parent task
2023-07-02 23:06:24 +02:00
c97ff69148 Fix for new publications: Add to collection 2023-07-02 22:46:01 +02:00
1735bbcf8a Fix wrong query from allTasks to runningTasks 2023-06-30 00:23:00 +02:00
9ae8ca65df resolves #25 characters encoding mistake 2023-06-29 21:09:42 +02:00
00599cd24e Infinite loop on unavailable chapters 2023-06-28 23:00:24 +02:00
6d5618a1f7 Infinite loop on unavailable chapters 2023-06-28 22:46:22 +02:00
a1202a875d Moved sucessstate to taskmanager 2023-06-28 22:43:46 +02:00
98946b4aa3 Fixed null chapterNumber on mangadex 2023-06-28 22:43:24 +02:00
41b6bb77b6 Moved GetPublicationsFromConnector to connector.
Moved GetNewChaptersList to Connector.
Removed knownPublications file
Renamed chapterCollection to collection and only contains Publications
2023-06-28 22:43:03 +02:00
e70a14ca56 Only send notifications if more than 0 new chapters 2023-06-28 19:23:06 +02:00
b099da1156 Chapter fix RegexMatching on chapter number 2023-06-28 00:13:23 +02:00
01d1f922c2 MangaDex chapterNumber non.nullable 2023-06-28 00:13:09 +02:00
47a80d67a8 TrangaTask Success-State and child task deletion 2023-06-27 23:55:13 +02:00
16e3549455 Export Data after deleting task 2023-06-27 23:54:44 +02:00
be8c6b50ba Notification moved to TrangaTask 2023-06-27 23:37:13 +02:00
a38fcf50ca nullable types removed 2023-06-27 23:25:35 +02:00
82f6c7b3fe Moved GetArchiveFilePath, CheckChapterIsDownloaded and GetComicInfoXmlString to Chapter.cs 2023-06-27 23:22:23 +02:00
5586d2c104 Connector CheckChapterIsDownloaded more Regex 2023-06-27 23:14:22 +02:00
62dc9fee2a GetComicInfoXmlString: protected -> internal 2023-06-27 23:09:09 +02:00
ac96fca6dc Chapter illegalstring regex 2023-06-27 23:08:29 +02:00
25a6ceff10 Remove sortNumber-field from Chapter
API: Change Tasks/Progress chapterSortNumber to ChapterNumber
2023-06-27 23:06:37 +02:00
b3e1d39d0f Rename Connector.SearchChapters -> SelectChapters
Added "a(ll)"-option to SelectChapters
2023-06-27 23:02:55 +02:00
2833b7f22a Remove Legacy support for "DownloadNewChapters" 2023-06-27 22:59:33 +02:00
cbdd305b69 TaskManager AddTask make better use of GetTasksMatching and GetTasksMatching easier usage 2023-06-27 22:59:23 +02:00
b88890817e TaskManager _runningDownloadChapterTasks -> _runningTasks for all TrangaTasks 2023-06-27 22:58:40 +02:00
f66ab7d40b Connector use TrangaSettings instead of own values for imageCache and downloadLocation 2023-06-27 22:57:44 +02:00
4cb3694cd5 Re-add task timeout 2023-06-27 22:23:53 +02:00
a05d4c8bd9 Merge remote-tracking branch 'origin/master' 2023-06-27 22:23:23 +02:00
22f87a74b2 Re-add task timeout 2023-06-27 22:23:19 +02:00
ba57282879 Re-add task timeout 2023-06-27 22:19:06 +02:00
9ccba6fba6 Fix CheckChapterIsDownloaded Directory does not exists exception returning 0 chapters 2023-06-25 23:56:22 +02:00
4f01c1166f Fix taskIds being changed during requests, no workaround this time 2023-06-25 23:56:00 +02:00
0a51e7ad3d Fix taskIds being changed during requests 2023-06-25 23:26:36 +02:00
e541b922dc Merge pull request #24 from arxae/master
Added MangaKatana connector
2023-06-25 21:38:18 +02:00
604abd5f9a Fix bug where ChildTasks hung parentTasks 2023-06-24 21:00:26 +02:00
7b311eae75 Will break: CheckChapterIsDownloaded 2023-06-24 20:46:35 +02:00
Andy Maesen
d4eb72cd99 Required changes 2023-06-23 22:14:27 +02:00
b515215f4b Fix taskIds being changed during requests 2023-06-22 23:09:59 +02:00
a16686dfbf Fix wrong taskNames 2023-06-22 22:52:26 +02:00
Andy Maesen
4275703941 Added MangaKatana connector 2023-06-22 14:22:21 +02:00
c3342984ea Server fixed bug where ?& in request url caused variables to not parse 2023-06-21 18:04:41 +02:00
ed4bdb5b33 TrangaSettings export after change 2023-06-21 18:04:12 +02:00
0f0902c932 LunaSea changed to id device/id or user/id instead of full url 2023-06-21 18:03:48 +02:00
6508055b43 API Fix closed response socket 2023-06-21 17:42:56 +02:00
abc66511d8 Fixed progress tracking this time for realsies. resolves #5 2023-06-21 17:30:31 +02:00
9ed36c47b5 Fixed taskId on init deserialization 2023-06-21 17:29:48 +02:00
fd1b2a8470 API Fix closed response socket 2023-06-21 17:29:20 +02:00
8058749ab5 Website fix wrong task on deletion 2023-06-21 16:53:56 +02:00
8737617e5f Fix deletion of successful child tasks 2023-06-21 16:53:41 +02:00
7e4f43f1e2 API fix CORS preflight 2023-06-21 16:53:07 +02:00
12b1b2afd6 Server fix interfaces on windows 2023-06-21 16:52:57 +02:00
0f9ac60fcd closes #11 readme update 2023-06-21 16:17:40 +02:00
8c87f2948c README updated screenshots 2023-06-21 16:08:36 +02:00
e0fb817256 Changed glax/tranga-base to latest 2023-06-20 23:26:49 +02:00
cdd2d94ba1 Wrote my own Http-Server.
ASP-NET can **** my **** and *** :)
2023-06-20 23:15:56 +02:00
d5b7645cd2 "Thread-safe" message adding.. 2023-06-20 23:15:22 +02:00
9af5c1603e Using HttpStatusCode to signify Task-Success
When DownloadChapterTask returns notfound, do not retry.
2023-06-20 15:46:54 +02:00
1035939309 Fix overflow 2023-06-20 15:18:58 +02:00
3b542c04f6 ReShaper cleanup,
Remove unnecessary using directives
2023-06-20 14:59:08 +02:00
a809b7c285 Added timeout to Connector DownloadClient 2023-06-20 14:58:02 +02:00
e883277400 Renamed DownloadNewChaptersTask to MonitorPublicationTask
Added TrangaTask.Clone() method
Rewrote TrangaTask.progress for the billionth+1 time.
Removed Increment and DecrementProgress methods
Removed TrangaTask.ReplaceFailedChildTask method
Changed return type of TrangaTask.ExecuteTask to bool, signifying success.
Added Failed Execution state to TrangaTask
Replaced taskManager failed-task logic
Removed TaskManager bulky AddTask and DeleteTask methods
Removed TaskManager bulky Constructor
2023-06-20 14:57:44 +02:00
23dfdc0933 Connector DownloadChapter, DownloadImage, DownloadChapterImages returns successState.
RequestResult replace HttpStatusCode with success-status boolean.
DownloadChapterTask: Only send Notification when Chapter download successful
2023-06-19 22:45:33 +02:00
edc24fff5b Moved notification to DownloadChapterTask, sends when parentTask exists. 2023-06-19 22:34:34 +02:00
6cdccdf66b Fix infinite loop of DownloadNewChaptersTask 2023-06-19 22:32:32 +02:00
a4c9168551 Selector for task-sanitizer 2023-06-19 17:17:47 +02:00
821a1b7c3a Unique IDs for TrangaTask now based on Random-generator 2023-06-19 17:17:24 +02:00
b2b4256972 Startup message api 2023-06-19 16:46:12 +02:00
d2f46e4637 #21 Deserialization of LunaSea Object 2023-06-19 11:27:07 +02:00
303fc293ba Fixed Bug on AddTask where no new UpdateLibraryTask would be added 2023-06-15 22:32:55 +02:00
36c145da26 Gotify change to normal priority 2023-06-15 21:24:01 +02:00
c822c74f42 website fix taskSelectOutput overflow issue 2023-06-15 21:16:56 +02:00
dda4054d34 API: Fix nullable bug on Getchapters 2023-06-15 21:15:44 +02:00
5b2546fdbc removed unnecessary log 2023-06-15 19:07:25 +02:00
c11e3993ea Added successmessage to NotificationManager 2023-06-15 19:06:53 +02:00
02a382a99a Website: Added connector NotificationManager LunaSea
Added Update Method for TrangaSettings for LunaSea
#21
2023-06-15 18:57:50 +02:00
c6c8f5cdf6 TrangaSettings nullable library and notificationManagers will initialize a new Hashset 2023-06-15 18:50:50 +02:00
84842aed3c Added connector NotificationManager LunaSea 2023-06-15 18:50:19 +02:00
d9ced11cd1 Website: Added gotify config 2023-06-15 18:38:47 +02:00
25c90782dc Moved UpdateSettings to TrangaSettings
Added NotificaitonManager
Added Gotify
Added Notification on MonitorTask download new chapters
2023-06-15 18:25:32 +02:00
e789c429cd TaskManager when deleting task also remove from parent. 2023-06-15 18:24:19 +02:00
93de471836 Added TrangaTask.RemoveChildTask 2023-06-15 18:22:59 +02:00
8b58e7dd13 Website: On Download Chapters only show chapters that have not yet been downloaded
API: Added new variables to /Publications/GetChapters: onlyNew and onlyExisting. API will return only new, only existing or all chapters depending on variables.
#19
2023-06-15 17:14:20 +02:00
b571bfa43d Moved GetNewChaptersList to taskManager and added GetExistingChaptersList 2023-06-15 17:07:32 +02:00
088d1c4647 Derived Constructor 2023-06-15 17:06:41 +02:00
f280c01802 Browser Version for both windows and linux 2023-06-15 16:30:07 +02:00
1be10b310d Fix Regex Bug on downlaod volumes 2023-06-11 19:17:03 +02:00
a0469f3145 Cancel DownloadChapter-Task on removal 2023-06-11 19:16:05 +02:00
fcd81f03b3 resolves #17 no cover image 2023-06-11 19:05:08 +02:00
76604d84d8 Better way of handling progress, and childProgress. 2023-06-11 18:24:26 +02:00
af822febbe fixed nullable warning 2023-06-11 18:01:04 +02:00
8e207c3119 Better way of handling progress, and childProgress. 2023-06-11 17:27:33 +02:00
b6f8c8aab5 TaskType check 2023-06-11 17:05:24 +02:00
36f7cbd3e9 Better way of handling progress, and childProgress.
More reliable taskFinishTime
2023-06-11 17:04:33 +02:00
3b2643d949 Website show remaining time instead of percentage 2023-06-11 16:38:12 +02:00
9fd8bf1741 website uses taskId 2023-06-10 16:00:41 +02:00
d5c9c5ba96 Redid progress calcuation on DownloadNewChaptersTask and DownloadChapterTask 2023-06-10 16:00:16 +02:00
c8e27921ab Added taskId to trangaTask and parentTaskId to DownloadChapterTask as unique identifier to attach ChildTasks to ParentTask on deserialization. 2023-06-10 15:59:42 +02:00
6eaba07801 Changed progress type from float to double 2023-06-10 15:58:11 +02:00
41929e0c72 DownloadChapterTask sets execution of parentTask 2023-06-10 15:04:37 +02:00
4fcaca1a6e Multiple authors resolves #7 2023-06-10 14:45:04 +02:00
0e3c7f32d7 Added CancellationToken to TrangaTask #14 2023-06-10 14:34:30 +02:00
1c94625840 Added CancellationToken to TrangaTask #14 2023-06-10 14:27:09 +02:00
32f89f9dce Multiple authors resolves #7 2023-06-10 14:05:23 +02:00
234735a562 Order of tasks closes #15
Also API /Queue/Get orders in order of nextExecution
2023-06-10 00:45:55 +02:00
8b916eb854 invalid Ids 2023-06-10 00:23:23 +02:00
29e1790c93 website tasks-width now max 95vw 2023-06-10 00:10:16 +02:00
ac4c799a74 Better indication if tasks have started. 2023-06-10 00:07:41 +02:00
7c62883c37 invalid id 2023-06-10 00:02:51 +02:00
02018253bf wrong nesting ... 2023-06-10 00:01:38 +02:00
2aec884009 Moved update interval for task-progress to own interval, progress gets continually updated. 2023-06-09 23:58:04 +02:00
b3321ff030 unnecessary log 2023-06-09 23:48:39 +02:00
16c1094875 Replaced Task-Progress-Tracking Window with more fancy one 2023-06-09 23:46:10 +02:00
5763d50409 #14 temporary workaround for disposing tasks 2023-06-09 23:45:53 +02:00
ad43297358 API: Updated /Tasks/GetProgress to return progress of specific task (by sortNumber) 2023-06-09 23:43:57 +02:00
b17800e0ef Decrement progress of parenttask when childtask fails 2023-06-09 23:43:19 +02:00
89c80d2997 Fixed bug where tasks would instantly failed when launched #14 2023-06-09 23:42:54 +02:00
6485b8744f API: Updated /Tasks/GetProgress to return progress of specific task (by sortNumber) 2023-06-09 23:42:18 +02:00
a3a96b6b55 Added DecrementProgress function to TrangaTask 2023-06-09 23:38:28 +02:00
5bce3c6fdd Website: Monitor task creation styling 2023-06-09 22:15:29 +02:00
5fa0c98d05 Documentation how to create tasks #11 2023-06-09 11:26:51 +02:00
b166013770 resolves #13 Website: Clear previous results 2023-06-09 11:12:43 +02:00
02fe849046 Better downloadChapter selection 2023-06-09 11:06:18 +02:00
d42393c83a Website + API ability to download specific volumes 2023-06-08 19:53:05 +02:00
c685bd622f Website:
New task-Creation dialog
Redesigned Settings dialog
2023-06-08 19:25:28 +02:00
dc83cc2194 Fixed Range on CLI downloadchaptertask creation 2023-06-08 19:25:03 +02:00
7784f2024e API changes:
/Tranga/GetAvailableControllers => /Controllers/Get
/Tranga/GetKnownPublications =>/Publications/GetKnown
/Tranga/GetPublicationsFromConnector => /Publications/GetFromConnector
/Tasks/GetTaskTypes => /Tasks/GetTypes
/Tasks/GetTaskProgress => /Tasks/GetProgress
/Tasks/Create is now split in 3:
    /Tasks/CreateMonitorTask
    /Tasks/CreateUpdateLibraryTask
    /Tasks/CreateDownloadChaptersTask
2023-06-08 19:24:46 +02:00
4895079887 Remove DownloadChapterTask from _runningDownloadChapterTasks after completion 2023-06-07 15:01:24 +02:00
ab1ddc6dc8 Less cluttered log 2023-06-07 00:31:27 +02:00
87eade10cf #40 task timeout criteria 2023-06-07 00:27:53 +02:00
1f3ac41b30 removed unnecessary cast 2023-06-07 00:24:58 +02:00
6a304bb330 #40 task timeout 2023-06-07 00:24:27 +02:00
b0642d1251 removed unnecessary check 2023-06-06 22:11:57 +02:00
63b5139e93 Split error message for better logging 2023-06-06 22:11:38 +02:00
e938784388 Created own base image for tranga-api (to stop apt always updating) 2023-06-06 22:11:26 +02:00
c436389426 renamed wrong variable names publicationId -> internalId 2023-06-06 21:57:10 +02:00
5099e25f3f Fixed wrong comparison on add new task 2023-06-06 21:56:51 +02:00
cf6fc3b8f6 Update readme HtmlAgilityPack 2023-06-06 21:27:12 +02:00
f5141d0f8e removed unnecessary async 2023-06-06 21:27:02 +02:00
5c753e7a7d Added fields to TrangaTask:
executionStarted,
executionApproximatelyFinished,
executionApproximatelyRemaining
to track progress
2023-06-06 21:19:30 +02:00
17ce820cf3 Changed taskProgress increment to call method that updates progress accordingly (with parent-tasks being also updated) 2023-06-06 20:54:21 +02:00
5b4a3b9d7c README update 2023-06-06 15:41:26 +02:00
f73997e563 Fix browser startup 2023-06-05 21:38:32 +02:00
437136804d Also delete downloadChapterTask when delete download new chapter tasks 2023-06-05 21:16:04 +02:00
e14683d21a Merge remote-tracking branch 'origin/master'
# Conflicts:
#	Tranga/TaskManager.cs
2023-06-05 21:11:10 +02:00
5ae02ee0ed Fix Bug where all tasks would be deleted... 2023-06-05 21:10:42 +02:00
a2e9a3f34a Fix Bug where all tasks would be deleted... 2023-06-05 20:52:21 +02:00
bbf05e3dec name and volume number 2023-06-05 20:37:44 +02:00
d95839e5df Chromium Version and Dependencies 2023-06-05 20:36:01 +02:00
5a303598fe Download progress updates every 5 seconds 2023-06-05 19:53:00 +02:00
db2103963e Merge remote-tracking branch 'origin/master' 2023-06-05 19:47:08 +02:00
2c1105527a Add Connector Mangasee #34 2023-06-05 19:47:04 +02:00
ed19dcb5c3 Add Connector Mangasee 2023-06-05 19:46:22 +02:00
46f06c2685 Duplicate logs 2023-06-05 19:46:10 +02:00
d4f47e057c Fix sortNumber when volume null 2023-06-05 19:45:50 +02:00
61712d0537 variable name 2023-06-05 01:03:03 +02:00
1f8f8c09e3 Wrong comparison publicationId -> internalId 2023-06-05 00:50:51 +02:00
0522fa6215 Fix wrong cast 2023-06-05 00:40:19 +02:00
0383a7d686 Merge remote-tracking branch 'origin/master' 2023-06-05 00:38:11 +02:00
bd189984a9 Rewrote entire Task-Structure:
TrangaTask now only contains essentials, derived classes contain specific information such as connectorName, publication, chapter, etc.
Removed taskQueue system, instead all tasks are kept in _allTasks.
Progress is being tracked in TrangaTask resolves #36 resolves #32
Added new TrangaTask: DownloadChapter to download single chapters. #35
Fixed duplicate file-access when writing settings.
2023-06-05 00:38:07 +02:00
58c01b2174 Rewrote entire Task-Structure:
TrangaTask now only contains essentials, derived classes contain specific information such as connectorName, publication, chapter, etc.
Removed taskQueue system, instead all tasks are kept in _allTasks.
Progress is being tracked in TrangaTask
Added new TrangaTask: DownloadChapter to download single chapters.
Fixed duplicate file-access when writing settings.
2023-06-05 00:35:57 +02:00
459558a514 Merge remote-tracking branch 'origin/master' 2023-06-04 21:15:37 +02:00
721b316209 Legal Characters in folder names #38 2023-06-04 21:15:33 +02:00
b1befa2ecc Legal Characters in folder names 2023-06-04 21:14:45 +02:00
57a4cc4ab5 #38 Filenames 2023-06-03 23:44:58 +02:00
655e8db2b6 #38 Publication naming scheme 2023-06-03 23:42:59 +02:00
7cdf77cbb9 Less logging clutter 2023-06-03 22:55:53 +02:00
5a9aed4969 Kavita naming convention 2023-06-03 22:39:27 +02:00
5b41f687d0 Kavita naming convention 2023-06-03 22:34:02 +02:00
d6a62dc315 Manganato download order 2023-06-03 22:25:34 +02:00
6d91788655 Kavita naming convention 2023-06-03 22:25:24 +02:00
14785e5672 increased refresh time 2023-06-03 21:37:21 +02:00
496d502cd2 Kavita Auth is a pain. 2023-06-03 21:26:29 +02:00
7612411917 #33 Update Website 2023-06-03 16:25:04 +02:00
ed1402b5ec TrangaSettings Return libraryManageres on serialization 2023-06-03 16:24:54 +02:00
5adaee4821 redundant calls 2023-06-03 16:24:30 +02:00
2d82fe1489 libraryType in constructor 2023-06-03 16:24:14 +02:00
783fd8129e API: Kavita Auth #33 2023-06-03 15:40:26 +02:00
4f29eff48e Kavita authorization 2023-06-03 15:39:48 +02:00
e0e7abb62b #33 Added custom serializer for LibraryManager
Added Code for Kavita
2023-06-03 15:32:54 +02:00
ae63a216a5 unnecessary params 2023-06-03 15:27:35 +02:00
5d98295c59 #33 Preparation:
TrangaSettings now stores Hashset of LibraryManagers
2023-06-03 15:17:08 +02:00
0c580933f9 #33 Preparation:
Abstracted class Komga into LibraryManager
Fixed logger not attaching to LibraryManager
2023-06-03 15:02:15 +02:00
06f735aadd #32 API endpoint 2023-06-01 23:08:43 +02:00
439d69d8e0 Sanitization Manganto 2023-06-01 22:59:51 +02:00
933df58712 Moved publicationFolder creation to Publication with Permissions 2023-06-01 22:59:04 +02:00
165bbc412e Adjusted Manganato ratelimit 2023-06-01 22:49:20 +02:00
6158fa072b File permissions 2023-06-01 22:32:11 +02:00
0d3799e00d Fix Bug when strings where shorter than 25 characters on logger.writeline
Fixed CLI output
2023-06-01 22:27:37 +02:00
e977bed5a5 #32 formatting length 2023-06-01 22:14:00 +02:00
cacd5fede2 removed unnecessary todo 2023-06-01 22:06:10 +02:00
1bca99cb6a #32 Added progress tracking to task (internal and log use for now) 2023-06-01 22:05:48 +02:00
15fc367263 logging 2023-06-01 21:16:57 +02:00
8bb6fb902b File Permissions 2023-06-01 18:28:58 +02:00
a57903cd5a readme update 2023-06-01 16:04:21 +02:00
1cd819b21d update docker-compose 2023-06-01 15:40:14 +02:00
27afedc1b4 year in series.json 2023-06-01 15:25:26 +02:00
fac0a3f7eb resolves #2 2023-06-01 15:08:32 +02:00
03ca480fe8 remove empty lines at start of description 2023-06-01 14:59:09 +02:00
c2915468a5 status .tolower 2023-06-01 14:58:58 +02:00
8805c53cb8 wrong url for manga info page 2023-06-01 13:37:06 +02:00
adbbe3f6cc logs 2023-06-01 13:27:58 +02:00
14b694d3be Description value duplicate #2 2023-06-01 13:25:58 +02:00
72ce75c6e0 #2 Multiple alt titles 2023-06-01 13:18:26 +02:00
8381951168 #2 First Attempt 2023-06-01 13:13:53 +02:00
b24032d124 remove env var 2023-06-01 11:29:47 +02:00
8bc23f7c69 Instead of relying on concreate tasks to do chores, create method in abstract class that calls the BL in concrete class and does chores before and after execution 2023-06-01 10:35:23 +02:00
48b7371a18 Issue missing parameter 2023-05-31 22:34:13 +02:00
61ecefb615 Logging and Chores in abstract class 2023-05-31 22:32:37 +02:00
8ff65bf400 compatibility with older tasks.json 2023-05-31 22:26:53 +02:00
932057cca0 update execution time 2023-05-31 22:20:33 +02:00
67d06cd887 resolves #23 website filter 2023-05-31 22:16:14 +02:00
cbb012a659 alt and label 2023-05-31 22:16:01 +02:00
e4f33bcca9 Set ExecutionState to waiting after finished 2023-05-31 21:52:50 +02:00
fbba7c45b9 annotation 2023-05-31 21:44:16 +02:00
d9b6062767 Custom JSON Deserializer for concrete classes of abstract class TrangaTask 2023-05-31 21:43:07 +02:00
d477cd1ccd corrected check for tasktype on due 2023-05-31 21:42:23 +02:00
f892db7dda Switch Execution State to Running when executing 2023-05-31 21:40:00 +02:00
16c1b5c506 Create imageCacheFolder 2023-05-31 21:39:18 +02:00
d5ecc1c37d Spelling, redundant calls 2023-05-31 21:18:41 +02:00
1b9ebd096b Removed TaskExecutor
TrangaTask is now abstract
TrangaTask implements Execute Method, that is now called instead of TaskExecutor
Created inheriting classes of TrangaTask: UpdateKomgaLibraryTask, DownloadNewChaptersTask
2023-05-31 21:15:32 +02:00
8619630269 Renamed ExportData to ExportDataAndSettings 2023-05-31 21:14:11 +02:00
2bc92556e3 Method description and Name CopyCoverFromCacheToDownloadLocation 2023-05-31 20:39:57 +02:00
f1ab823e7f Method description 2023-05-31 20:39:23 +02:00
8261d02cc7 Renamed methods, made some methods non-static in Connector, some more logging 2023-05-31 20:29:30 +02:00
8d3b8be95c settings placeholders for komga if configured 2023-05-31 17:55:09 +02:00
60519910de cursors 2023-05-31 17:54:54 +02:00
0940afe41f publication viewer tags and styling 2023-05-31 17:54:09 +02:00
3dc376c19f favicon 2023-05-31 17:52:47 +02:00
3e56ef842b Update docker-compose.yaml with version 2023-05-30 19:37:09 +02:00
5a44e3b8b9 #25 only replace settings if parameter actually contains value. 2023-05-30 19:32:22 +02:00
732c2f119c More logging 2023-05-26 15:09:26 +02:00
81638f4b4a Path.join joins paths ya know 2023-05-26 14:51:11 +02:00
c547aa6422 favicon <3 2023-05-26 14:49:17 +02:00
d80980512e #28 is a pain in the buttcheeks 2023-05-26 14:47:13 +02:00
f9f802155d #28 why was there a '!' 2023-05-26 14:43:47 +02:00
eef0955009 #28 wrong filesnames 2023-05-26 14:36:02 +02:00
ec25900ac0 resolves #29 start manual execution 2023-05-26 14:32:08 +02:00
e5fe14a09e #28 2023-05-26 14:31:34 +02:00
5dc91095f8 #28 2023-05-26 14:30:52 +02:00
985ac8fc7a Fix #28 coverimages 2023-05-26 14:07:11 +02:00
c9537a9963 #24 2023-05-26 13:39:42 +02:00
4fd3c03804 Styling 2023-05-26 13:30:20 +02:00
a1e9dd0232 resolves #27 Foldernames ending in '.' 2023-05-25 22:22:57 +02:00
aa1f9b1b56 background fade 2023-05-25 22:17:47 +02:00
6069578b6e style choices 2023-05-25 22:15:06 +02:00
a84b768e24 design choices 2023-05-25 22:05:29 +02:00
d1a21af15d resolves #26 2023-05-25 21:58:45 +02:00
7423ae6ace Update README.md 2023-05-25 18:36:21 +02:00
3aa7ba9d96 screenshots 2023-05-25 18:28:43 +02:00
fdbb4570be Adjusted settings style 2023-05-25 18:18:31 +02:00
b643a0c2a9 Fix Wrong API uri for GetRunningTasks
Added GetQueue function

Added display for running, queued, total tasks
2023-05-25 18:09:18 +02:00
6fa6f897aa More legal characters 2023-05-25 17:34:24 +02:00
2bfab0298d border-radius 2023-05-25 17:33:55 +02:00
147a20385b illegal filenames 2023-05-25 16:55:58 +02:00
afa18d6a2c Illlegal characters on linux 2023-05-25 16:47:24 +02:00
66980eef23 Position publication viewer always withing display 2023-05-25 16:05:54 +02:00
65f468a30a popup position now "fixed"
changed publicationviewer width
2023-05-25 16:05:40 +02:00
a91c33ee4f image sizing 2023-05-25 15:50:00 +02:00
f39482fe4c Corrected image path in publication preview 2023-05-25 15:48:38 +02:00
41f47b4d6b styling 2023-05-25 15:47:59 +02:00
be40091102 Publication background fade 2023-05-25 15:41:24 +02:00
665092be6a image scaling 2023-05-25 15:40:03 +02:00
653cb699d0 Removed Sidebar
Moved settings tab to popup
Added footer
2023-05-25 15:34:10 +02:00
8dbc5446ad depends_on compose 2023-05-25 14:46:05 +02:00
750df4ed52 Wrong return value 2023-05-25 14:38:43 +02:00
4772ae0756 No unnecessary downloads of covers if they already exist 2023-05-25 14:35:33 +02:00
23f703d5a5 imageCache readonly for website 2023-05-25 14:30:33 +02:00
6aa0ea277b #22 2023-05-25 14:28:56 +02:00
780df1cd6e Created Image-Cache 2023-05-25 14:25:23 +02:00
0b7da2e9cb Merge remote-tracking branch 'origin/master'
# Conflicts:
#	Website/interaction.js
2023-05-25 13:59:06 +02:00
01a059d26b Base 64 images #22 2023-05-25 13:58:54 +02:00
a8dbece237 Base 64 images #22 2023-05-25 13:58:10 +02:00
5efa00e059 Added field posterBase64 to Publication #22 2023-05-25 13:50:48 +02:00
02075ed1b1 Renamed RequestType Cover to CoverUrl 2023-05-25 13:50:08 +02:00
fabd16ccea Remove unnecessary output from dockerfile 2023-05-25 13:49:29 +02:00
153 changed files with 10555 additions and 4227 deletions

View File

@@ -1,25 +0,0 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

21
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Bug Report
description: File a bug report
title: "[It broke]: "
labels: ["bug"]
body:
- type: textarea
attributes:
label: What is broken?
description: What happened? How did we get here?
placeholder: The place where you tell me what you expected to happen, and what happened instead.
validations:
required: true
- type: textarea
attributes:
label: Log-output
description: The output of `docker logs tranga-api`
render: C#
- type: textarea
attributes:
label: Additional stuff
description: Screenshots, anything you think might help

View File

@@ -0,0 +1,23 @@
name: New Connector Request
description: Request a new site to be added
title: "[New Connector]: "
labels: ["New Connector"]
body:
- type: input
attributes:
label: Website-Link
placeholder: https://
validations:
required: true
- type: checkboxes
attributes:
label: Is the Website free to access?
description: We can't support pay-to-use sites, or captcha-proxied sites as Cloudflare.
options:
- label: The Website is freely accessible.
required: true
- type: textarea
attributes:
label: Anything else?
validations:
required: false

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

View File

@@ -0,0 +1,45 @@
name: Docker Image CI
on:
push:
branches: [ "cuttingedge" ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
# https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3.11.1
# https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push API
uses: docker/build-push-action@v6.18.0
with:
context: ./
file: ./Dockerfile
#platforms: linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/arm/v7
pull: true
push: true
tags: |
glax/tranga-api:cuttingedge

View File

@@ -0,0 +1,45 @@
name: Docker Image CI
on:
push:
branches: [ "master" ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
# https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3.11.1
# https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push API
uses: docker/build-push-action@v6.18.0
with:
context: ./
file: ./Dockerfile
#platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6
platforms: linux/amd64,linux/arm64
pull: true
push: true
tags: |
glax/tranga-api:latest

View File

@@ -0,0 +1,45 @@
name: Docker Image CI
on:
push:
branches: [ "testing" ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
# https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3.11.1
# https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push API
uses: docker/build-push-action@v6.18.0
with:
context: ./
file: ./Dockerfile
#platforms: linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/arm/v7
pull: true
push: true
tags: |
glax/tranga-api:testing

8
.gitignore vendored
View File

@@ -18,4 +18,10 @@ riderModule.iml
/dataSources.local.xml
/.idea
cover.jpg
cover.png
cover.png
/.vscode
/.vs/
Tranga/Properties/launchSettings.json
/Manga
/settings
*.DotSettings.user

42
API/API.csproj Normal file
View File

@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.2" />
<PackageReference Include="JikanDotNet" Version="2.9.1" />
<PackageReference Include="log4net" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Npgsql" Version="9.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="3.0.978" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="9.0.4" />
<PackageReference Include="System.Drawing.Common" Version="9.0.8" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\Manga\" />
</ItemGroup>
<ItemGroup>
<None Include="Log4Net.config.xml" CopyToOutputDirectory="Always" CopyToPublishDirectory="Always" />
</ItemGroup>
</Project>

6
API/API.http Normal file
View File

@@ -0,0 +1,6 @@
@API_HostAddress = http://localhost:5105
GET {{API_HostAddress}}/weatherforecast/
Accept: application/json
###

18
API/Constants.cs Normal file
View File

@@ -0,0 +1,18 @@
using SixLabors.ImageSharp;
namespace API;
public struct Constants
{
public const string TRANGA =
"\n\n" +
" _______ v2\n" +
"|_ _|.----..---.-..-----..-----..---.-.\n" +
" | | | _|| _ || || _ || _ |\n" +
" |___| |__| |___._||__|__||___ ||___._|\n" +
" |_____| \n\n";
public static readonly Size ImageSmSize = new (225, 320);
public static readonly Size ImageMdSize = new (450, 640);
public static readonly Size ImageLgSize = new (900, 1280);
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.DTOs;
/// <summary>
/// <see cref="API.Schema.MangaContext.AltTitle"/> DTO
/// </summary>
public sealed record AltTitle(string Language, string Title)
{
/// <summary>
/// Language of the Title
/// </summary>
[Required]
[Description("Language of the Title")]
public string Language { get; init; } = Language;
/// <summary>
/// Title
/// </summary>
[Required]
[Description("Title")]
public string Title { get; init; } = Title;
}

View File

@@ -0,0 +1,17 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.DTOs;
/// <summary>
/// The <see cref="API.Schema.MangaContext.Author"/> DTO
/// </summary>
public sealed record Author(string Key, string Name) : Identifiable(Key)
{
/// <summary>
/// Name of the Author.
/// </summary>
[Required]
[Description("Name of the Author.")]
public string Name { get; init; } = Name;
}

View File

@@ -0,0 +1,52 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.DTOs;
/// <summary>
/// <see cref="API.Schema.MangaContext.Chapter"/> DTO
/// </summary>
public sealed record Chapter(string Key, string MangaId, int? Volume, string ChapterNumber, string? Title, IEnumerable<MangaConnectorId> MangaConnectorIds, bool Downloaded) : Identifiable(Key)
{
/// <summary>
/// Identifier of the Manga this Chapter belongs to
/// </summary>
[Required]
[Description("Identifier of the Manga this Chapter belongs to")]
public string MangaId { get; init; } = MangaId;
/// <summary>
/// Volume number
/// </summary>
[Required]
[Description("Volume number")]
public int? Volume { get; init; } = Volume;
/// <summary>
/// Chapter number
/// </summary>
[Required]
[Description("Chapter number")]
public string ChapterNumber { get; init; } = ChapterNumber;
/// <summary>
/// Title of the Chapter
/// </summary>
[Required]
[Description("Title of the Chapter")]
public string? Title { get; init; } = Title;
/// <summary>
/// Whether Chapter is Downloaded (on disk)
/// </summary>
[Required]
[Description("Whether Chapter is Downloaded (on disk)")]
public bool Downloaded { get; init; } = Downloaded;
/// <summary>
/// Ids of the Manga on MangaConnectors
/// </summary>
[Required]
[Description("Ids of the Manga on MangaConnectors")]
public IEnumerable<MangaConnectorId> MangaConnectorIds { get; init; } = MangaConnectorIds;
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.DTOs;
public sealed record FileLibrary(string Key, string BasePath, string LibraryName) : Identifiable(Key)
{
/// <summary>
/// The directory Path of the library
/// </summary>
[Required]
[Description("The directory Path of the library")]
public string BasePath { get; internal set; } = BasePath;
/// <summary>
/// The Name of the library
/// </summary>
[Required]
[Description("The Name of the library")]
public string LibraryName { get; internal set; } = LibraryName;
}

View File

@@ -0,0 +1,18 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.DTOs;
/// <summary>
/// <see cref="API.Schema.Identifiable"/>
/// </summary>
public record Identifiable(string Key)
{
/// <summary>
/// Unique Identifier of the DTO
/// </summary>
[Required]
[Description("Unique Identifier of the DTO")]
[StringLength(TokenGen.MaximumLength, MinimumLength = TokenGen.MinimumLength)]
public string Key { get; init; } = Key;
}

View File

@@ -0,0 +1,23 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using API.Schema.LibraryContext.LibraryConnectors;
namespace API.Controllers.DTOs;
public record LibraryConnector(string Key, string BaseUrl, LibraryType Type) : Identifiable(Key)
{
/// <summary>
/// The Url of the Library instance
/// </summary>
[Required]
[Url]
[Description("The Url of the Library instance")]
public string BaseUrl { get; init;} = BaseUrl;
/// <summary>
/// The <see cref="LibraryType"/>
/// </summary>
[Required]
[Description("The Library Type")]
public LibraryType Type { get; init; } = Type;
}

View File

@@ -0,0 +1,25 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.DTOs;
/// <summary>
/// <see cref="API.Schema.MangaContext.Link"/> DTO
/// </summary>
public sealed record Link(string Key, string Provider, string Url) : Identifiable(Key)
{
/// <summary>
/// Name of the Provider
/// </summary>
[Required]
[Description("Name of the Provider")]
public string Provider { get; init; } = Provider;
/// <summary>
/// Url
/// </summary>
[Required]
[Description("Url")]
public string Url { get; init; } = Url;
}

View File

@@ -0,0 +1,73 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using API.Schema.MangaContext;
namespace API.Controllers.DTOs;
/// <summary>
/// <see cref="Schema.MangaContext.Manga"/> DTO
/// </summary>
public sealed record Manga(string Key, string Name, string Description, MangaReleaseStatus ReleaseStatus, IEnumerable<MangaConnectorId> MangaConnectorIds, float IgnoreChaptersBefore, uint? Year, string? OriginalLanguage, IEnumerable<string> ChapterIds, IEnumerable<Author> Authors, IEnumerable<string> Tags, IEnumerable<Link> Links, IEnumerable<AltTitle> AltTitles, string? FileLibraryId)
: MinimalManga(Key, Name, Description, ReleaseStatus, MangaConnectorIds)
{
/// <summary>
/// Chapter cutoff for Downloads (Chapters before this will not be downloaded)
/// </summary>
[Required]
[Description("Chapter cutoff for Downloads (Chapters before this will not be downloaded)")]
public float IgnoreChaptersBefore { get; init; } = IgnoreChaptersBefore;
/// <summary>
/// Release Year
/// </summary>
[Description("Release Year")]
public uint? Year { get; init; } = Year;
/// <summary>
/// Release Language
/// </summary>
[Description("Release Language")]
public string? OriginalLanguage { get; init; } = OriginalLanguage;
/// <summary>
/// Keys of ChapterDTOs
/// </summary>
[Required]
[Description("Keys of ChapterDTOs")]
public IEnumerable<string> ChapterIds { get; init; } = ChapterIds;
/// <summary>
/// Author-names
/// </summary>
[Required]
[Description("Author-names")]
public IEnumerable<Author> Authors { get; init; } = Authors;
/// <summary>
/// Manga Tags
/// </summary>
[Required]
[Description("Manga Tags")]
public IEnumerable<string> Tags { get; init; } = Tags;
/// <summary>
/// Links for more Metadata
/// </summary>
[Required]
[Description("Links for more Metadata")]
public IEnumerable<Link> Links { get; init; } = Links;
/// <summary>
/// Alt Titles of Manga
/// </summary>
[Required]
[Description("Alt Titles of Manga")]
public IEnumerable<AltTitle> AltTitles { get; init; } = AltTitles;
/// <summary>
/// Id of the Library the Manga gets downloaded to
/// </summary>
[Required]
[Description("Id of the Library the Manga gets downloaded to")]
public string? FileLibraryId { get; init; } = FileLibraryId;
}

View File

@@ -0,0 +1,28 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.DTOs;
public sealed record MangaConnector(string Name, bool Enabled, string IconUrl, string[] SupportedLanguages) : Identifiable(Name)
{
/// <summary>
/// Whether Connector is used for Searches and Downloads
/// </summary>
[Required]
[Description("Whether Connector is used for Searches and Downloads")]
public bool Enabled { get; init; } = Enabled;
/// <summary>
/// Languages supported by the Connector
/// </summary>
[Required]
[Description("Languages supported by the Connector")]
public string[] SupportedLanguages { get; init; } = SupportedLanguages;
/// <summary>
/// Url of the Website Icon
/// </summary>
[Required]
[Description("Url of the Website Icon")]
public string IconUrl { get; init; } = IconUrl;
}

View File

@@ -0,0 +1,38 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using API.Schema.MangaContext;
namespace API.Controllers.DTOs;
/// <summary>
/// <see cref="MangaConnectorId{T}"/> DTO
/// </summary>
public sealed record MangaConnectorId(string Key, string MangaConnectorName, string ForeignKey, string? WebsiteUrl, bool UseForDownload) : Identifiable(Key)
{
/// <summary>
/// Name of the Connector
/// </summary>
[Required]
[Description("Name of the Connector")]
public string MangaConnectorName { get; init; } = MangaConnectorName;
/// <summary>
/// Key of the referenced DTO
/// </summary>
[Required]
[Description("Key of the referenced DTO")]
public string ForeignKey { get; init; } = ForeignKey;
/// <summary>
/// Website Link for reference, if any
/// </summary>
[Description("Website Link for reference, if any")]
public string? WebsiteUrl { get; init; } = WebsiteUrl;
/// <summary>
/// Whether this Link is used for downloads
/// </summary>
[Required]
[Description("Whether this Link is used for downloads")]
public bool UseForDownload { get; init; } = UseForDownload;
}

View File

@@ -0,0 +1,39 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using API.Schema.MangaContext;
namespace API.Controllers.DTOs;
/// <summary>
/// Shortened Version of <see cref="Manga"/>
/// </summary>
public record MinimalManga(string Key, string Name, string Description, MangaReleaseStatus ReleaseStatus, IEnumerable<MangaConnectorId> MangaConnectorIds) : Identifiable(Key)
{
/// <summary>
/// Name of the Manga
/// </summary>
[Required]
[Description("Name of the Manga")]
public string Name { get; init; } = Name;
/// <summary>
/// Description of the Manga
/// </summary>
[Required]
[Description("Description of the Manga")]
public string Description { get; init; } = Description;
/// <summary>
/// ReleaseStatus of the Manga
/// </summary>
[Required]
[Description("ReleaseStatus of the Manga")]
public MangaReleaseStatus ReleaseStatus { get; init; } = ReleaseStatus;
/// <summary>
/// Ids of the Manga on MangaConnectors
/// </summary>
[Required]
[Description("Ids of the Manga on MangaConnectors")]
public IEnumerable<MangaConnectorId> MangaConnectorIds { get; init; } = MangaConnectorIds;
}

View File

@@ -0,0 +1,46 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.DTOs;
public record NotificationConnector(string Name, string Url, string HttpMethod, string Body, Dictionary<string, string> Headers) : Identifiable(Name)
{
/// <summary>
/// The Name of the Notification Connector
/// </summary>
[Required]
[Description("The Name of the Notification Connector")]
public string Name { get; init; } = Name;
/// <summary>
/// The Url of the Instance
/// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
[Required]
[Url]
[Description("The Url of the Instance")]
public string Url { get; internal set; } = Url;
/// <summary>
/// The HTTP Request Method to use for notifications
/// </summary>
[Required]
[Description("The HTTP Request Method to use for notifications")]
public string HttpMethod { get; internal set; } = HttpMethod;
/// <summary>
/// The Request Body to use to send notifications
/// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
[Required]
[Description("The Request Body to use to send notifications")]
public string Body { get; internal set; } = Body;
/// <summary>
/// The Request Headers to use to send notifications
/// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
[Required]
[Description("The Request Headers to use to send notifications")]
public Dictionary<string, string> Headers { get; internal set; } = Headers;
}

View File

@@ -0,0 +1,33 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using API.Workers;
namespace API.Controllers.DTOs;
/// <summary>
/// <see cref="IPeriodic"/> DTO (<seealso cref="Worker"/> <seealso cref="BaseWorker"/>)
/// </summary>
public sealed record PeriodicWorker(string Key, IEnumerable<string> Dependencies, IEnumerable<string> MissingDependencies, bool DependenciesFulfilled, WorkerExecutionState State, DateTime LastExecution, TimeSpan Interval, DateTime NextExecution)
: Worker(Key, Dependencies, MissingDependencies, DependenciesFulfilled, State)
{
/// <summary>
/// Timestamp when Worker executed last.
/// </summary>
[Required]
[Description("Timestamp when Worker executed last.")]
public DateTime LastExecution { get; init; } = LastExecution;
/// <summary>
/// Interval at which Worker runs.
/// </summary>
[Required]
[Description("Interval at which Worker runs.")]
public TimeSpan Interval { get; init; } = Interval;
/// <summary>
/// Timestamp when Worker is scheduled to execute next.
/// </summary>
[Required]
[Description("Timestamp when Worker is scheduled to execute next.")]
public DateTime NextExecution { get; init; } = LastExecution;
}

View File

@@ -0,0 +1,39 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using API.Workers;
namespace API.Controllers.DTOs;
/// <summary>
/// <see cref="BaseWorker"/> DTO
/// </summary>
public record Worker(string Key, IEnumerable<string> Dependencies, IEnumerable<string> MissingDependencies, bool DependenciesFulfilled, WorkerExecutionState State) : Identifiable(Key)
{
/// <summary>
/// Workers this worker depends on having ran.
/// </summary>
[Required]
[Description("Workers this worker depends on having ran.")]
public IEnumerable<string> Dependencies { get; init; } = Dependencies;
/// <summary>
/// Workers that have not yet ran, that need to run for this Worker to run.
/// </summary>
[Required]
[Description("Workers that have not yet ran, that need to run for this Worker to run.")]
public IEnumerable<string> MissingDependencies { get; init; } = MissingDependencies;
/// <summary>
/// Worker can run.
/// </summary>
[Required]
[Description("Worker can run.")]
public bool DependenciesFulfilled { get; init; } = DependenciesFulfilled;
/// <summary>
/// Execution state of the Worker.
/// </summary>
[Required]
[Description("Execution state of the Worker.")]
public WorkerExecutionState State { get; init; } = State;
}

View File

@@ -0,0 +1,147 @@
using API.Controllers.Requests;
using API.Schema.MangaContext;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes;
using FileLibrary = API.Controllers.DTOs.FileLibrary;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class FileLibraryController(MangaContext context) : Controller
{
/// <summary>
/// Returns all <see cref="DTOs.FileLibrary"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpGet]
[ProducesResponseType<List<FileLibrary>>(Status200OK, "application/json")]
[ProducesResponseType(Status500InternalServerError)]
public async Task<Results<Ok<List<FileLibrary>>, InternalServerError>> GetFileLibraries ()
{
if (await context.FileLibraries.ToListAsync(HttpContext.RequestAborted) is not { } result)
return TypedResults.InternalServerError();
List<FileLibrary> fileLibraries = result.Select(f => new FileLibrary(f.Key, f.BasePath, f.LibraryName)).ToList();
return TypedResults.Ok(fileLibraries);
}
/// <summary>
/// Returns <see cref="FileLibrary"/> with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
[HttpGet("{FileLibraryId}")]
[ProducesResponseType<FileLibrary>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<FileLibrary>, NotFound<string>>> GetFileLibrary (string FileLibraryId)
{
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
return TypedResults.NotFound(nameof(FileLibraryId));
return TypedResults.Ok(new FileLibrary(library.Key, library.BasePath, library.LibraryName));
}
/// <summary>
/// Changes the <see cref="FileLibraryId"/>.BasePath with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <param name="newBasePath">New <see cref="FileLibraryId"/>.BasePath</param>
/// <response code="200"></response>
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{FileLibraryId}/ChangeBasePath")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> ChangeLibraryBasePath (string FileLibraryId, [FromBody]string newBasePath)
{
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
return TypedResults.NotFound(nameof(FileLibraryId));
//TODO Path check
library.BasePath = newBasePath;
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Ok();
}
/// <summary>
/// Changes the <see cref="FileLibraryId"/>.LibraryName with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <param name="newName">New <see cref="FileLibraryId"/>.LibraryName</param>
/// <response code="200"></response>
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{FileLibraryId}/ChangeName")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> ChangeLibraryName (string FileLibraryId, [FromBody] string newName)
{
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
return TypedResults.NotFound(nameof(FileLibraryId));
//TODO Name check
library.LibraryName = newName;
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Ok();
}
/// <summary>
/// Creates new <see cref="FileLibrary"/>
/// </summary>
/// <param name="requestData">New <see cref="FileLibrary"/> to add</param>
/// <response code="200">Key of new Library</response>
/// <response code="500">Error during Database Operation</response>
[HttpPut]
[ProducesResponseType<string>(Status201Created, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Created<string>, InternalServerError<string>>> CreateNewLibrary ([FromBody]CreateLibraryRecord requestData)
{
//TODO Parameter check
Schema.MangaContext.FileLibrary library = new Schema.MangaContext.FileLibrary(requestData.BasePath, requestData.LibraryName);
context.FileLibraries.Add(library);
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Created(string.Empty, library.Key);
}
/// <summary>
/// Deletes the <see cref="FileLibraryId"/>.LibraryName with <paramref name="FileLibraryId"/>
/// </summary>
/// <param name="FileLibraryId"><see cref="FileLibrary"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="FileLibrary"/> with <paramref name="FileLibraryId"/> not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{FileLibraryId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> DeleteLocalLibrary (string FileLibraryId)
{
if(await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == FileLibraryId, HttpContext.RequestAborted) is not { } library)
return TypedResults.NotFound(nameof(FileLibraryId));
context.FileLibraries.Remove(library);
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Ok();
}
}

View File

@@ -0,0 +1,102 @@
using API.Controllers.Requests;
using API.Schema.LibraryContext;
using API.Schema.LibraryContext.LibraryConnectors;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes;
using LibraryConnector = API.Controllers.DTOs.LibraryConnector;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class LibraryConnectorController(LibraryContext context) : Controller
{
/// <summary>
/// Gets all configured <see cref="DTOs.LibraryConnector"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpGet]
[ProducesResponseType<List<LibraryConnector>>(Status200OK, "application/json")]
public async Task<Results<Ok<List<LibraryConnector>>, InternalServerError>> GetAllConnectors ()
{
if (await context.LibraryConnectors.ToListAsync(HttpContext.RequestAborted) is not { } connectors)
return TypedResults.InternalServerError();
List<LibraryConnector> libraryConnectors = connectors.Select(c => new LibraryConnector(c.Key, c.BaseUrl, c.LibraryType)).ToList();
return TypedResults.Ok(libraryConnectors);
}
/// <summary>
/// Returns <see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/>
/// </summary>
/// <param name="LibraryConnectorId"><see cref="LibraryConnector"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/> not found.</response>
[HttpGet("{LibraryConnectorId}")]
[ProducesResponseType<LibraryConnector>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<LibraryConnector>, NotFound<string>>> GetConnector (string LibraryConnectorId)
{
if (await context.LibraryConnectors.FirstOrDefaultAsync(l => l.Key == LibraryConnectorId) is not { } connector)
return TypedResults.NotFound(nameof(LibraryConnectorId));
return TypedResults.Ok(new LibraryConnector(connector.Key, connector.BaseUrl, connector.LibraryType));
}
/// <summary>
/// Creates a new <see cref="LibraryConnector"/>
/// </summary>
/// <param name="requestData"></param>
/// <response code="201"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut]
[ProducesResponseType<string>(Status201Created, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Created<string>, InternalServerError<string>>> CreateConnector ([FromBody]CreateLibraryConnectorRecord requestData)
{
//TODO verify data
API.Schema.LibraryContext.LibraryConnectors.LibraryConnector connector = requestData.LibraryType switch
{
LibraryType.Kavita => new Kavita(requestData.Url, requestData.Username, requestData.Password),
LibraryType.Komga => new Komga(requestData.Url, requestData.Username, requestData.Password),
_ => throw new NotImplementedException()
};
context.LibraryConnectors.Add(connector);
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Created(string.Empty, connector.Key);
}
/// <summary>
/// Deletes <see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/>
/// </summary>
/// <param name="LibraryConnectorId">ToFileLibrary-Connector-ID</param>
/// <response code="200"></response>
/// <response code="404"><see cref="LibraryConnector"/> with <paramref name="LibraryConnectorId"/> not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{LibraryConnectorId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> DeleteConnector (string LibraryConnectorId)
{
if (await context.LibraryConnectors.FirstOrDefaultAsync(l => l.Key == LibraryConnectorId) is not { } connector)
return TypedResults.NotFound(nameof(LibraryConnectorId));
context.LibraryConnectors.Remove(connector);
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Ok();
}
}

View File

@@ -0,0 +1,39 @@
using API.MangaConnectors;
using API.Schema.MangaContext;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes;
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class MaintenanceController(MangaContext mangaContext) : Controller
{
/// <summary>
/// Removes all <see cref="Manga"/> not marked for Download on any <see cref="MangaConnector"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("CleanupNoDownloadManga")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok, InternalServerError<string>>> CleanupNoDownloadManga()
{
if (await mangaContext.Mangas
.Include(m => m.MangaConnectorIds)
.Where(m => !m.MangaConnectorIds.Any(id => id.UseForDownload))
.ToArrayAsync(HttpContext.RequestAborted) is not { } noDownloads)
return TypedResults.InternalServerError("Could not fetch Manga");
mangaContext.Mangas.RemoveRange(noDownloads);
if(await mangaContext.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Ok();
}
}

View File

@@ -0,0 +1,98 @@
using API.Controllers.DTOs;
using API.Schema.MangaContext;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class MangaConnectorController(MangaContext context) : Controller
{
/// <summary>
/// Get all <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites)
/// </summary>
/// <response code="200">Names of <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites)</response>
[HttpGet]
[ProducesResponseType<List<MangaConnector>>(Status200OK, "application/json")]
public Ok<List<MangaConnector>> GetConnectors()
{
return TypedResults.Ok(Tranga.MangaConnectors
.Select(c => new MangaConnector(c.Name, c.Enabled, c.IconUrl, c.SupportedLanguages))
.ToList());
}
/// <summary>
/// Returns the <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites) with the requested Name
/// </summary>
/// <param name="MangaConnectorName"><see cref="API.MangaConnectors.MangaConnector"/>.Name</param>
/// <response code="200"></response>
/// <response code="404"><see cref="MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
[HttpGet("{MangaConnectorName}")]
[ProducesResponseType<MangaConnector>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public Results<Ok<MangaConnector>, NotFound<string>> GetConnector(string MangaConnectorName)
{
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
return TypedResults.NotFound(nameof(MangaConnectorName));
return TypedResults.Ok(new MangaConnector(connector.Name, connector.Enabled, connector.IconUrl, connector.SupportedLanguages));
}
/// <summary>
/// Get all enabled <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites)
/// </summary>
/// <response code="200"></response>
[HttpGet("Enabled")]
[ProducesResponseType<List<MangaConnector>>(Status200OK, "application/json")]
public Ok<List<MangaConnector>> GetEnabledConnectors()
{
return TypedResults.Ok(Tranga.MangaConnectors
.Where(c => c.Enabled)
.Select(c => new MangaConnector(c.Name, c.Enabled, c.IconUrl, c.SupportedLanguages))
.ToList());
}
/// <summary>
/// Get all disabled <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites)
/// </summary>
/// <response code="200"></response>
[HttpGet("Disabled")]
[ProducesResponseType<List<MangaConnector>>(Status200OK, "application/json")]
public Ok<List<MangaConnector>> GetDisabledConnectors()
{
return TypedResults.Ok(Tranga.MangaConnectors
.Where(c => c.Enabled == false)
.Select(c => new MangaConnector(c.Name, c.Enabled, c.IconUrl, c.SupportedLanguages))
.ToList());
}
/// <summary>
/// Enabled or disables <see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites) with Name
/// </summary>
/// <param name="MangaConnectorName"><see cref="API.MangaConnectors.MangaConnector"/>.Name</param>
/// <param name="Enabled">Set true to enable, false to disable</param>
/// <response code="200"></response>
/// <response code="404"><see cref="API.MangaConnectors.MangaConnector"/> (Scanlation-Sites) with Name not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{MangaConnectorName}/SetEnabled/{Enabled}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> SetEnabled(string MangaConnectorName, bool Enabled)
{
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
return TypedResults.NotFound(nameof(MangaConnectorName));
connector.Enabled = Enabled;
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Ok();
}
}

View File

@@ -0,0 +1,579 @@
using API.Controllers.DTOs;
using API.Schema.MangaContext;
using API.Workers;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Net.Http.Headers;
using static Microsoft.AspNetCore.Http.StatusCodes;
using AltTitle = API.Controllers.DTOs.AltTitle;
using Author = API.Controllers.DTOs.Author;
using Chapter = API.Controllers.DTOs.Chapter;
using Link = API.Controllers.DTOs.Link;
using Manga = API.Controllers.DTOs.Manga;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class MangaController(MangaContext context) : Controller
{
/// <summary>
/// Returns all cached <see cref="DTOs.Manga"/>
/// </summary>
/// <response code="200"><see cref="MinimalManga"/> exert of <see cref="Schema.MangaContext.Manga"/>. Use <see cref="GetManga"/> for more information</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet]
[ProducesResponseType<List<MinimalManga>>(Status200OK, "application/json")]
[ProducesResponseType(Status500InternalServerError)]
public async Task<Results<Ok<List<MinimalManga>>, InternalServerError>> GetAllManga ()
{
if (await context.Mangas.Include(m => m.MangaConnectorIds).ToArrayAsync(HttpContext.RequestAborted) is not
{ } result)
return TypedResults.InternalServerError();
return TypedResults.Ok(result.Select(m =>
{
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids);
}).ToList());
}
/// <summary>
/// Returns all cached <see cref="Schema.MangaContext.Manga"/>.Keys
/// </summary>
/// <response code="200"><see cref="Schema.MangaContext.Manga"/> Keys/IDs</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("Keys")]
[ProducesResponseType<string[]>(Status200OK, "application/json")]
[ProducesResponseType(Status500InternalServerError)]
public async Task<Results<Ok<string[]>, InternalServerError>> GetAllMangaKeys ()
{
if (await context.Mangas.Select(m => m.Key).ToArrayAsync(HttpContext.RequestAborted) is not { } result)
return TypedResults.InternalServerError();
return TypedResults.Ok(result);
}
/// <summary>
/// Returns all <see cref="Schema.MangaContext.Manga"/> that are being downloaded from at least one <see cref="API.MangaConnectors.MangaConnector"/>
/// </summary>
/// <response code="200"><see cref="MinimalManga"/> exert of <see cref="Schema.MangaContext.Manga"/>. Use <see cref="GetManga"/> for more information</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("Downloading")]
[ProducesResponseType<MinimalManga[]>(Status200OK, "application/json")]
[ProducesResponseType(Status500InternalServerError)]
public async Task<Results<Ok<List<MinimalManga>>, InternalServerError>> GetMangaDownloading()
{
if (await context.Mangas
.Include(m => m.MangaConnectorIds)
.Where(m => m.MangaConnectorIds.Any(id => id.UseForDownload))
.ToArrayAsync(HttpContext.RequestAborted) is not { } result)
return TypedResults.InternalServerError();
return TypedResults.Ok(result.Select(m =>
{
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids);
}).ToList());
}
/// <summary>
/// Returns all cached <see cref="Schema.MangaContext.Manga"/> with <paramref name="MangaIds"/>
/// </summary>
/// <param name="MangaIds">Array of <see cref="Schema.MangaContext.Manga"/>.Key</param>
/// <response code="200"><see cref="DTOs.Manga"/></response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("WithIDs")]
[ProducesResponseType<List<Manga>>(Status200OK, "application/json")]
[ProducesResponseType(Status500InternalServerError)]
public async Task<Results<Ok<List<Manga>>, InternalServerError>> GetMangaWithIds ([FromBody]string[] MangaIds)
{
if (await context.MangaIncludeAll()
.Where(m => MangaIds.Contains(m.Key))
.ToArrayAsync(HttpContext.RequestAborted) is not { } result)
return TypedResults.InternalServerError();
return TypedResults.Ok(result.Select(m =>
{
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
IEnumerable<Author> authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName));
IEnumerable<string> tags = m.MangaTags.Select(t => t.Tag);
IEnumerable<Link> links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl));
IEnumerable<AltTitle> altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title));
return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles, m.LibraryId);
}).ToList());
}
/// <summary>
/// Return <see cref="Schema.MangaContext.Manga"/> with <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId"><see cref="Schema.MangaContext.Manga"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
[HttpGet("{MangaId}")]
[ProducesResponseType<Manga>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<Manga>, NotFound<string>>> GetManga (string MangaId)
{
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
IEnumerable<MangaConnectorId> ids = manga.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
IEnumerable<Author> authors = manga.Authors.Select(a => new Author(a.Key, a.AuthorName));
IEnumerable<string> tags = manga.MangaTags.Select(t => t.Tag);
IEnumerable<Link> links = manga.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl));
IEnumerable<AltTitle> altTitles = manga.AltTitles.Select(a => new AltTitle(a.Language, a.Title));
Manga result = new (manga.Key, manga.Name, manga.Description, manga.ReleaseStatus, ids, manga.IgnoreChaptersBefore, manga.Year, manga.OriginalLanguage, manga.ChapterIds, authors, tags, links, altTitles, manga.LibraryId);
return TypedResults.Ok(result);
}
/// <summary>
/// Delete <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{MangaId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> DeleteManga (string MangaId)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
context.Mangas.Remove(manga);
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Ok();
}
/// <summary>
/// Merge two <see cref="Manga"/> into one. THIS IS NOT REVERSIBLE!
/// </summary>
/// <param name="MangaIdFrom"><see cref="Manga"/>.Key of <see cref="Manga"/> merging data from (getting deleted)</param>
/// <param name="MangaIdInto"><see cref="Manga"/>.Key of <see cref="Manga"/> merging data into</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaIdFrom"/> or <paramref name="MangaIdInto"/> not found</response>
[HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdInto}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok, NotFound<string>>> MergeIntoManga (string MangaIdFrom, string MangaIdInto)
{
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdFrom, HttpContext.RequestAborted) is not { } from)
return TypedResults.NotFound(nameof(MangaIdFrom));
if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdInto, HttpContext.RequestAborted) is not { } into)
return TypedResults.NotFound(nameof(MangaIdInto));
BaseWorker[] newJobs = into.MergeFrom(from, context);
Tranga.AddWorkers(newJobs);
return TypedResults.Ok();
}
/// <summary>
/// Returns Cover of <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="CoverSize">Size of the cover returned
/// <br /> - <see cref="CoverSize.Small"/> <see cref="Constants.ImageSmSize"/>
/// <br /> - <see cref="CoverSize.Medium"/> <see cref="Constants.ImageMdSize"/>
/// <br /> - <see cref="CoverSize.Large"/> <see cref="Constants.ImageLgSize"/>
/// </param>
/// <response code="200">JPEG Image</response>
/// <response code="204">Cover not loaded</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="503">Retry later, downloading cover</response>
[HttpGet("{MangaId}/Cover/{CoverSize?}")]
[ProducesResponseType<FileContentResult>(Status200OK,"image/jpeg")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType(Status503ServiceUnavailable)]
public async Task<Results<FileContentHttpResult, NoContent, BadRequest, NotFound<string>, StatusCodeHttpResult>> GetCover (string MangaId, CoverSize? CoverSize = null)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
string cache = CoverSize switch
{
MangaController.CoverSize.Small => TrangaSettings.coverImageCacheSmall,
MangaController.CoverSize.Medium => TrangaSettings.coverImageCacheMedium,
MangaController.CoverSize.Large => TrangaSettings.coverImageCacheLarge,
_ => TrangaSettings.coverImageCacheOriginal
};
if (await manga.GetCoverImage(cache, HttpContext.RequestAborted) is not { } data)
{
if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId))
{
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return TypedResults.StatusCode(Status503ServiceUnavailable);
}
return TypedResults.NoContent();
}
DateTime lastModified = data.fileInfo.LastWriteTime;
EntityTagHeaderValue entityTagHeaderValue = EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\"");
if(HttpContext.Request.Headers.ETag.Equals(entityTagHeaderValue.Tag.Value))
return TypedResults.StatusCode(Status304NotModified);
HttpContext.Response.Headers.CacheControl = "public";
return TypedResults.Bytes(data.stream.ToArray(), "image/jpeg", lastModified: new DateTimeOffset(lastModified), entityTag: entityTagHeaderValue);
}
public enum CoverSize { Original, Large, Medium, Small }
/// <summary>
/// Returns all <see cref="Chapter"/> of <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
[HttpGet("{MangaId}/Chapters")]
[ProducesResponseType<List<Chapter>>(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public async Task<Results<Ok<List<Chapter>>, NotFound<string>>> GetChapters(string MangaId)
{
if (await context.Mangas.Include(m => m.Chapters).ThenInclude(c => c.MangaConnectorIds).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
List<Chapter> chapters = manga.Chapters.Select(c =>
{
IEnumerable<MangaConnectorId> ids = c.MangaConnectorIds.Select(id =>
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded);
}).ToList();
return TypedResults.Ok(chapters);
}
/// <summary>
/// Returns all downloaded <see cref="Chapter"/> for <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
[HttpGet("{MangaId}/Chapters/Downloaded")]
[ProducesResponseType<Chapter[]>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<List<Chapter>>, NoContent, NotFound<string>>> GetChaptersDownloaded(string MangaId)
{
if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded).Select(c =>
{
IEnumerable<MangaConnectorId> ids = c.MangaConnectorIds.Select(id =>
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded);
}).ToList();
if (chapters.Count == 0)
return TypedResults.NoContent();
return TypedResults.Ok(chapters);
}
/// <summary>
/// Returns all <see cref="Chapter"/> not downloaded for <see cref="Manga"/> with <paramref name="MangaId"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
[HttpGet("{MangaId}/Chapters/NotDownloaded")]
[ProducesResponseType<List<Chapter>>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<List<Chapter>>, NoContent, NotFound<string>>> GetChaptersNotDownloaded(string MangaId)
{
if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
List<Chapter> chapters = manga.Chapters.Where(c => c.Downloaded == false).Select(c =>
{
IEnumerable<MangaConnectorId> ids = c.MangaConnectorIds.Select(id =>
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded);
}).ToList();
if (chapters.Count == 0)
return TypedResults.NoContent();
return TypedResults.Ok(chapters);
}
/// <summary>
/// Returns the latest <see cref="Chapter"/> of requested <see cref="Manga"/> available on <see cref="API.MangaConnectors.MangaConnector"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
/// <response code="412">Could not retrieve the maximum chapter-number</response>
/// <response code="503">Retry after timeout, updating value</response>
[HttpGet("{MangaId}/Chapter/LatestAvailable")]
[ProducesResponseType<int>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType(Status500InternalServerError)]
[ProducesResponseType(Status503ServiceUnavailable)]
public async Task<Results<Ok<Chapter>, NoContent, InternalServerError, NotFound<string>, StatusCodeHttpResult>> GetLatestChapter(string MangaId)
{
if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
List<API.Schema.MangaContext.Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0)
{
if (Tranga.GetRunningWorkers().Any(worker =>
worker is RetrieveMangaChaptersFromMangaconnectorWorker w &&
context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId &&
w.State < WorkerExecutionState.Completed))
{
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return TypedResults.StatusCode(Status503ServiceUnavailable);
}
else
return TypedResults.NoContent();
}
API.Schema.MangaContext.Chapter? max = chapters.Max();
if (max is null)
return TypedResults.InternalServerError();
foreach (CollectionEntry collectionEntry in context.Entry(max).Collections)
await collectionEntry.LoadAsync(HttpContext.RequestAborted);
IEnumerable<MangaConnectorId> ids = max.MangaConnectorIds.Select(id =>
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
return TypedResults.Ok(new Chapter(max.Key, max.ParentMangaId, max.VolumeNumber, max.ChapterNumber, max.Title,ids, max.Downloaded));
}
/// <summary>
/// Returns the latest <see cref="Chapter"/> of requested <see cref="Manga"/> that is downloaded
/// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <response code="200"></response>
/// <response code="204">No available chapters</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
/// <response code="412">Could not retrieve the maximum chapter-number</response>
/// <response code="503">Retry after timeout, updating value</response>
[HttpGet("{MangaId}/Chapter/LatestDownloaded")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType(Status412PreconditionFailed)]
[ProducesResponseType(Status503ServiceUnavailable)]
public async Task<Results<Ok<Chapter>, NoContent, NotFound<string>, StatusCodeHttpResult>> GetLatestChapterDownloaded(string MangaId)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(HttpContext.RequestAborted);
List<API.Schema.MangaContext.Chapter> chapters = manga.Chapters.ToList();
if (chapters.Count == 0)
{
if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed))
{
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return TypedResults.StatusCode(Status503ServiceUnavailable);
}else
return TypedResults.NoContent();
}
API.Schema.MangaContext.Chapter? max = chapters.Max();
if (max is null)
return TypedResults.StatusCode(Status412PreconditionFailed);
IEnumerable<MangaConnectorId> ids = max.MangaConnectorIds.Select(id =>
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
return TypedResults.Ok(new Chapter(max.Key, max.ParentMangaId, max.VolumeNumber, max.ChapterNumber, max.Title,ids, max.Downloaded));
}
/// <summary>
/// Configure the <see cref="Chapter"/> cut-off for <see cref="Manga"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="chapterThreshold">Threshold (<see cref="Chapter"/> ChapterNumber)</param>
/// <response code="202"></response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found.</response>
/// <response code="500">Error during Database Operation</response>
[HttpPatch("{MangaId}/IgnoreChaptersBefore")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
manga.IgnoreChaptersBefore = chapterThreshold;
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Ok();
}
/// <summary>
/// Move <see cref="Manga"/> to different <see cref="FileLibrary"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="LibraryId"><see cref="FileLibrary"/>.Key</param>
/// <response code="202">Folder is going to be moved</response>
/// <response code="404"><paramref name="MangaId"/> or <paramref name="LibraryId"/> not found</response>
[HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok, NotFound<string>>> ChangeLibrary(string MangaId, string LibraryId)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
if (await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, HttpContext.RequestAborted) is not { } library)
return TypedResults.NotFound(nameof(LibraryId));
if(manga.LibraryId == library.Key)
return TypedResults.Ok();
MoveMangaLibraryWorker moveLibrary = new(manga, library);
Tranga.AddWorkers([moveLibrary]);
return TypedResults.Ok();
}
/// <summary>
/// (Un-)Marks <see cref="Manga"/> as requested for Download from <see cref="API.MangaConnectors.MangaConnector"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/> with <paramref name="MangaId"/></param>
/// <param name="MangaConnectorName"><see cref="API.MangaConnectors.MangaConnector"/> with <paramref name="MangaConnectorName"/></param>
/// <param name="IsRequested">true to mark as requested, false to mark as not-requested</param>
/// <response code="200"></response>
/// <response code="404"><paramref name="MangaId"/> or <paramref name="MangaConnectorName"/> not found</response>
/// <response code="412"><see cref="Manga"/> was not linked to <see cref="API.MangaConnectors.MangaConnector"/>, so nothing changed</response>
/// <response code="428"><see cref="Manga"/> is not linked to <see cref="API.MangaConnectors.MangaConnector"/> yet. Search for <see cref="Manga"/> on <see cref="API.MangaConnectors.MangaConnector"/> first (to create a <see cref="MangaConnectorId{T}"/>).</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MangaId}/SetAsDownloadFrom/{MangaConnectorName}/{IsRequested}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status412PreconditionFailed, "text/plain")]
[ProducesResponseType<string>(Status428PreconditionRequired, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok, NotFound<string>, StatusCodeHttpResult, InternalServerError<string>>> MarkAsRequested(string MangaId, string MangaConnectorName, bool IsRequested)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } _)
return TypedResults.NotFound(nameof(MangaId));
if(!Tranga.TryGetMangaConnector(MangaConnectorName, out API.MangaConnectors.MangaConnector? _))
return TypedResults.NotFound(nameof(MangaConnectorName));
if (context.MangaConnectorToManga
.FirstOrDefault(id => id.MangaConnectorName == MangaConnectorName && id.ObjId == MangaId)
is not { } mcId)
{
if(IsRequested)
return TypedResults.StatusCode(Status428PreconditionRequired);
else
return TypedResults.StatusCode(Status412PreconditionFailed);
}
mcId.UseForDownload = IsRequested;
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId);
RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage);
Tranga.AddWorkers([downloadCover, retrieveChapters]);
return TypedResults.Ok();
}
/// <summary>
/// Initiate a search for <see cref="API.Schema.MangaContext.Manga"/> on a different <see cref="API.MangaConnectors.MangaConnector"/>
/// </summary>
/// <param name="MangaId"><see cref="API.Schema.MangaContext.Manga"/> with <paramref name="MangaId"/></param>
/// <param name="MangaConnectorName"><see cref="API.MangaConnectors.MangaConnector"/>.Name</param>
/// <response code="200"><see cref="MinimalManga"/> exert of <see cref="Schema.MangaContext.Manga"/></response>
/// <response code="404"><see cref="API.MangaConnectors.MangaConnector"/> with Name not found</response>
/// <response code="412"><see cref="API.MangaConnectors.MangaConnector"/> with Name is disabled</response>
[HttpPost("{MangaId}/SearchOn/{MangaConnectorName}")]
[ProducesResponseType<List<MinimalManga>>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType(Status406NotAcceptable)]
public async Task<Results<Ok<List<MinimalManga>>, NotFound<string>, StatusCodeHttpResult>> SearchOnDifferentConnector (string MangaId, string MangaConnectorName)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
return new SearchController(context).SearchManga(MangaConnectorName, manga.Name);
}
/// <summary>
/// Returns all <see cref="Manga"/> which where Authored by <see cref="Author"/> with <paramref name="AuthorId"/>
/// </summary>
/// <param name="AuthorId"><see cref="Author"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/></response>
/// /// <response code="500">Error during Database Operation</response>
[HttpGet("WithAuthorId/{AuthorId}")]
[ProducesResponseType<List<Manga>>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<List<Manga>>, NotFound<string>, InternalServerError>> GetMangaWithAuthorIds (string AuthorId)
{
if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } _)
return TypedResults.NotFound(nameof(AuthorId));
if (await context.MangaIncludeAll().Where(m => m.Authors.Any(a => a.Key == AuthorId)).ToListAsync(HttpContext.RequestAborted) is not { } result)
return TypedResults.InternalServerError();
return TypedResults.Ok(result.Select(m =>
{
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
IEnumerable<Author> authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName));
IEnumerable<string> tags = m.MangaTags.Select(t => t.Tag);
IEnumerable<Link> links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl));
IEnumerable<AltTitle> altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title));
return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles, m.LibraryId);
}).ToList());
}
/// <summary>
/// Returns all <see cref="Manga"/> with <see cref="Tag"/>
/// </summary>
/// <param name="Tag"><see cref="Tag"/>.Tag</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Tag"/> not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("WithTag/{Tag}")]
[ProducesResponseType<Manga[]>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType(Status500InternalServerError)]
public async Task<Results<Ok<List<Manga>>, NotFound<string>, InternalServerError>> GetMangasWithTag (string Tag)
{
if (await context.Tags.FirstOrDefaultAsync(t => t.Tag == Tag, HttpContext.RequestAborted) is not { } tag)
return TypedResults.NotFound(nameof(Tag));
if (await context.MangaIncludeAll().Where(m => m.MangaTags.Any(t => t.Tag.Equals(tag))) .ToListAsync(HttpContext.RequestAborted) is not { } result)
return TypedResults.InternalServerError();
return TypedResults.Ok(result.Select(m =>
{
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
IEnumerable<Author> authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName));
IEnumerable<string> tags = m.MangaTags.Select(t => t.Tag);
IEnumerable<Link> links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl));
IEnumerable<AltTitle> altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title));
return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles, m.LibraryId);
}).ToList());
}
}

View File

@@ -0,0 +1,129 @@
using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class MetadataFetcherController(MangaContext context) : Controller
{
/// <summary>
/// Get all <see cref="MetadataFetcher"/> (Metadata-Sites)
/// </summary>
/// <response code="200">Names of <see cref="MetadataFetcher"/> (Metadata-Sites)</response>
[HttpGet]
[ProducesResponseType<List<string>>(Status200OK, "application/json")]
public Ok<List<string>> GetConnectors ()
{
return TypedResults.Ok(Tranga.MetadataFetchers.Select(m => m.Name).ToList());
}
/// <summary>
/// Returns all <see cref="MetadataEntry"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("Links")]
[ProducesResponseType<List<MetadataEntry>>(Status200OK, "application/json")]
[ProducesResponseType(Status500InternalServerError)]
public async Task<Results<Ok<List<MetadataEntry>>, InternalServerError>> GetLinkedEntries ()
{
if (await context.MetadataEntries.ToListAsync() is not { } result)
return TypedResults.InternalServerError();
return TypedResults.Ok(result);
}
/// <summary>
/// Searches <see cref="MetadataFetcher"/> (Metadata-Sites) for Manga-Metadata
/// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="MetadataFetcherName"><see cref="MetadataFetcher"/>.Name</param>
/// <param name="searchTerm">Instead of using the <paramref name="MangaId"/> for search on Website, use a specific term</param>
/// <response code="200"></response>
/// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
[HttpPost("{MetadataFetcherName}/SearchManga/{MangaId}")]
[ProducesResponseType<MetadataSearchResult[]>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<List<MetadataSearchResult>>, BadRequest, NotFound<string>>> SearchMangaMetadata(string MangaId, string MetadataFetcherName, [FromBody (EmptyBodyBehavior = EmptyBodyBehavior.Allow)]string? searchTerm = null)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
return TypedResults.BadRequest();
MetadataSearchResult[] searchResults = searchTerm is null ? fetcher.SearchMetadataEntry(manga) : fetcher.SearchMetadataEntry(searchTerm);
return TypedResults.Ok(searchResults.ToList());
}
/// <summary>
/// Links <see cref="MetadataFetcher"/> (Metadata-Sites) using Provider-Specific Identifier to <see cref="Manga"/>
/// </summary>
/// <param name="MangaId"><see cref="Manga"/>.Key</param>
/// <param name="MetadataFetcherName"><see cref="MetadataFetcher"/>.Name</param>
/// <param name="Identifier"><see cref="MetadataFetcherName"/>-Specific ID</param>
/// <response code="200"></response>
/// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MetadataFetcherName}/Link/{MangaId}")]
[ProducesResponseType<MetadataEntry>(Status200OK, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok<MetadataEntry>, BadRequest, NotFound<string>, InternalServerError<string>>> LinkMangaMetadata (string MangaId, string MetadataFetcherName, [FromBody]string Identifier)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is not { } fetcher)
return TypedResults.BadRequest();
MetadataEntry entry = fetcher.CreateMetadataEntry(manga, Identifier);
context.MetadataEntries.Add(entry);
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Ok(entry);
}
/// <summary>
/// Un-Links <see cref="MetadataFetcher"/> (Metadata-Sites) from <see cref="Manga"/>
/// </summary>
/// <response code="200"></response>
/// <response code="400"><see cref="MetadataFetcher"/> (Metadata-Sites) with <paramref name="MetadataFetcherName"/> does not exist</response>
/// <response code="404"><see cref="Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="412">No <see cref="MetadataEntry"/> linking <see cref="Manga"/> and <see cref="MetadataFetcher"/> found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("{MetadataFetcherName}/Unlink/{MangaId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType(Status412PreconditionFailed)]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok, BadRequest, NotFound<string>, InternalServerError<string>, StatusCodeHttpResult>> UnlinkMangaMetadata (string MangaId, string MetadataFetcherName)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } _)
return TypedResults.NotFound(nameof(MangaId));
if(Tranga.MetadataFetchers.FirstOrDefault(f => f.Name == MetadataFetcherName) is null)
return TypedResults.BadRequest();
if (context.MetadataEntries.FirstOrDefault(e =>
e.MangaId == MangaId && e.MetadataFetcherName == MetadataFetcherName) is not { } entry)
return TypedResults.StatusCode(Status412PreconditionFailed);
context.Remove(entry);
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Ok();
}
}

View File

@@ -0,0 +1,165 @@
using System.Text;
using API.Controllers.DTOs;
using API.Controllers.Requests;
using API.Schema.NotificationsContext;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Produces("application/json")]
[Route("v{v:apiVersion}/[controller]")]
public class NotificationConnectorController(NotificationsContext context) : Controller
{
/// <summary>
/// Gets all configured <see cref="NotificationConnector"/>
/// </summary>
/// <response code="200"></response>
/// <response code="500">Error during Database Operation</response>
[HttpGet]
[ProducesResponseType<List<NotificationConnector>>(Status200OK, "application/json")]
[ProducesResponseType(Status500InternalServerError)]
public async Task<Results<Ok<List<NotificationConnector>>, InternalServerError>> GetAllConnectors ()
{
if(await context.NotificationConnectors.ToListAsync(HttpContext.RequestAborted) is not { } result)
return TypedResults.InternalServerError();
List<NotificationConnector> notificationConnectors = result.Select(n => new NotificationConnector(n.Name, n.Url, n.HttpMethod, n.Body, n.Headers)).ToList();
return TypedResults.Ok(notificationConnectors);
}
/// <summary>
/// Returns <see cref="NotificationConnector"/> with requested Name
/// </summary>
/// <param name="Name"><see cref="NotificationConnector"/>.Name</param>
/// <response code="200"></response>
/// <response code="404"><see cref="NotificationConnector"/> with <paramref name="Name"/> not found</response>
[HttpGet("{Name}")]
[ProducesResponseType<NotificationConnector>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<NotificationConnector>, NotFound<string>>> GetConnector (string Name)
{
if (await context.NotificationConnectors.FirstOrDefaultAsync(c => c.Name == Name, HttpContext.RequestAborted) is not { } connector)
return TypedResults.NotFound(nameof(Name));
NotificationConnector notificationConnector = new NotificationConnector(connector.Name, connector.Url, connector.HttpMethod, connector.Body, connector.Headers);
return TypedResults.Ok(notificationConnector);
}
/// <summary>
/// Creates a new <see cref="NotificationConnector"/>
/// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent</remarks>
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut]
[ProducesResponseType<string>(Status201Created, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Created<string>, InternalServerError<string>>> CreateConnector ([FromBody]CreateNotificationConnectorRecord requestData)
{
// TODO validate data
API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector newConnector =
new(requestData.Name, requestData.Url, requestData.Headers, requestData.HttpMethod, requestData.Body);
context.NotificationConnectors.Add(newConnector);
context.Notifications.Add(new ("Added new Notification Connector!", newConnector.Name, NotificationUrgency.High));
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Created(string.Empty, newConnector.Name);
}
/// <summary>
/// Creates a new Gotify-<see cref="NotificationConnector"/>
/// </summary>
/// <remarks>Priority needs to be between 0 and 10</remarks>
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("Gotify")]
[ProducesResponseType<string>(Status201Created, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Created<string>, InternalServerError<string>>> CreateGotifyConnector ([FromBody]CreateGotifyConnectorRecord createGotifyConnectorData)
{
//TODO Validate Data
CreateNotificationConnectorRecord gotifyConnector = new (createGotifyConnectorData.Name,
createGotifyConnectorData.Url,
"POST",
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {createGotifyConnectorData.Priority}}}",
new () { { "X-Gotify-Key", createGotifyConnectorData.AppToken } });
return await CreateConnector(gotifyConnector);
}
/// <summary>
/// Creates a new Ntfy-<see cref="NotificationConnector"/>
/// </summary>
/// <remarks>Priority needs to be between 1 and 5</remarks>
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("Ntfy")]
[ProducesResponseType<string>(Status201Created, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Created<string>, InternalServerError<string>>> CreateNtfyConnector ([FromBody]CreateNtfyConnectorRecord createNtfyConnectorRecord)
{
//TODO Validate Data
string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{createNtfyConnectorRecord.Username}:{createNtfyConnectorRecord.Password}"));
string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=","");
CreateNotificationConnectorRecord ntfyConnector = new (createNtfyConnectorRecord.Name,
$"{createNtfyConnectorRecord.Url}?auth={auth}",
"POST",
$"{{\"message\": \"%text\", \"title\": \"%title\", \"Priority\": {createNtfyConnectorRecord.Priority} \"Topic\": \"{createNtfyConnectorRecord.Topic}\"}}",
new () {{"Authorization", auth}});
return await CreateConnector(ntfyConnector);
}
/// <summary>
/// Creates a new Pushover-<see cref="NotificationConnector"/>
/// </summary>
/// <remarks>https://pushover.net/api</remarks>
/// <response code="200">ID of the new <see cref="NotificationConnector"/></response>
/// <response code="500">Error during Database Operation</response>
[HttpPut("Pushover")]
[ProducesResponseType<string>(Status201Created, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Created<string>, InternalServerError<string>>> CreatePushoverConnector ([FromBody]CreatePushoverConnectorRecord createPushoverConnectorRecord)
{
//TODO Validate Data
CreateNotificationConnectorRecord pushoverConnector = new (createPushoverConnectorRecord.Name,
$"https://api.pushover.net/1/messages.json",
"POST",
$"{{\"token\": \"{createPushoverConnectorRecord.AppToken}\", \"user\": \"{createPushoverConnectorRecord.Username}\", \"message:\":\"%text\", \"%title\" }}",
new ());
return await CreateConnector(pushoverConnector);
}
/// <summary>
/// Deletes the <see cref="NotificationConnector"/> with the requested Name
/// </summary>
/// <param name="Name"><see cref="NotificationConnector"/>.Name</param>
/// <response code="200"></response>
/// <response code="404"><see cref="NotificationConnector"/> with Name not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpDelete("{Name}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok, NotFound<string>, InternalServerError<string>>> DeleteConnector (string Name)
{
if (await context.NotificationConnectors.FirstOrDefaultAsync(c => c.Name == Name, HttpContext.RequestAborted) is not { } connector)
return TypedResults.NotFound(nameof(Name));
context.NotificationConnectors.Remove(connector);
if(await context.Sync(HttpContext.RequestAborted) is { success: false } result)
return TypedResults.InternalServerError(result.exceptionMessage);
return TypedResults.Ok();
}
}

View File

@@ -0,0 +1,124 @@
using API.Controllers.DTOs;
using API.Schema.MangaContext;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Soenneker.Utils.String.NeedlemanWunsch;
using static Microsoft.AspNetCore.Http.StatusCodes;
using Author = API.Controllers.DTOs.Author;
using Chapter = API.Controllers.DTOs.Chapter;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class QueryController(MangaContext context) : Controller
{
/// <summary>
/// Returns the <see cref="Author"/> with <paramref name="AuthorId"/>
/// </summary>
/// <param name="AuthorId"><see cref="Author"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Author"/> with <paramref name="AuthorId"/> not found</response>
[HttpGet("Author/{AuthorId}")]
[ProducesResponseType<Author>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<Author>, NotFound<string>>> GetAuthor (string AuthorId)
{
if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } author)
return TypedResults.NotFound(nameof(AuthorId));
return TypedResults.Ok(new Author(author.Key, author.AuthorName));
}
/// <summary>
/// Returns <see cref="Chapter"/> with <paramref name="ChapterId"/>
/// </summary>
/// <param name="ChapterId"><see cref="Chapter"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="Chapter"/> with <paramref name="ChapterId"/> not found</response>
[HttpGet("Chapter/{ChapterId}")]
[ProducesResponseType<Chapter>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<Chapter>, NotFound<string>>> GetChapter (string ChapterId)
{
if (await context.Chapters.FirstOrDefaultAsync(c => c.Key == ChapterId, HttpContext.RequestAborted) is not { } chapter)
return TypedResults.NotFound(nameof(ChapterId));
IEnumerable<MangaConnectorId> ids = chapter.MangaConnectorIds.Select(id =>
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
return TypedResults.Ok(new Chapter(chapter.Key, chapter.ParentMangaId, chapter.VolumeNumber, chapter.ChapterNumber, chapter.Title,ids, chapter.Downloaded));
}
/// <summary>
/// Returns the <see cref="MangaConnectorId{Manga}"/> with <see cref="MangaConnectorId{Manga}"/>.Key
/// </summary>
/// <param name="MangaConnectorIdId">Key of <see cref="MangaConnectorId{Manga}"/></param>
/// <response code="200"></response>
/// <response code="404"><see cref="MangaConnectorId{Manga}"/> with <paramref name="MangaConnectorIdId"/> not found</response>
[HttpGet("Manga/MangaConnectorId/{MangaConnectorIdId}")]
[ProducesResponseType<MangaConnectorId>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<MangaConnectorId>, NotFound<string>>> GetMangaMangaConnectorId (string MangaConnectorIdId)
{
if (await context.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, HttpContext.RequestAborted) is not { } mcIdManga)
return TypedResults.NotFound(nameof(MangaConnectorIdId));
MangaConnectorId result = new (mcIdManga.Key, mcIdManga.MangaConnectorName, mcIdManga.ObjId, mcIdManga.WebsiteUrl, mcIdManga.UseForDownload);
return TypedResults.Ok(result);
}
/// <summary>
/// Returns <see cref="Schema.MangaContext.Manga"/> with names similar to <see cref="Schema.MangaContext.Manga"/> (identified by <paramref name="MangaId"/>)
/// </summary>
/// <param name="MangaId">Key of <see cref="Schema.MangaContext.Manga"/></param>
/// <response code="200"></response>
/// <response code="404"><see cref="Schema.MangaContext.Manga"/> with <paramref name="MangaId"/> not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpGet("Manga/{MangaId}/SimilarName")]
[ProducesResponseType<List<string>>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType(Status500InternalServerError)]
public async Task<Results<Ok<List<string>>, NotFound<string>, InternalServerError>> GetSimilarManga (string MangaId)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
string name = manga.Name;
if (await context.Mangas.Where(m => m.Key != MangaId)
.ToDictionaryAsync(m => m.Key, m => m.Name, HttpContext.RequestAborted) is not { } mangaNames)
return TypedResults.InternalServerError();
List<string> similarIds = mangaNames
.Where(kv => NeedlemanWunschStringUtil.CalculateSimilarityPercentage(name, kv.Value) > 0.8)
.Select(kv => kv.Key)
.ToList();
return TypedResults.Ok(similarIds);
}
/// <summary>
/// Returns the <see cref="MangaConnectorId{Chapter}"/> with <see cref="MangaConnectorId{Chapter}"/>.Key
/// </summary>
/// <param name="MangaConnectorIdId">Key of <see cref="MangaConnectorId{Manga}"/></param>
/// <response code="200"></response>
/// <response code="404"><see cref="MangaConnectorId{Chapter}"/> with <paramref name="MangaConnectorIdId"/> not found</response>
[HttpGet("Chapter/MangaConnectorId/{MangaConnectorIdId}")]
[ProducesResponseType<MangaConnectorId>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public async Task<Results<Ok<MangaConnectorId>, NotFound<string>>> GetChapterMangaConnectorId (string MangaConnectorIdId)
{
if (await context.MangaConnectorToManga.FirstOrDefaultAsync(c => c.Key == MangaConnectorIdId, HttpContext.RequestAborted) is not { } mcIdChapter)
return TypedResults.NotFound(nameof(MangaConnectorIdId));
MangaConnectorId result = new(mcIdChapter.Key, mcIdChapter.MangaConnectorName, mcIdChapter.ObjId, mcIdChapter.WebsiteUrl, mcIdChapter.UseForDownload);
return TypedResults.Ok(result);
}
}

View File

@@ -0,0 +1,37 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.Requests;
public record CreateGotifyConnectorRecord(string Name, string Url, string AppToken, int Priority)
{
/// <summary>
/// The Name of the Notification Connector
/// </summary>
[Required]
[Description("The Name of the Notification Connector")]
public string Name { get; init; } = Name;
/// <summary>
/// The Url of the Instance
/// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
[Required]
[Url]
[Description("The Url of the Instance")]
public string Url { get; internal set; } = Url;
/// <summary>
/// The Apptoken used for authentication
/// </summary>
[Required]
[Description("The Apptoken used for authentication")]
public string AppToken { get; init; } = AppToken;
/// <summary>
/// The Priority of Notifications
/// </summary>
[Required]
[Description("The Priority of Notifications")]
public int Priority { get; init; } = Priority;
}

View File

@@ -0,0 +1,37 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using API.Schema.LibraryContext.LibraryConnectors;
namespace API.Controllers.Requests;
public sealed record CreateLibraryConnectorRecord(LibraryType LibraryType, string Url, string Username, string Password)
{
/// <summary>
/// The <see cref="LibraryType"/>
/// </summary>
[Required]
[Description("The Library Type")]
public LibraryType LibraryType { get; init; } = LibraryType;
/// <summary>
/// The Url of the Library instance
/// </summary>
[Required]
[Url]
[Description("The Url of the Library instance")]
public string Url { get; init; } = Url;
/// <summary>
/// The Username to authenticate to the Library instance
/// </summary>
[Required]
[Description("The Username to authenticate to the Library instance")]
public string Username { get; init; } = Username;
/// <summary>
/// The Password to authenticate to the Library instance
/// </summary>
[Required]
[Description("The Password to authenticate to the Library instance")]
public string Password { get; init; } = Password;
}

View File

@@ -0,0 +1,21 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.Requests;
public sealed record CreateLibraryRecord(string BasePath, string LibraryName)
{
/// <summary>
/// The directory Path of the library
/// </summary>
[Required]
[Description("The directory Path of the library")]
public string BasePath { get; init; } = BasePath;
/// <summary>
/// The Name of the library
/// </summary>
[Required]
[Description("The Name of the library")]
public string LibraryName { get; init; } = LibraryName;
}

View File

@@ -0,0 +1,46 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.Requests;
public record CreateNotificationConnectorRecord(string Name, string Url, string HttpMethod, string Body, Dictionary<string, string> Headers)
{
/// <summary>
/// The Name of the Notification Connector
/// </summary>
[Required]
[Description("The Name of the Notification Connector")]
public string Name { get; init; } = Name;
/// <summary>
/// The Url of the Instance
/// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
[Required]
[Url]
[Description("The Url of the Instance")]
public string Url { get; internal set; } = Url;
/// <summary>
/// The HTTP Request Method to use for notifications
/// </summary>
[Required]
[Description("The HTTP Request Method to use for notifications")]
public string HttpMethod { get; internal set; } = HttpMethod;
/// <summary>
/// The Request Body to use to send notifications
/// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
[Required]
[Description("The Request Body to use to send notifications")]
public string Body { get; internal set; } = Body;
/// <summary>
/// The Request Headers to use to send notifications
/// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
[Required]
[Description("The Request Headers to use to send notifications")]
public Dictionary<string, string> Headers { get; internal set; } = Headers;
}

View File

@@ -0,0 +1,51 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.Requests;
public record CreateNtfyConnectorRecord(string Name, string Url, string Username, string Password, string Topic, int Priority)
{
/// <summary>
/// The Name of the Notification Connector
/// </summary>
[Required]
[Description("The Name of the Notification Connector")]
public string Name { get; init; } = Name;
/// <summary>
/// The Url of the Instance
/// </summary>
/// <remarks>Formatting placeholders: "%title" and "%text" will be replaced when notifications are sent</remarks>
[Required]
[Url]
[Description("The Url of the Instance")]
public string Url { get; internal set; } = Url;
/// <summary>
/// The Priority of Notifications
/// </summary>
[Required]
[Description("The Priority of Notifications")]
public int Priority { get; init; } = Priority;
/// <summary>
/// The Username used for authentication
/// </summary>
[Required]
[Description("The Username used for authentication")]
public string Username { get; init; } = Username;
/// <summary>
/// The Password used for authentication
/// </summary>
[Required]
[Description("The Password used for authentication")]
public string Password { get; init; } = Password;
/// <summary>
/// The Topic of Notifications
/// </summary>
[Required]
[Description("The Topic of Notifications")]
public string Topic { get; init; } = Topic;
}

View File

@@ -0,0 +1,28 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace API.Controllers.Requests;
public record CreatePushoverConnectorRecord(string Name, string AppToken, string Username)
{
/// <summary>
/// The Name of the Notification Connector
/// </summary>
[Required]
[Description("The Name of the Notification Connector")]
public string Name { get; init; } = Name;
/// <summary>
/// The Apptoken used for authentication
/// </summary>
[Required]
[Description("The Apptoken used for authentication")]
public string AppToken { get; init; } = AppToken;
/// <summary>
/// The Username used for authentication
/// </summary>
[Required]
[Description("The Username used for authentication")]
public string Username { get; init; } = Username;
}

View File

@@ -0,0 +1,79 @@
using API.Controllers.DTOs;
using API.Schema.MangaContext;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
using Manga = API.Schema.MangaContext.Manga;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class SearchController(MangaContext context) : Controller
{
/// <summary>
/// Initiate a search for a <see cref="Schema.MangaContext.Manga"/> on <see cref="MangaConnector"/> with searchTerm
/// </summary>
/// <param name="MangaConnectorName"><see cref="MangaConnector"/>.Name</param>
/// <param name="Query">searchTerm</param>
/// <response code="200"><see cref="MinimalManga"/> exert of <see cref="Schema.MangaContext.Manga"/></response>
/// <response code="404"><see cref="MangaConnector"/> with Name not found</response>
/// <response code="412"><see cref="MangaConnector"/> with Name is disabled</response>
[HttpGet("{MangaConnectorName}/{Query}")]
[ProducesResponseType<List<MinimalManga>>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType(Status406NotAcceptable)]
public Results<Ok<List<MinimalManga>>, NotFound<string>, StatusCodeHttpResult> SearchManga (string MangaConnectorName, string Query)
{
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals(MangaConnectorName, StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
return TypedResults.NotFound(nameof(MangaConnectorName));
if (connector.Enabled is false)
return TypedResults.StatusCode(Status412PreconditionFailed);
(Manga manga, MangaConnectorId<Manga> id)[] mangas = connector.SearchManga(Query);
IEnumerable<(Manga manga, MangaConnectorId<Manga> id)> addedManga = mangas.Where(kv => context.AddMangaToContext(kv, HttpContext.RequestAborted).GetAwaiter().GetResult());
IEnumerable<MinimalManga> result = addedManga.Select(manga => manga.manga).Select(m =>
{
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id =>
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids);
});
return TypedResults.Ok(result.ToList());
}
/// <summary>
/// Returns <see cref="Schema.MangaContext.Manga"/> from the <see cref="MangaConnector"/> associated with <paramref name="url"/>
/// </summary>
/// <param name="url"></param>
/// <response code="200"><see cref="MinimalManga"/> exert of <see cref="Schema.MangaContext.Manga"/>.</response>
/// <response code="404"><see cref="Manga"/> not found</response>
/// <response code="500">Error during Database Operation</response>
[HttpPost("Url")]
[ProducesResponseType<MinimalManga>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType<string>(Status500InternalServerError, "text/plain")]
public async Task<Results<Ok<MinimalManga>, NotFound<string>, InternalServerError<string>>> GetMangaFromUrl ([FromBody]string url)
{
if(Tranga.MangaConnectors.FirstOrDefault(c => c.Name.Equals("Global", StringComparison.InvariantCultureIgnoreCase)) is not { } connector)
return TypedResults.InternalServerError("Could not find Global Connector.");
if(connector.GetMangaFromUrl(url) is not ({ } m, not null) manga)
return TypedResults.NotFound("Could not retrieve Manga");
if(await context.AddMangaToContext(manga, HttpContext.RequestAborted) == false)
return TypedResults.InternalServerError("Could not add Manga to context");
IEnumerable<MangaConnectorId> ids = m.MangaConnectorIds.Select(id =>
new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload));
MinimalManga result = new (m.Key, m.Name, m.Description, m.ReleaseStatus, ids);
return TypedResults.Ok(result);
}
}

View File

@@ -0,0 +1,293 @@
using API.MangaDownloadClients;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{v:apiVersion}/[controller]")]
public class SettingsController() : Controller
{
/// <summary>
/// Get all <see cref="Tranga.Settings"/>
/// </summary>
/// <response code="200"></response>
[HttpGet]
[ProducesResponseType<TrangaSettings>(Status200OK, "application/json")]
public Ok<TrangaSettings> GetSettings()
{
return TypedResults.Ok(Tranga.Settings);
}
/// <summary>
/// Get the current UserAgent used by Tranga
/// </summary>
/// <response code="200"></response>
[HttpGet("UserAgent")]
[ProducesResponseType<string>(Status200OK, "text/plain")]
public Ok<string> GetUserAgent()
{
return TypedResults.Ok(Tranga.Settings.UserAgent);
}
/// <summary>
/// Set a new UserAgent
/// </summary>
/// <response code="200"></response>
[HttpPatch("UserAgent")]
[ProducesResponseType(Status200OK)]
public Ok SetUserAgent([FromBody]string userAgent)
{
//TODO Validate
Tranga.Settings.SetUserAgent(userAgent);
return TypedResults.Ok();
}
/// <summary>
/// Reset the UserAgent to default
/// </summary>
/// <response code="200"></response>
[HttpDelete("UserAgent")]
[ProducesResponseType(Status200OK)]
public Ok ResetUserAgent()
{
Tranga.Settings.SetUserAgent(TrangaSettings.DefaultUserAgent);
return TypedResults.Ok();
}
/// <summary>
/// Get all Request-Limits
/// </summary>
/// <response code="200"></response>
[HttpGet("RequestLimits")]
[ProducesResponseType<Dictionary<RequestType,int>>(Status200OK, "application/json")]
public Ok<Dictionary<RequestType,int>> GetRequestLimits()
{
return TypedResults.Ok(Tranga.Settings.RequestLimits);
}
/// <summary>
/// Update all Request-Limits to new values
/// </summary>
/// <remarks><h1>NOT IMPLEMENTED</h1></remarks>
[HttpPatch("RequestLimits")]
[ProducesResponseType(Status501NotImplemented)]
public StatusCodeHttpResult SetRequestLimits()
{
return TypedResults.StatusCode(Status501NotImplemented);
}
/// <summary>
/// Updates a Request-Limit value
/// </summary>
/// <param name="RequestType">Type of Request</param>
/// <param name="requestLimit">New limit in Requests/Minute</param>
/// <response code="200"></response>
/// <response code="400">Limit needs to be greater than 0</response>
[HttpPatch("RequestLimits/{RequestType}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status400BadRequest)]
public Results<Ok, BadRequest> SetRequestLimit(RequestType RequestType, [FromBody]int requestLimit)
{
if (requestLimit <= 0)
return TypedResults.BadRequest();
Tranga.Settings.SetRequestLimit(RequestType, requestLimit);
return TypedResults.Ok();
}
/// <summary>
/// Reset Request-Limit
/// </summary>
/// <response code="200"></response>
[HttpDelete("RequestLimits/{RequestType}")]
[ProducesResponseType<string>(Status200OK)]
public Ok ResetRequestLimits(RequestType RequestType)
{
Tranga.Settings.SetRequestLimit(RequestType, TrangaSettings.DefaultRequestLimits[RequestType]);
return TypedResults.Ok();
}
/// <summary>
/// Reset Request-Limit
/// </summary>
/// <response code="200"></response>
[HttpDelete("RequestLimits")]
[ProducesResponseType<string>(Status200OK)]
public Ok ResetRequestLimits()
{
Tranga.Settings.ResetRequestLimits();
return TypedResults.Ok();
}
/// <summary>
/// Returns Level of Image-Compression for Images
/// </summary>
/// <response code="200">JPEG ImageCompression-level as Integer</response>
[HttpGet("ImageCompressionLevel")]
[ProducesResponseType<int>(Status200OK, "text/plain")]
public Ok<int> GetImageCompression()
{
return TypedResults.Ok(Tranga.Settings.ImageCompression);
}
/// <summary>
/// Set the Image-Compression-Level for Images
/// </summary>
/// <param name="level">100 to disable, 0-99 for JPEG ImageCompression-Level</param>
/// <response code="200"></response>
/// <response code="400">Level outside permitted range</response>
[HttpPatch("ImageCompressionLevel/{level}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status400BadRequest)]
public Results<Ok, BadRequest> SetImageCompression(int level)
{
if (level < 1 || level > 100)
return TypedResults.BadRequest();
Tranga.Settings.UpdateImageCompression(level);
return TypedResults.Ok();
}
/// <summary>
/// Get state of Black/White-Image setting
/// </summary>
/// <response code="200">True if enabled</response>
[HttpGet("BWImages")]
[ProducesResponseType<bool>(Status200OK, "text/plain")]
public Ok<bool> GetBwImagesToggle()
{
return TypedResults.Ok(Tranga.Settings.BlackWhiteImages);
}
/// <summary>
/// Enable/Disable conversion of Images to Black and White
/// </summary>
/// <param name="enabled">true to enable</param>
/// <response code="200"></response>
[HttpPatch("BWImages/{enabled}")]
[ProducesResponseType(Status200OK)]
public Ok SetBwImagesToggle(bool enabled)
{
Tranga.Settings.SetBlackWhiteImageEnabled(enabled);
return TypedResults.Ok();
}
/// <summary>
/// Gets the Chapter Naming Scheme
/// </summary>
/// <remarks>
/// Placeholders:
/// %M Obj Name
/// %V Volume
/// %C Chapter
/// %T Title
/// %A Author (first in list)
/// %I Chapter Internal ID
/// %i Obj Internal ID
/// %Y Year (Obj)
///
/// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null
/// </remarks>
/// <response code="200"></response>
[HttpGet("ChapterNamingScheme")]
[ProducesResponseType<string>(Status200OK, "text/plain")]
public Ok<string> GetCustomNamingScheme()
{
return TypedResults.Ok(Tranga.Settings.ChapterNamingScheme);
}
/// <summary>
/// Sets the Chapter Naming Scheme
/// </summary>
/// <remarks>
/// Placeholders:
/// %M Obj Name
/// %V Volume
/// %C Chapter
/// %T Title
/// %A Author (first in list)
/// %Y Year (Obj)
///
/// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null
/// </remarks>
/// <response code="200"></response>
[HttpPatch("ChapterNamingScheme")]
[ProducesResponseType(Status200OK)]
public Ok SetCustomNamingScheme([FromBody]string namingScheme)
{
//TODO Move old Chapters
Tranga.Settings.SetChapterNamingScheme(namingScheme);
return TypedResults.Ok();
}
/// <summary>
/// Sets the FlareSolverr-URL
/// </summary>
/// <param name="flareSolverrUrl">URL of FlareSolverr-Instance</param>
/// <response code="200"></response>
[HttpPost("FlareSolverr/Url")]
[ProducesResponseType(Status200OK)]
public Ok SetFlareSolverrUrl([FromBody]string flareSolverrUrl)
{
Tranga.Settings.SetFlareSolverrUrl(flareSolverrUrl);
return TypedResults.Ok();
}
/// <summary>
/// Resets the FlareSolverr-URL (HttpClient does not use FlareSolverr anymore)
/// </summary>
/// <response code="200"></response>
[HttpDelete("FlareSolverr/Url")]
[ProducesResponseType(Status200OK)]
public Ok ClearFlareSolverrUrl()
{
Tranga.Settings.SetFlareSolverrUrl(string.Empty);
return TypedResults.Ok();
}
/// <summary>
/// Test FlareSolverr
/// </summary>
/// <response code="200">FlareSolverr is working!</response>
/// <response code="500">FlareSolverr is not working</response>
[HttpPost("FlareSolverr/Test")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status500InternalServerError)]
public Results<Ok, InternalServerError> TestFlareSolverrReachable()
{
const string knownProtectedUrl = "https://prowlarr.servarr.com/v1/ping";
FlareSolverrDownloadClient client = new();
RequestResult result = client.MakeRequestInternal(knownProtectedUrl);
return (int)result.statusCode >= 200 && (int)result.statusCode < 300 ? TypedResults.Ok() : TypedResults.InternalServerError();
}
/// <summary>
/// Returns the language in which Manga are downloaded
/// </summary>
/// <response code="200"></response>
[HttpGet("DownloadLanguage")]
[ProducesResponseType<string>(Status200OK, "text/plain")]
public Ok<string> GetDownloadLanguage()
{
return TypedResults.Ok(Tranga.Settings.DownloadLanguage);
}
/// <summary>
/// Sets the language in which Manga are downloaded
/// </summary>
/// <response code="200"></response>
[HttpPatch("DownloadLanguage/{Language}")]
[ProducesResponseType(Status200OK)]
public Ok SetDownloadLanguage(string Language)
{
//TODO Validation
Tranga.Settings.SetDownloadLanguage(Language);
return TypedResults.Ok();
}
}

View File

@@ -0,0 +1,135 @@
using API.Controllers.DTOs;
using API.Workers;
using Asp.Versioning;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
public class WorkerController : Controller
{
/// <summary>
/// Returns all <see cref="BaseWorker"/>
/// </summary>
/// <response code="200"><see cref="Worker"/></response>
[HttpGet]
[ProducesResponseType<List<Worker>>(Status200OK, "application/json")]
public Ok<List<Worker>> GetWorkers()
{
IEnumerable<Worker> result = Tranga.GetRunningWorkers().Select(w =>
new Worker(w.Key, w.AllDependencies.Select(d => d.Key), w.MissingDependencies.Select(d => d.Key), w.AllDependenciesFulfilled, w.State));
return TypedResults.Ok(result.ToList());
}
/// <summary>
/// Returns all <see cref="BaseWorker"/>.Keys
/// </summary>
/// <response code="200"></response>
[HttpGet("Keys")]
[ProducesResponseType<string[]>(Status200OK, "application/json")]
public Ok<List<string>> GetWorkerIds()
{
return TypedResults.Ok(Tranga.GetRunningWorkers().Select(w => w.Key).ToList());
}
/// <summary>
/// Get all <see cref="BaseWorker"/> in requested <see cref="WorkerExecutionState"/>
/// </summary>
/// <param name="State">Requested <see cref="WorkerExecutionState"/></param>
/// <response code="200"></response>
[HttpGet("State/{State}")]
[ProducesResponseType<List<Worker>>(Status200OK, "application/json")]
public Ok<List<Worker>> GetWorkersInState(WorkerExecutionState State)
{
IEnumerable<Worker> result = Tranga.GetRunningWorkers().Where(worker => worker.State == State).Select(w =>
new Worker(w.Key, w.AllDependencies.Select(d => d.Key), w.MissingDependencies.Select(d => d.Key), w.AllDependenciesFulfilled, w.State));
return TypedResults.Ok(result.ToList());
}
/// <summary>
/// Return <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary>
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
[HttpGet("{WorkerId}")]
[ProducesResponseType<Worker>(Status200OK, "application/json")]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public Results<Ok<Worker>, NotFound<string>> GetWorker(string WorkerId)
{
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } w)
return TypedResults.NotFound(nameof(WorkerId));
Worker result = new (w.Key, w.AllDependencies.Select(d => d.Key), w.MissingDependencies.Select(d => d.Key), w.AllDependenciesFulfilled, w.State);
return TypedResults.Ok(result);
}
/// <summary>
/// Delete <see cref="BaseWorker"/> with <paramref name="WorkerId"/> and all child-<see cref="BaseWorker"/>s
/// </summary>
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
[HttpDelete("{WorkerId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
public Results<Ok, NotFound<string>> DeleteWorker(string WorkerId)
{
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return TypedResults.NotFound(nameof(WorkerId));
Tranga.StopWorker(worker);
return TypedResults.Ok();
}
/// <summary>
/// Starts <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary>
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
/// <response code="412"><see cref="BaseWorker"/> was already running</response>
[HttpPost("{WorkerId}/Start")]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType(Status412PreconditionFailed)]
public Results<Ok, NotFound<string>, StatusCodeHttpResult> StartWorker(string WorkerId)
{
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return TypedResults.NotFound(nameof(WorkerId));
if (worker.State >= WorkerExecutionState.Waiting)
return TypedResults.StatusCode(Status412PreconditionFailed);
Tranga.StartWorker(worker);
return TypedResults.Ok();
}
/// <summary>
/// Stops <see cref="BaseWorker"/> with <paramref name="WorkerId"/>
/// </summary>
/// <param name="WorkerId"><see cref="BaseWorker"/>.Key</param>
/// <response code="200"></response>
/// <response code="404"><see cref="BaseWorker"/> with <paramref name="WorkerId"/> could not be found</response>
/// <response code="412"><see cref="BaseWorker"/> was already not running</response>
[HttpPost("{WorkerId}/Stop")]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType<string>(Status404NotFound, "text/plain")]
[ProducesResponseType(Status412PreconditionFailed)]
public Results<Ok, NotFound<string>, StatusCodeHttpResult> StopWorker(string WorkerId)
{
if(Tranga.GetRunningWorkers().FirstOrDefault(w => w.Key == WorkerId) is not { } worker)
return TypedResults.NotFound(nameof(WorkerId));
if(worker.State is < WorkerExecutionState.Running or >= WorkerExecutionState.Completed)
return TypedResults.StatusCode(Status412PreconditionFailed);
Tranga.StopWorker(worker);
return TypedResults.Ok();
}
}

View File

@@ -0,0 +1,35 @@
namespace API;
public interface IHttpRequestTimeFeature
{
DateTime RequestTime { get; }
}
public class HttpRequestTimeFeature : IHttpRequestTimeFeature
{
public DateTime RequestTime { get; }
public HttpRequestTimeFeature()
{
RequestTime = DateTime.Now;
}
}
public class RequestTimeMiddleware
{
private readonly RequestDelegate _next;
public RequestTimeMiddleware(RequestDelegate next)
{
_next = next;
}
public Task InvokeAsync(HttpContext context)
{
var httpRequestTimeFeature = new HttpRequestTimeFeature();
context.Features.Set<IHttpRequestTimeFeature>(httpRequestTimeFeature);
// Call the next delegate/middleware in the pipeline
return this._next(context);
}
}

23
API/Log4Net.config.xml Normal file
View File

@@ -0,0 +1,23 @@
<log4net>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
</layout>
</appender>
<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="/usr/share/tranga-api/log/Tranga.log" />
<appendToFile value="true" />
<rollingStyle value="Composite" />
<datePattern value="yyyyMMdd" />
<maxSizeRollBackups value="4" />
<maximumFileSize value="256MB" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
</layout>
</appender>
<root>
<level value="DEBUG" />
<appender-ref ref="ConsoleAppender" />
<appender-ref ref="RollingLogFileAppender" />
</root>
</log4net>

View File

@@ -0,0 +1,262 @@
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using API.Schema.MangaContext;
using Newtonsoft.Json.Linq;
namespace API.MangaConnectors;
public class ComickIo : MangaConnector
{
//https://api.comick.io/docs/
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
public ComickIo() : base("ComickIo",
["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"],
["comick.io"],
"https://comick.io/static/icons/unicorn-64.png")
{
this.downloadClient = new HttpDownloadClient();
}
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
{
Log.Info($"Searching Obj: {mangaSearchName}");
List<string> slugs = new();
int page = 1;
while(page < 50)
{
string requestUrl = $"https://api.comick.fun/v1.0/search/?type=comic&t=false&limit=100&showall=true&" +
$"page={page}&q={mangaSearchName}";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return [];
}
using StreamReader sr = new (result.result);
JArray data = JArray.Parse(sr.ReadToEnd());
if (data.Count < 1)
break;
slugs.AddRange(data.Select(token => token.Value<string>("slug")!));
page++;
}
Log.Debug($"Search {mangaSearchName} yielded {slugs.Count} slugs. Requesting mangas now...");
List<(Manga, MangaConnectorId<Manga>)> mangas = new ();
foreach (string slug in slugs)
{
if(GetMangaFromId(slug) is { } entry)
mangas.Add(entry);
}
Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
return mangas.ToArray();
}
private readonly Regex _getSlugFromTitleRex = new(@"https?:\/\/comick\.io\/comic\/(.+)(?:\/.*)*");
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
{
Match m = _getSlugFromTitleRex.Match(url);
return m.Groups[1].Success ? GetMangaFromId(m.Groups[1].Value) : null;
}
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite)
{
string requestUrl = $"https://api.comick.fun/comic/{mangaIdOnSite}";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return null;
}
using StreamReader sr = new (result.result);
JToken data = JToken.Parse(sr.ReadToEnd());
return ParseMangaFromJToken(data);
}
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> mangaConnectorId,
string? language = null)
{
Log.Info($"Getting Chapters: {mangaConnectorId.IdOnConnectorSite}");
List<(Chapter, MangaConnectorId<Chapter>)> chapters = new();
int page = 1;
while(page < 50)
{
string requestUrl = $"https://api.comick.fun/comic/{mangaConnectorId.IdOnConnectorSite}/chapters?limit=100&page={page}&lang={language}";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return [];
}
using StreamReader sr = new (result.result);
JToken data = JToken.Parse(sr.ReadToEnd());
JArray? chaptersArray = data["chapters"] as JArray;
if (chaptersArray is null || chaptersArray.Count < 1)
break;
chapters.AddRange(ParseChapters(mangaConnectorId, chaptersArray));
page++;
}
return chapters.ToArray();
}
private readonly Regex _hidFromUrl = new(@"https?:\/\/comick\.io\/comic\/.+\/([^-]+).*");
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
{
Log.Info($"Getting Chapter Image-Urls: {chapterId.Obj}");
if (chapterId.WebsiteUrl is null || !UrlMatchesConnector(chapterId.WebsiteUrl))
{
Log.Debug($"Url is not for Connector. {chapterId.WebsiteUrl}");
return [];
}
Match m = _hidFromUrl.Match(chapterId.WebsiteUrl);
if (!m.Groups[1].Success)
{
Log.Debug($"Could not parse hid from url. {chapterId.WebsiteUrl}");
return [];
}
string hid = m.Groups[1].Value;
string requestUrl = $"https://api.comick.fun/chapter/{hid}/get_images";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaInfo);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return [];
}
using StreamReader sr = new (result.result);
JArray data = JArray.Parse(sr.ReadToEnd());
return data.Select(token =>
{
string url = $"https://meo.comick.pictures/{token.Value<string>("b2key")}";
return url;
}).ToArray();
}
private (Manga manga, MangaConnectorId<Manga> id) ParseMangaFromJToken(JToken json)
{
string? hid = json["comic"]?.Value<string>("hid");
string? slug = json["comic"]?.Value<string>("slug");
string? name = json["comic"]?.Value<string>("title");
string? description = json["comic"]?.Value<string>("desc");
string? originalLanguage = json["comic"]?.Value<string>("country");
string url = $"https://comick.io/comic/{slug}";
string? coverName = json["comic"]?["md_covers"]?.First?.Value<string>("b2key");
string coverUrl = $"https://meo.comick.pictures/{coverName}";
int? releaseStatusStr = json["comic"]?.Value<int>("status");
MangaReleaseStatus status = releaseStatusStr switch
{
1 => MangaReleaseStatus.Continuing,
2 => MangaReleaseStatus.Completed,
3 => MangaReleaseStatus.Cancelled,
4 => MangaReleaseStatus.OnHiatus,
_ => MangaReleaseStatus.Unreleased
};
uint? year = json["comic"]?.Value<uint?>("year");
JArray? altTitlesArray = json["comic"]?["md_titles"] as JArray;
//Cant let language be null, so fill with whatever.
byte whatever = 0;
List<AltTitle> altTitles = altTitlesArray?
.Select(token => new AltTitle(token.Value<string>("lang")??whatever++.ToString(), token.Value<string>("title")!))
.ToList()??[];
JArray? authorsArray = json["authors"] as JArray;
JArray? artistsArray = json["artists"] as JArray;
List<Author> authors = authorsArray?.Concat(artistsArray!)
.Select(token => new Author(token.Value<string>("name")!))
.DistinctBy(a => a.Key)
.ToList()??[];
JArray? genreArray = json["comic"]?["md_comic_md_genres"] as JArray;
List<MangaTag> tags = genreArray?
.Select(token => new MangaTag(token["md_genres"]?.Value<string>("name")!))
.ToList()??[];
JArray? linksArray = json["comic"]?["links"] as JArray;
List<Link> links = linksArray?
.ToObject<Dictionary<string,string>>()?
.Select(kv =>
{
string fullUrl = kv.Key switch
{
"al" => $"https://anilist.co/manga/{kv.Value}",
"ap" => $"https://www.anime-planet.com/manga/{kv.Value}",
"bw" => $"https://bookwalker.jp/{kv.Value}",
"mu" => $"https://www.mangaupdates.com/series.html?id={kv.Value}",
"nu" => $"https://www.novelupdates.com/series/{kv.Value}",
"mal" => $"https://myanimelist.net/manga/{kv.Value}",
_ => kv.Value
};
string key = kv.Key switch
{
"al" => "AniList",
"ap" => "Anime Planet",
"bw" => "BookWalker",
"mu" => "Obj Updates",
"nu" => "Novel Updates",
"kt" => "Kitsu.io",
"amz" => "Amazon",
"ebj" => "eBookJapan",
"mal" => "MyAnimeList",
"cdj" => "CDJapan",
_ => kv.Key
};
return new Link(key, fullUrl);
}).ToList()??[];
if(hid is null)
throw new Exception("hid is null");
if(slug is null)
throw new Exception("slug is null");
if(name is null)
throw new Exception("name is null");
Manga manga = new (name, description??"", coverUrl, status, authors, tags, links, altTitles,
year: year, originalLanguage: originalLanguage);
MangaConnectorId<Manga> mcId = new (manga, this, hid, url);
manga.MangaConnectorIds.Add(mcId);
return (manga, mcId);
}
private List<(Chapter, MangaConnectorId<Chapter>)> ParseChapters(MangaConnectorId<Manga> mcIdManga, JArray chaptersArray)
{
List<(Chapter, MangaConnectorId<Chapter>)> chapters = new ();
foreach (JToken chapter in chaptersArray)
{
string? chapterNum = chapter.Value<string>("chap");
string? volumeNumStr = chapter.Value<string>("vol");
int? volumeNum = volumeNumStr is null ? null : int.Parse(volumeNumStr);
string? title = chapter.Value<string>("title");
string? hid = chapter.Value<string>("hid");
string url = $"https://comick.io/comic/{mcIdManga.IdOnConnectorSite}/{hid}";
if(chapterNum is null || hid is null)
continue;
Chapter ch = new (mcIdManga.Obj, chapterNum, volumeNum, title);
MangaConnectorId<Chapter> mcId = new(ch, this, hid, url);
ch.MangaConnectorIds.Add(mcId);
chapters.Add((ch, mcId));
}
return chapters;
}
}

View File

@@ -0,0 +1,58 @@
using API.Schema.MangaContext;
namespace API.MangaConnectors;
public class Global : MangaConnector
{
public Global() : base("Global", ["all"], [""], "https://avatars.githubusercontent.com/u/13404778")
{
}
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
{
//Get all enabled Connectors
MangaConnector[] enabledConnectors = Tranga.MangaConnectors.Where(c => c.Enabled && c.Name != "Global").ToArray();
//Create Task for each MangaConnector to search simultaneously
Task<(Manga, MangaConnectorId<Manga>)[]>[] tasks =
enabledConnectors.Select(c => new Task<(Manga, MangaConnectorId<Manga>)[]>(() => c.SearchManga(mangaSearchName))).ToArray();
foreach (var task in tasks)
task.Start();
//Wait for all tasks to finish
do
{
Thread.Sleep(50);
}while(tasks.Any(t => t.Status < TaskStatus.RanToCompletion));
//Concatenate all results into one
(Manga, MangaConnectorId<Manga>)[] ret = tasks.Select(t => t.IsCompletedSuccessfully ? t.Result : []).ToArray().SelectMany(i => i).ToArray();
return ret;
}
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
{
MangaConnector? mc = Tranga.MangaConnectors.FirstOrDefault(c => c.UrlMatchesConnector(url));
return mc?.GetMangaFromUrl(url) ?? null;
}
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite)
{
return null;
}
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> manga,
string? language = null)
{
if (!Tranga.TryGetMangaConnector(manga.MangaConnectorName, out MangaConnector? mangaConnector))
return [];
return mangaConnector.GetChapters(manga, language);
}
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
{
if (!Tranga.TryGetMangaConnector(chapterId.MangaConnectorName, out MangaConnector? mangaConnector))
return [];
return mangaConnector.GetChapterImageUrls(chapterId);
}
}

View File

@@ -0,0 +1,91 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using API.Schema.MangaContext;
using log4net;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
namespace API.MangaConnectors;
[PrimaryKey("Name")]
public abstract class MangaConnector(string name, string[] supportedLanguages, string[] baseUris, string iconUrl)
{
[NotMapped] internal DownloadClient downloadClient { get; init; } = null!;
[NotMapped] protected ILog Log { get; init; } = LogManager.GetLogger(name);
[StringLength(32)] public string Name { get; init; } = name;
[StringLength(8)] public string[] SupportedLanguages { get; init; } = supportedLanguages;
[StringLength(2048)] public string IconUrl { get; init; } = iconUrl;
[StringLength(256)] public string[] BaseUris { get; init; } = baseUris;
public bool Enabled { get; internal set; } = true;
public abstract (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName);
public abstract (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url);
public abstract (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite);
public abstract (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> mangaId,
string? language = null);
internal abstract string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId);
public bool UrlMatchesConnector(string url) => BaseUris.Any(baseUri => Regex.IsMatch(url, "https?://" + baseUri + "/.*"));
internal string? SaveCoverImageToCache(MangaConnectorId<Manga> mangaId, int retries = 3)
{
if(retries < 0)
return null;
Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
//https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
Match match = urlRex.Match(mangaId.Obj.CoverUrl);
string filename = $"{match.Groups[1].Value}-{mangaId.ObjId}.{mangaId.MangaConnectorName}.{match.Groups[3].Value}";
string saveImagePath = Path.Join(TrangaSettings.coverImageCacheOriginal, filename);
if (File.Exists(saveImagePath))
return filename;
RequestResult coverResult = downloadClient.MakeRequest(mangaId.Obj.CoverUrl, RequestType.MangaCover, $"https://{match.Groups[1].Value}");
if ((int)coverResult.statusCode < 200 || (int)coverResult.statusCode >= 300)
return SaveCoverImageToCache(mangaId, --retries);
try
{
using MemoryStream ms = new();
coverResult.result.CopyTo(ms);
byte[] imageBytes = ms.ToArray();
Directory.CreateDirectory(TrangaSettings.coverImageCacheOriginal);
File.WriteAllBytes(saveImagePath, imageBytes);
using Image image = Image.Load(imageBytes);
Directory.CreateDirectory(TrangaSettings.coverImageCacheLarge);
using Image large = image.Clone(x => x.Resize(new ResizeOptions
{ Size = Constants.ImageLgSize, Mode = ResizeMode.Max }));
large.SaveAsJpeg(Path.Join(TrangaSettings.coverImageCacheLarge, filename), new (){ Quality = 40 });
Directory.CreateDirectory(TrangaSettings.coverImageCacheMedium);
using Image medium = image.Clone(x => x.Resize(new ResizeOptions
{ Size = Constants.ImageMdSize, Mode = ResizeMode.Max }));
medium.SaveAsJpeg(Path.Join(TrangaSettings.coverImageCacheMedium, filename), new (){ Quality = 40 });
Directory.CreateDirectory(TrangaSettings.coverImageCacheSmall);
using Image small = image.Clone(x => x.Resize(new ResizeOptions
{ Size = Constants.ImageSmSize, Mode = ResizeMode.Max }));
small.SaveAsJpeg(Path.Join(TrangaSettings.coverImageCacheSmall, filename), new (){ Quality = 40 });
}
catch (Exception e)
{
Log.Error(e);
}
return filename;
}
}

View File

@@ -0,0 +1,343 @@
using System.Text.RegularExpressions;
using API.MangaDownloadClients;
using API.Schema.MangaContext;
using Newtonsoft.Json.Linq;
namespace API.MangaConnectors;
public class MangaDex : MangaConnector
{
//https://api.mangadex.org/docs/3-enumerations/#language-codes--localization
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
//https://gist.github.com/Josantonius/b455e315bc7f790d14b136d61d9ae469
public MangaDex() : base("MangaDex",
["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"],
["mangadex.org"],
"https://mangadex.org/favicon.ico")
{
this.downloadClient = new HttpDownloadClient();
}
private const int Limit = 100;
public override (Manga, MangaConnectorId<Manga>)[] SearchManga(string mangaSearchName)
{
Log.Info($"Searching Obj: {mangaSearchName}");
List<(Manga, MangaConnectorId<Manga>)> mangas = new ();
int offset = 0;
int total = int.MaxValue;
while(offset < total)
{
string requestUrl =
$"https://api.mangadex.org/manga?limit={Limit}&offset={offset}&title={mangaSearchName}" +
$"&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica" +
$"&includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
offset += Limit;
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return [];
}
using StreamReader sr = new (result.result);
JObject jObject = JObject.Parse(sr.ReadToEnd());
if (jObject.Value<string>("result") != "ok")
{
JArray? errors = jObject["errors"] as JArray;
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
return [];
}
total = jObject.Value<int>("total");
JArray? data = jObject.Value<JArray>("data");
if (data is null)
{
Log.Error("Data was null");
return [];
}
mangas.AddRange(data.Select(ParseMangaFromJToken));
}
Log.Info($"Search {mangaSearchName} yielded {mangas.Count} results.");
return mangas.ToArray();
}
private static readonly Regex GetMangaIdFromUrl = new(@"https?:\/\/mangadex\.org\/title\/([a-z0-9-]+)\/?.*");
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromUrl(string url)
{
Log.Info($"Getting Obj: {url}");
if (!UrlMatchesConnector(url))
{
Log.Debug($"Url is not for Connector. {url}");
return null;
}
Match match = GetMangaIdFromUrl.Match(url);
if (!match.Success || !match.Groups[1].Success)
{
Log.Debug($"Url is not for Connector (Could not retrieve id). {url}");
return null;
}
string id = match.Groups[1].Value;
return GetMangaFromId(id);
}
public override (Manga, MangaConnectorId<Manga>)? GetMangaFromId(string mangaIdOnSite)
{
Log.Info($"Getting Obj: {mangaIdOnSite}");
string requestUrl =
$"https://api.mangadex.org/manga/{mangaIdOnSite}" +
$"?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag'";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return null;
}
using StreamReader sr = new (result.result);
JObject jObject = JObject.Parse(sr.ReadToEnd());
if (jObject.Value<string>("result") != "ok")
{
JArray? errors = jObject["errors"] as JArray;
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
return null;
}
JObject? data = jObject["data"] as JObject;
if (data is null)
{
Log.Error("Data was null");
return null;
}
return ParseMangaFromJToken(data);
}
public override (Chapter, MangaConnectorId<Chapter>)[] GetChapters(MangaConnectorId<Manga> manga, string? language = null)
{
Log.Info($"Getting Chapters: {manga.IdOnConnectorSite}");
List<(Chapter, MangaConnectorId<Chapter>)> chapters = new ();
int offset = 0;
int total = int.MaxValue;
while(offset < total)
{
string requestUrl =
$"https://api.mangadex.org/manga/{manga.IdOnConnectorSite}/feed?limit={Limit}&offset={offset}&" +
$"translatedLanguage%5B%5D={language}&" +
$"contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&includeFutureUpdates=0&includes%5B%5D=";
offset += Limit;
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.MangaDexFeed);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return [];
}
using StreamReader sr = new (result.result);
JObject jObject = JObject.Parse(sr.ReadToEnd());
if (jObject.Value<string>("result") != "ok")
{
JArray? errors = jObject["errors"] as JArray;
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
return [];
}
total = jObject.Value<int>("total");
JArray? data = jObject.Value<JArray>("data");
if (data is null)
{
Log.Error("Data was null");
return [];
}
chapters.AddRange(data.Select(d => ParseChapterFromJToken(manga, d)));
}
Log.Info($"Request for chapters for {manga.Obj.Name} yielded {chapters.Count} results.");
return chapters.ToArray();
}
private static readonly Regex GetChapterIdFromUrl = new(@"https?:\/\/mangadex\.org\/chapter\/([a-z0-9-]+)\/?.*");
internal override string[] GetChapterImageUrls(MangaConnectorId<Chapter> chapterId)
{
Log.Info($"Getting Chapter Image-Urls: {chapterId.Obj}");
if (chapterId.WebsiteUrl is null || !UrlMatchesConnector(chapterId.WebsiteUrl))
{
Log.Debug($"Url is not for Connector. {chapterId.WebsiteUrl}");
return [];
}
Match match = GetChapterIdFromUrl.Match(chapterId.WebsiteUrl);
if (!match.Success || !match.Groups[1].Success)
{
Log.Debug($"Url is not for Connector (Could not retrieve id). {chapterId.WebsiteUrl}");
return [];
}
string id = match.Groups[1].Value;
string requestUrl = $"https://api.mangadex.org/at-home/server/{id}";
RequestResult result = downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300)
{
Log.Error("Request failed");
return [];
}
using StreamReader sr = new (result.result);
JObject jObject = JObject.Parse(sr.ReadToEnd());
if (jObject.Value<string>("result") != "ok")
{
JArray? errors = jObject["errors"] as JArray;
Log.Error($"Request failed: {string.Join(',', errors?.Select(e => e.Value<string>("title")) ?? [])}");
return [];
}
string? baseUrl = jObject.Value<string>("baseUrl");
JToken? chapterToken = jObject["chapter"];
string? hash = chapterToken?.Value<string>("hash");
JArray? data = chapterToken?["data"] as JArray;
if (baseUrl is null || hash is null || data is null)
{
Log.Error("Data was null");
return [];
}
IEnumerable<string> urls = data.Select(t => $"{baseUrl}/data/{hash}/{t.Value<string>()}");
return urls.ToArray();
}
private (Manga manga, MangaConnectorId<Manga> id) ParseMangaFromJToken(JToken jToken)
{
string? id = jToken.Value<string>("id");
if(id is null)
throw new Exception("jToken was not in expected format");
JObject? attributes = jToken["attributes"] as JObject;
if(attributes is null)
throw new Exception("jToken was not in expected format");
string? name = attributes["title"]?.Value<string>("en") ?? attributes["title"]?.First?.First?.Value<string>();
string description = attributes["description"]?.Value<string>("en")??attributes["description"]?.First?.First?.Value<string>()??"";
string? status = attributes["status"]?.Value<string>();
uint? year = attributes["year"]?.Value<uint?>();
string? originalLanguage = attributes["originalLanguage"]?.Value<string>();
JArray? altTitlesJArray = attributes.TryGetValue("altTitles", out JToken? altTitlesArray) ? altTitlesArray as JArray : null;
JArray? tagsJArray = attributes.TryGetValue("tags", out JToken? tagsArray) ? tagsArray as JArray : null;
JArray? relationships = jToken["relationships"] as JArray;
if (name is null || status is null || relationships is null)
throw new Exception("jToken was not in expected format");
string? coverFileName = relationships.FirstOrDefault(r => r["type"]?.Value<string>() == "cover_art")?["attributes"]?.Value<string>("fileName");
if(coverFileName is null)
throw new Exception("jToken was not in expected format");
List<Link> links = attributes["links"]?
.ToObject<Dictionary<string,string>>()?
.Select(kv =>
{
//https://api.mangadex.org/docs/3-enumerations/#manga-links-data
string url = kv.Key switch
{
"al" => $"https://anilist.co/manga/{kv.Value}",
"ap" => $"https://www.anime-planet.com/manga/{kv.Value}",
"bw" => $"https://bookwalker.jp/{kv.Value}",
"mu" => $"https://www.mangaupdates.com/series.html?id={kv.Value}",
"nu" => $"https://www.novelupdates.com/series/{kv.Value}",
"mal" => $"https://myanimelist.net/manga/{kv.Value}",
_ => kv.Value
};
string key = kv.Key switch
{
"al" => "AniList",
"ap" => "Anime Planet",
"bw" => "BookWalker",
"mu" => "Obj Updates",
"nu" => "Novel Updates",
"kt" => "Kitsu.io",
"amz" => "Amazon",
"ebj" => "eBookJapan",
"mal" => "MyAnimeList",
"cdj" => "CDJapan",
_ => kv.Key
};
return new Link(key, url);
}).ToList()??[];
List<AltTitle> altTitles = altTitlesJArray?
.Select(t =>
{
JObject? j = t as JObject;
JProperty? p = j?.Properties().First();
if (p is null)
return null;
return new AltTitle(p.Name, p.Value.ToString());
}).Where(x => x is not null).Cast<AltTitle>().ToList()??[];
List<MangaTag> tags = tagsJArray?
.Where(t => t.Value<string>("type") == "tag")
.Select(t => t["attributes"]?["name"]?.Value<string>("en")??t["attributes"]?["name"]?.First?.First?.Value<string>())
.Select(str => str is not null ? new MangaTag(str) : null)
.Where(x => x is not null).Cast<MangaTag>().ToList()??[];
List<Author> authors = relationships
.Where(r => r["type"]?.Value<string>() == "author")
.Select(t => t["attributes"]?.Value<string>("name"))
.Select(str => str is not null ? new Author(str) : null)
.Where(x => x is not null).Cast<Author>().ToList();
MangaReleaseStatus releaseStatus = status switch
{
"completed" => MangaReleaseStatus.Completed,
"ongoing" => MangaReleaseStatus.Continuing,
"cancelled" => MangaReleaseStatus.Cancelled,
"hiatus" => MangaReleaseStatus.OnHiatus,
_ => MangaReleaseStatus.Unreleased
};
string websiteUrl = $"https://mangadex.org/title/{id}";
string coverUrl = $"https://uploads.mangadex.org/covers/{id}/{coverFileName}";
Manga manga = new (name, description, coverUrl, releaseStatus, authors, tags, links,altTitles,
null, 0f, year, originalLanguage);
MangaConnectorId<Manga> mcId = new (manga, this, id, websiteUrl);
manga.MangaConnectorIds.Add(mcId);
return (manga, mcId);
}
private (Chapter chapter, MangaConnectorId<Chapter> id) ParseChapterFromJToken(MangaConnectorId<Manga> mcIdManga, JToken jToken)
{
string? id = jToken.Value<string>("id");
JToken? attributes = jToken["attributes"];
string? chapterStr = attributes?.Value<string>("chapter") ?? "0";
string? volumeStr = attributes?.Value<string>("volume");
int? volumeNumber = null;
string? title = attributes?.Value<string>("title");
if(id is null || chapterStr is null)
throw new Exception("jToken was not in expected format");
if(volumeStr is not null)
volumeNumber = int.Parse(volumeStr);
string websiteUrl = $"https://mangadex.org/chapter/{id}";
Chapter chapter = new (mcIdManga.Obj, chapterStr, volumeNumber, title);
MangaConnectorId<Chapter> mcId = new(chapter, this, id, websiteUrl);
chapter.MangaConnectorIds.Add(mcId);
return (chapter, mcId);
}
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Concurrent;
using System.Net;
using log4net;
namespace API.MangaDownloadClients;
public abstract class DownloadClient
{
private static readonly ConcurrentDictionary<RequestType, DateTime> LastExecutedRateLimit = new();
protected ILog Log { get; init; }
protected DownloadClient()
{
this.Log = LogManager.GetLogger(GetType());
}
// TODO Requests still go too fast across threads!
public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
{
Log.Debug($"Requesting {requestType} {url}");
// If we don't have a RequestLimit set for a Type, use the default one
if (!Tranga.Settings.RequestLimits.ContainsKey(requestType))
requestType = RequestType.Default;
int rateLimit = Tranga.Settings.RequestLimits[requestType];
// TODO this probably needs a better check whether the useragent matches...
// If the UserAgent is the default one, do not exceed the default request-limits.
if (Tranga.Settings.UserAgent == TrangaSettings.DefaultUserAgent && rateLimit > TrangaSettings.DefaultRequestLimits[requestType])
rateLimit = TrangaSettings.DefaultRequestLimits[requestType];
// Apply the delay
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
DateTime now = DateTime.Now;
LastExecutedRateLimit.TryAdd(requestType, now.Subtract(timeBetweenRequests));
TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(now.Subtract(LastExecutedRateLimit[requestType]));
Log.Debug($"Request limit {requestType} {rateLimit}/Minute timeBetweenRequests: {timeBetweenRequests:ss'.'fffff} Timeout: {rateLimitTimeout:ss'.'fffff}");
if (rateLimitTimeout > TimeSpan.Zero)
Thread.Sleep(rateLimitTimeout);
// Make the request
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
// Update the time the last request was made
LastExecutedRateLimit[requestType] = DateTime.UtcNow;
Log.Debug($"Result {url}: {result}");
return result;
}
internal abstract RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null);
}

View File

@@ -0,0 +1,178 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Text;
using System.Text.Json;
using HtmlAgilityPack;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace API.MangaDownloadClients;
public class FlareSolverrDownloadClient : DownloadClient
{
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
{
if (clickButton is not null)
Log.Warn("Client can not click button");
if(referrer is not null)
Log.Warn("Client can not set referrer");
if (Tranga.Settings.FlareSolverrUrl == string.Empty)
{
Log.Error("FlareSolverr URL is empty");
return new(HttpStatusCode.InternalServerError, null, Stream.Null);
}
Uri flareSolverrUri = new (Tranga.Settings.FlareSolverrUrl);
if (flareSolverrUri.Segments.Last() != "v1")
flareSolverrUri = new UriBuilder(flareSolverrUri)
{
Path = "v1"
}.Uri;
HttpClient client = new()
{
Timeout = TimeSpan.FromSeconds(10),
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
DefaultRequestHeaders = { { "User-Agent", Tranga.Settings.UserAgent } }
};
JObject requestObj = new()
{
["cmd"] = "request.get",
["url"] = url
};
HttpRequestMessage requestMessage = new(HttpMethod.Post, flareSolverrUri)
{
Content = new StringContent(JsonConvert.SerializeObject(requestObj)),
};
requestMessage.Content.Headers.ContentType = new ("application/json");
Log.Debug($"Requesting {url}");
HttpResponseMessage? response;
try
{
response = client.Send(requestMessage);
}
catch (HttpRequestException e)
{
Log.Error(e);
return new (HttpStatusCode.Unused, null, Stream.Null);
}
if (!response.IsSuccessStatusCode)
{
Log.Debug($"Request returned status code {(int)response.StatusCode} {response.StatusCode}:\n" +
$"=====\n" +
$"Request:\n" +
$"{requestMessage.Method} {requestMessage.RequestUri}\n" +
$"{requestMessage.Version} {requestMessage.VersionPolicy}\n" +
$"Headers:\n\t{string.Join("\n\t", requestMessage.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
$"{requestMessage.Content?.ReadAsStringAsync().Result}" +
$"=====\n" +
$"Response:\n" +
$"{response.Version}\n" +
$"Headers:\n\t{string.Join("\n\t", response.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
$"{response.Content.ReadAsStringAsync().Result}");
return new (response.StatusCode, null, Stream.Null);
}
string responseString = response.Content.ReadAsStringAsync().Result;
JObject responseObj = JObject.Parse(responseString);
if (!IsInCorrectFormat(responseObj, out string? reason))
{
Log.Error($"Wrong format: {reason}");
return new(HttpStatusCode.Unused, null, Stream.Null);
}
string statusResponse = responseObj["status"]!.Value<string>()!;
if (statusResponse != "ok")
{
Log.Debug($"Status is not ok: {statusResponse}");
return new(HttpStatusCode.Unused, null, Stream.Null);
}
JObject solution = (responseObj["solution"] as JObject)!;
if (!Enum.TryParse(solution["status"]!.Value<int>().ToString(), out HttpStatusCode statusCode))
{
Log.Error($"Wrong format: Cant parse status code: {solution["status"]!.Value<int>()}");
return new(HttpStatusCode.Unused, null, Stream.Null);
}
if (statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.MultipleChoices)
{
Log.Debug($"Status is: {statusCode}");
return new(statusCode, null, Stream.Null);
}
if (solution["response"]!.Value<string>() is not { } htmlString)
{
Log.Error("Wrong format: Cant find response in solution");
return new(HttpStatusCode.Unused, null, Stream.Null);
}
if (IsJson(htmlString, out HtmlDocument document, out string? json))
{
MemoryStream ms = new();
ms.Write(Encoding.UTF8.GetBytes(json));
ms.Position = 0;
return new(statusCode, document, ms);
}
else
{
MemoryStream ms = new();
ms.Write(Encoding.UTF8.GetBytes(htmlString));
ms.Position = 0;
return new(statusCode, document, ms);
}
}
private bool IsInCorrectFormat(JObject responseObj, [NotNullWhen(false)]out string? reason)
{
reason = null;
if (!responseObj.ContainsKey("status"))
{
reason = "Cant find status on response";
return false;
}
if (responseObj["solution"] is not JObject solution)
{
reason = "Cant find solution";
return false;
}
if (!solution.ContainsKey("status"))
{
reason = "Wrong format: Cant find status in solution";
return false;
}
if (!solution.ContainsKey("response"))
{
reason = "Wrong format: Cant find response in solution";
return false;
}
return true;
}
private bool IsJson(string htmlString, out HtmlDocument document, [NotNullWhen(true)]out string? jsonString)
{
jsonString = null;
document = new();
document.LoadHtml(htmlString);
HtmlNode pre = document.DocumentNode.SelectSingleNode("//pre");
try
{
using JsonDocument _ = JsonDocument.Parse(pre.InnerText);
jsonString = pre.InnerText;
return true;
}
catch (Exception)
{
return false;
}
}
}

View File

@@ -0,0 +1,88 @@
using System.Net;
using HtmlAgilityPack;
namespace API.MangaDownloadClients;
internal class HttpDownloadClient : DownloadClient
{
private static readonly FlareSolverrDownloadClient FlareSolverrDownloadClient = new();
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
{
if (clickButton is not null)
Log.Warn("Client can not click button");
HttpClient client = new();
client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
client.DefaultRequestHeaders.Add("User-Agent", Tranga.Settings.UserAgent);
HttpResponseMessage? response;
Uri uri = new(url);
HttpRequestMessage requestMessage = new(HttpMethod.Get, uri);
if (referrer is not null)
requestMessage.Headers.Referrer = new (referrer);
Log.Debug($"Requesting {url}");
try
{
response = client.Send(requestMessage);
}
catch (HttpRequestException e)
{
Log.Error(e);
return new (HttpStatusCode.Unused, null, Stream.Null);
}
if (!response.IsSuccessStatusCode)
{
Log.Debug($"Request returned status code {(int)response.StatusCode} {response.StatusCode}");
if (response.Headers.Server.Any(s =>
(s.Product?.Name ?? "").Contains("cloudflare", StringComparison.InvariantCultureIgnoreCase)))
{
Log.Debug("Retrying with FlareSolverr!");
return FlareSolverrDownloadClient.MakeRequestInternal(url, referrer, clickButton);
}
else
{
Log.Debug($"Request returned status code {(int)response.StatusCode} {response.StatusCode}:\n" +
$"=====\n" +
$"Request:\n" +
$"{requestMessage.Method} {requestMessage.RequestUri}\n" +
$"{requestMessage.Version} {requestMessage.VersionPolicy}\n" +
$"Headers:\n\t{string.Join("\n\t", requestMessage.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
$"{requestMessage.Content?.ReadAsStringAsync().Result}" +
$"=====\n" +
$"Response:\n" +
$"{response.Version}\n" +
$"Headers:\n\t{string.Join("\n\t", response.Headers.Select(h => $"{h.Key}: <{string.Join(">, <", h.Value)}"))}>\n" +
$"{response.Content.ReadAsStringAsync().Result}");
}
}
Stream stream;
try
{
stream = response.Content.ReadAsStream();
}
catch (Exception e)
{
Log.Error(e);
return new (HttpStatusCode.Unused, null, Stream.Null);
}
HtmlDocument? document = null;
if (response.Content.Headers.ContentType?.MediaType == "text/html")
{
StreamReader reader = new (stream);
document = new ();
document.LoadHtml(reader.ReadToEnd());
stream.Position = 0;
}
// Request has been redirected to another page. For example, it redirects directly to the results when there is only 1 result
if (response.RequestMessage is not null && response.RequestMessage.RequestUri is not null && response.RequestMessage.RequestUri != uri)
{
return new (response.StatusCode, document, stream, true, response.RequestMessage.RequestUri.AbsoluteUri);
}
return new (response.StatusCode, document, stream);
}
}

View File

@@ -0,0 +1,33 @@
using System.Net;
using HtmlAgilityPack;
namespace API.MangaDownloadClients;
public struct RequestResult
{
public HttpStatusCode statusCode { get; }
public Stream result { get; }
public bool hasBeenRedirected { get; }
public string? redirectedToUrl { get; }
public HtmlDocument? htmlDocument { get; }
public RequestResult(HttpStatusCode statusCode, HtmlDocument? htmlDocument, Stream result)
{
this.statusCode = statusCode;
this.htmlDocument = htmlDocument;
this.result = result;
}
public RequestResult(HttpStatusCode statusCode, HtmlDocument? htmlDocument, Stream result, bool hasBeenRedirected, string redirectedTo)
: this(statusCode, htmlDocument, result)
{
this.hasBeenRedirected = hasBeenRedirected;
redirectedToUrl = redirectedTo;
}
public override string ToString()
{
return
$"{(int)statusCode} {statusCode.ToString()} {(hasBeenRedirected ? "Redirected: " : "")} {redirectedToUrl}";
}
}

View File

@@ -0,0 +1,11 @@
namespace API.MangaDownloadClients;
public enum RequestType : byte
{
Default = 0,
MangaDexFeed = 1,
MangaImage = 2,
MangaCover = 3,
MangaDexImage = 5,
MangaInfo = 6
}

View File

@@ -0,0 +1,70 @@
// <auto-generated />
using API.Schema.LibraryContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Library
{
[DbContext(typeof(LibraryContext))]
[Migration("20250703191925_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("Auth")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("BaseUrl")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("Key");
b.ToTable("LibraryConnectors");
b.HasDiscriminator<byte>("LibraryType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
{
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
{
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)0);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.Library
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "LibraryConnectors",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
LibraryType = table.Column<byte>(type: "smallint", nullable: false),
BaseUrl = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Auth = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_LibraryConnectors", x => x.Key);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "LibraryConnectors");
}
}
}

View File

@@ -0,0 +1,71 @@
// <auto-generated />
using API.Schema.LibraryContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Library
{
[DbContext(typeof(LibraryContext))]
[Migration("20250901194308_KeyLengthChange")]
partial class KeyLengthChange
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Auth")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("BaseUrl")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("Key");
b.ToTable("LibraryConnectors");
b.HasDiscriminator<byte>("LibraryType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
{
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
{
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)0);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.Library
{
/// <inheritdoc />
public partial class KeyLengthChange : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "LibraryConnectors",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "LibraryConnectors",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
}
}
}

View File

@@ -0,0 +1,68 @@
// <auto-generated />
using API.Schema.LibraryContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Library
{
[DbContext(typeof(LibraryContext))]
partial class LibraryContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Auth")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("BaseUrl")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<byte>("LibraryType")
.HasColumnType("smallint");
b.HasKey("Key");
b.ToTable("LibraryConnectors");
b.HasDiscriminator<byte>("LibraryType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Kavita", b =>
{
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)1);
});
modelBuilder.Entity("API.Schema.LibraryContext.LibraryConnectors.Komga", b =>
{
b.HasBaseType("API.Schema.LibraryContext.LibraryConnectors.LibraryConnector");
b.HasDiscriminator().HasValue((byte)0);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,527 @@
// <auto-generated />
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Manga
{
[DbContext(typeof(MangaContext))]
[Migration("20250722203315_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.MangaConnectors.MangaConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.PrimitiveCollection<string[]>("BaseUris")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("IconUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("text[]");
b.HasKey("Name");
b.ToTable("MangaConnector");
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("Key");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("ChapterNumber")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded")
.HasColumnType("boolean");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("Key");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.ToTable("FileLibraries");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DirectoryName")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("Key");
b.HasIndex("LibraryId");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToChapter");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToManga");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
b.Property<string>("Identifier")
.HasColumnType("text");
b.Property<string>("MangaId")
.IsRequired()
.HasColumnType("text");
b.HasKey("MetadataFetcherName", "Identifier");
b.HasIndex("MangaId");
b.ToTable("MetadataEntries");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
{
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("MetadataEntry")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.HasKey("Name");
b.ToTable("MetadataFetcher");
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("text");
b.Property<string>("MangaIds")
.HasColumnType("text");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("text");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.MangaConnectors.Global", b =>
{
b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
{
b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher");
b.HasDiscriminator().HasValue("MyAnimeList");
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("AltTitle");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
{
b1.Property<string>("Key")
.HasColumnType("text");
b1.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("text");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
.WithMany()
.HasForeignKey("MetadataFetcherName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("MetadataFetcher");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.MangaContext.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Navigation("MangaConnectorIds");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("MangaConnectorIds");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,375 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.Manga
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Authors",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
AuthorName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Authors", x => x.Key);
});
migrationBuilder.CreateTable(
name: "FileLibraries",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
BasePath = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
LibraryName = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_FileLibraries", x => x.Key);
});
migrationBuilder.CreateTable(
name: "MangaConnector",
columns: table => new
{
Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
SupportedLanguages = table.Column<string[]>(type: "text[]", maxLength: 8, nullable: false),
IconUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
BaseUris = table.Column<string[]>(type: "text[]", maxLength: 256, nullable: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MangaConnector", x => x.Name);
});
migrationBuilder.CreateTable(
name: "MetadataFetcher",
columns: table => new
{
Name = table.Column<string>(type: "text", nullable: false),
MetadataEntry = table.Column<string>(type: "character varying(21)", maxLength: 21, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MetadataFetcher", x => x.Name);
});
migrationBuilder.CreateTable(
name: "Tags",
columns: table => new
{
Tag = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Tags", x => x.Tag);
});
migrationBuilder.CreateTable(
name: "Mangas",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Description = table.Column<string>(type: "text", nullable: false),
CoverUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
ReleaseStatus = table.Column<byte>(type: "smallint", nullable: false),
LibraryId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
IgnoreChaptersBefore = table.Column<float>(type: "real", nullable: false),
DirectoryName = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
CoverFileNameInCache = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
Year = table.Column<long>(type: "bigint", nullable: true),
OriginalLanguage = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Mangas", x => x.Key);
table.ForeignKey(
name: "FK_Mangas_FileLibraries_LibraryId",
column: x => x.LibraryId,
principalTable: "FileLibraries",
principalColumn: "Key",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "AltTitle",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
Language = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
MangaKey = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AltTitle", x => x.Key);
table.ForeignKey(
name: "FK_AltTitle_Mangas_MangaKey",
column: x => x.MangaKey,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AuthorToManga",
columns: table => new
{
AuthorIds = table.Column<string>(type: "text", nullable: false),
MangaIds = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AuthorToManga", x => new { x.AuthorIds, x.MangaIds });
table.ForeignKey(
name: "FK_AuthorToManga_Authors_AuthorIds",
column: x => x.AuthorIds,
principalTable: "Authors",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AuthorToManga_Mangas_MangaIds",
column: x => x.MangaIds,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Chapters",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
ParentMangaId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
VolumeNumber = table.Column<int>(type: "integer", nullable: true),
ChapterNumber = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Downloaded = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Chapters", x => x.Key);
table.ForeignKey(
name: "FK_Chapters_Mangas_ParentMangaId",
column: x => x.ParentMangaId,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Link",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
LinkProvider = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
LinkUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
MangaKey = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Link", x => x.Key);
table.ForeignKey(
name: "FK_Link_Mangas_MangaKey",
column: x => x.MangaKey,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MangaConnectorToManga",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
ObjId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
UseForDownload = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MangaConnectorToManga", x => x.Key);
table.ForeignKey(
name: "FK_MangaConnectorToManga_Mangas_ObjId",
column: x => x.ObjId,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MangaTagToManga",
columns: table => new
{
MangaTagIds = table.Column<string>(type: "character varying(64)", nullable: false),
MangaIds = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MangaTagToManga", x => new { x.MangaTagIds, x.MangaIds });
table.ForeignKey(
name: "FK_MangaTagToManga_Mangas_MangaIds",
column: x => x.MangaIds,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MangaTagToManga_Tags_MangaTagIds",
column: x => x.MangaTagIds,
principalTable: "Tags",
principalColumn: "Tag",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MetadataEntries",
columns: table => new
{
MetadataFetcherName = table.Column<string>(type: "text", nullable: false),
Identifier = table.Column<string>(type: "text", nullable: false),
MangaId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MetadataEntries", x => new { x.MetadataFetcherName, x.Identifier });
table.ForeignKey(
name: "FK_MetadataEntries_Mangas_MangaId",
column: x => x.MangaId,
principalTable: "Mangas",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MetadataEntries_MetadataFetcher_MetadataFetcherName",
column: x => x.MetadataFetcherName,
principalTable: "MetadataFetcher",
principalColumn: "Name",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MangaConnectorToChapter",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
ObjId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
MangaConnectorName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
IdOnConnectorSite = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
WebsiteUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
UseForDownload = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MangaConnectorToChapter", x => x.Key);
table.ForeignKey(
name: "FK_MangaConnectorToChapter_Chapters_ObjId",
column: x => x.ObjId,
principalTable: "Chapters",
principalColumn: "Key",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AltTitle_MangaKey",
table: "AltTitle",
column: "MangaKey");
migrationBuilder.CreateIndex(
name: "IX_AuthorToManga_MangaIds",
table: "AuthorToManga",
column: "MangaIds");
migrationBuilder.CreateIndex(
name: "IX_Chapters_ParentMangaId",
table: "Chapters",
column: "ParentMangaId");
migrationBuilder.CreateIndex(
name: "IX_Link_MangaKey",
table: "Link",
column: "MangaKey");
migrationBuilder.CreateIndex(
name: "IX_MangaConnectorToChapter_ObjId",
table: "MangaConnectorToChapter",
column: "ObjId");
migrationBuilder.CreateIndex(
name: "IX_MangaConnectorToManga_ObjId",
table: "MangaConnectorToManga",
column: "ObjId");
migrationBuilder.CreateIndex(
name: "IX_Mangas_LibraryId",
table: "Mangas",
column: "LibraryId");
migrationBuilder.CreateIndex(
name: "IX_MangaTagToManga_MangaIds",
table: "MangaTagToManga",
column: "MangaIds");
migrationBuilder.CreateIndex(
name: "IX_MetadataEntries_MangaId",
table: "MetadataEntries",
column: "MangaId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AltTitle");
migrationBuilder.DropTable(
name: "AuthorToManga");
migrationBuilder.DropTable(
name: "Link");
migrationBuilder.DropTable(
name: "MangaConnector");
migrationBuilder.DropTable(
name: "MangaConnectorToChapter");
migrationBuilder.DropTable(
name: "MangaConnectorToManga");
migrationBuilder.DropTable(
name: "MangaTagToManga");
migrationBuilder.DropTable(
name: "MetadataEntries");
migrationBuilder.DropTable(
name: "Authors");
migrationBuilder.DropTable(
name: "Chapters");
migrationBuilder.DropTable(
name: "Tags");
migrationBuilder.DropTable(
name: "MetadataFetcher");
migrationBuilder.DropTable(
name: "Mangas");
migrationBuilder.DropTable(
name: "FileLibraries");
}
}
}

View File

@@ -0,0 +1,535 @@
// <auto-generated />
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Manga
{
[DbContext(typeof(MangaContext))]
[Migration("20250901194144_KeyLengthChange")]
partial class KeyLengthChange
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.MangaConnectors.MangaConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.PrimitiveCollection<string[]>("BaseUris")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("IconUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("text[]");
b.HasKey("Name");
b.ToTable("MangaConnector");
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("Key");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ChapterNumber")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded")
.HasColumnType("boolean");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("Key");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.ToTable("FileLibraries");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DirectoryName")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("Key");
b.HasIndex("LibraryId");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToChapter");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToManga");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
b.Property<string>("Identifier")
.HasColumnType("text");
b.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.HasKey("MetadataFetcherName", "Identifier");
b.HasIndex("MangaId");
b.ToTable("MetadataEntries");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
{
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("MetadataEntry")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.HasKey("Name");
b.ToTable("MetadataFetcher");
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.MangaConnectors.Global", b =>
{
b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
{
b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher");
b.HasDiscriminator().HasValue("MyAnimeList");
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
{
b1.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("AltTitle");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
{
b1.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
.WithMany()
.HasForeignKey("MetadataFetcherName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("MetadataFetcher");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.MangaContext.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Navigation("MangaConnectorIds");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("MangaConnectorIds");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,258 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.Manga
{
/// <inheritdoc />
public partial class KeyLengthChange : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "MangaId",
table: "MetadataEntries",
type: "character varying(64)",
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "MangaIds",
table: "MangaTagToManga",
type: "character varying(64)",
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "Mangas",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "MangaConnectorToManga",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "MangaConnectorToChapter",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "MangaKey",
table: "Link",
type: "character varying(64)",
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "Link",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "FileLibraries",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "Chapters",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "MangaIds",
table: "AuthorToManga",
type: "character varying(64)",
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "AuthorIds",
table: "AuthorToManga",
type: "character varying(64)",
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "Authors",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "MangaKey",
table: "AltTitle",
type: "character varying(64)",
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "AltTitle",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "MangaId",
table: "MetadataEntries",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)");
migrationBuilder.AlterColumn<string>(
name: "MangaIds",
table: "MangaTagToManga",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "Mangas",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "MangaConnectorToManga",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "MangaConnectorToChapter",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<string>(
name: "MangaKey",
table: "Link",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "Link",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "FileLibraries",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "Chapters",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<string>(
name: "MangaIds",
table: "AuthorToManga",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)");
migrationBuilder.AlterColumn<string>(
name: "AuthorIds",
table: "AuthorToManga",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "Authors",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<string>(
name: "MangaKey",
table: "AltTitle",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "AltTitle",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
}
}
}

View File

@@ -0,0 +1,532 @@
// <auto-generated />
using API.Schema.MangaContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Manga
{
[DbContext(typeof(MangaContext))]
partial class MangaContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.MangaConnectors.MangaConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.PrimitiveCollection<string[]>("BaseUris")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("text[]");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("IconUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string[]>("SupportedLanguages")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("text[]");
b.HasKey("Name");
b.ToTable("MangaConnector");
b.HasDiscriminator<string>("Name").HasValue("MangaConnector");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("API.Schema.MangaContext.Author", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("AuthorName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.HasKey("Key");
b.ToTable("Authors");
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("ChapterNumber")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<bool>("Downloaded")
.HasColumnType("boolean");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ParentMangaId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Title")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<int?>("VolumeNumber")
.HasColumnType("integer");
b.HasKey("Key");
b.HasIndex("ParentMangaId");
b.ToTable("Chapters");
});
modelBuilder.Entity("API.Schema.MangaContext.FileLibrary", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("BasePath")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("LibraryName")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.ToTable("FileLibraries");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("CoverFileNameInCache")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("CoverUrl")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DirectoryName")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<float>("IgnoreChaptersBefore")
.HasColumnType("real");
b.Property<string>("LibraryId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("OriginalLanguage")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<byte>("ReleaseStatus")
.HasColumnType("smallint");
b.Property<long?>("Year")
.HasColumnType("bigint");
b.HasKey("Key");
b.HasIndex("LibraryId");
b.ToTable("Mangas");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToChapter");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("IdOnConnectorSite")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("MangaConnectorName")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("ObjId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<bool>("UseForDownload")
.HasColumnType("boolean");
b.Property<string>("WebsiteUrl")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.HasKey("Key");
b.HasIndex("ObjId");
b.ToTable("MangaConnectorToManga");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaTag", b =>
{
b.Property<string>("Tag")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.HasKey("Tag");
b.ToTable("Tags");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.Property<string>("MetadataFetcherName")
.HasColumnType("text");
b.Property<string>("Identifier")
.HasColumnType("text");
b.Property<string>("MangaId")
.IsRequired()
.HasColumnType("character varying(64)");
b.HasKey("MetadataFetcherName", "Identifier");
b.HasIndex("MangaId");
b.ToTable("MetadataEntries");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", b =>
{
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("MetadataEntry")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.HasKey("Name");
b.ToTable("MetadataFetcher");
b.HasDiscriminator<string>("MetadataEntry").HasValue("MetadataFetcher");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.Property<string>("AuthorIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("AuthorIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("AuthorToManga");
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.Property<string>("MangaTagIds")
.HasColumnType("character varying(64)");
b.Property<string>("MangaIds")
.HasColumnType("character varying(64)");
b.HasKey("MangaTagIds", "MangaIds");
b.HasIndex("MangaIds");
b.ToTable("MangaTagToManga");
});
modelBuilder.Entity("API.MangaConnectors.ComickIo", b =>
{
b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("ComickIo");
});
modelBuilder.Entity("API.MangaConnectors.Global", b =>
{
b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("Global");
});
modelBuilder.Entity("API.MangaConnectors.MangaDex", b =>
{
b.HasBaseType("API.MangaConnectors.MangaConnector");
b.HasDiscriminator().HasValue("MangaDex");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MyAnimeList", b =>
{
b.HasBaseType("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher");
b.HasDiscriminator().HasValue("MyAnimeList");
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "ParentManga")
.WithMany("Chapters")
.HasForeignKey("ParentMangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ParentManga");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.HasOne("API.Schema.MangaContext.FileLibrary", "Library")
.WithMany()
.HasForeignKey("LibraryId")
.OnDelete(DeleteBehavior.SetNull);
b.OwnsMany("API.Schema.MangaContext.AltTitle", "AltTitles", b1 =>
{
b1.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("Language")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("character varying(64)");
b1.Property<string>("Title")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("AltTitle");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.OwnsMany("API.Schema.MangaContext.Link", "Links", b1 =>
{
b1.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkProvider")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b1.Property<string>("LinkUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b1.Property<string>("MangaKey")
.IsRequired()
.HasColumnType("character varying(64)");
b1.HasKey("Key");
b1.HasIndex("MangaKey");
b1.ToTable("Link");
b1.WithOwner()
.HasForeignKey("MangaKey");
});
b.Navigation("AltTitles");
b.Navigation("Library");
b.Navigation("Links");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Chapter>", b =>
{
b.HasOne("API.Schema.MangaContext.Chapter", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaContext.MangaConnectorId<API.Schema.MangaContext.Manga>", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "Obj")
.WithMany("MangaConnectorIds")
.HasForeignKey("ObjId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Obj");
});
modelBuilder.Entity("API.Schema.MangaContext.MetadataFetchers.MetadataEntry", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", "Manga")
.WithMany()
.HasForeignKey("MangaId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.MetadataFetchers.MetadataFetcher", "MetadataFetcher")
.WithMany()
.HasForeignKey("MetadataFetcherName")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Manga");
b.Navigation("MetadataFetcher");
});
modelBuilder.Entity("AuthorToManga", b =>
{
b.HasOne("API.Schema.MangaContext.Author", null)
.WithMany()
.HasForeignKey("AuthorIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MangaTagToManga", b =>
{
b.HasOne("API.Schema.MangaContext.Manga", null)
.WithMany()
.HasForeignKey("MangaIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Schema.MangaContext.MangaTag", null)
.WithMany()
.HasForeignKey("MangaTagIds")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Schema.MangaContext.Chapter", b =>
{
b.Navigation("MangaConnectorIds");
});
modelBuilder.Entity("API.Schema.MangaContext.Manga", b =>
{
b.Navigation("Chapters");
b.Navigation("MangaConnectorIds");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,91 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using API.Schema.NotificationsContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Notifications
{
[DbContext(typeof(NotificationsContext))]
[Migration("20250703191820_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
{
b.Property<string>("Key")
.HasColumnType("text");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsSent")
.HasColumnType("boolean");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("Key");
b.ToTable("Notifications");
});
modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.Notifications
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:hstore", ",,");
migrationBuilder.CreateTable(
name: "NotificationConnectors",
columns: table => new
{
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
Headers = table.Column<Dictionary<string, string>>(type: "hstore", nullable: false),
HttpMethod = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
Body = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_NotificationConnectors", x => x.Name);
});
migrationBuilder.CreateTable(
name: "Notifications",
columns: table => new
{
Key = table.Column<string>(type: "text", nullable: false),
Urgency = table.Column<byte>(type: "smallint", nullable: false),
Title = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
Message = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
IsSent = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Notifications", x => x.Key);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "NotificationConnectors");
migrationBuilder.DropTable(
name: "Notifications");
}
}
}

View File

@@ -0,0 +1,92 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using API.Schema.NotificationsContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Notifications
{
[DbContext(typeof(NotificationsContext))]
[Migration("20250901194327_KeyLengthChange")]
partial class KeyLengthChange
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsSent")
.HasColumnType("boolean");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("Key");
b.ToTable("Notifications");
});
modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations.Notifications
{
/// <inheritdoc />
public partial class KeyLengthChange : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "Notifications",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "Notifications",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
}
}
}

View File

@@ -0,0 +1,89 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using API.Schema.NotificationsContext;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace API.Migrations.Notifications
{
[DbContext(typeof(NotificationsContext))]
partial class NotificationsContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("API.Schema.NotificationsContext.Notification", b =>
{
b.Property<string>("Key")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsSent")
.HasColumnType("boolean");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<byte>("Urgency")
.HasColumnType("smallint");
b.HasKey("Key");
b.ToTable("Notifications");
});
modelBuilder.Entity("API.Schema.NotificationsContext.NotificationConnectors.NotificationConnector", b =>
{
b.Property<string>("Name")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Body")
.IsRequired()
.HasMaxLength(4096)
.HasColumnType("character varying(4096)");
b.Property<Dictionary<string, string>>("Headers")
.IsRequired()
.HasColumnType("hstore");
b.Property<string>("HttpMethod")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("Url")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.HasKey("Name");
b.ToTable("NotificationConnectors");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,46 @@
using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace API;
public class NamedSwaggerGenOptions : IConfigureNamedOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider provider;
public NamedSwaggerGenOptions(IApiVersionDescriptionProvider provider)
{
this.provider = provider;
}
public void Configure(string? name, SwaggerGenOptions options)
{
Configure(options);
}
public void Configure(SwaggerGenOptions options)
{
// add swagger document for every API version discovered
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(
description.GroupName,
CreateVersionInfo(description));
}
}
private OpenApiInfo CreateVersionInfo(
ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = "Tranga " + description.GroupName,
Version = description.ApiVersion.ToString()
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
}

143
API/Program.cs Normal file
View File

@@ -0,0 +1,143 @@
using System.Reflection;
using API;
using API.Schema.LibraryContext;
using API.Schema.MangaContext;
using API.Schema.NotificationsContext;
using Asp.Versioning;
using Asp.Versioning.Builder;
using Asp.Versioning.Conventions;
using log4net;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll",
policy =>
{
policy
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
builder.Services.AddApiVersioning(option =>
{
option.AssumeDefaultVersionWhenUnspecified = true;
option.DefaultApiVersion = new ApiVersion(2);
option.ReportApiVersions = true;
option.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("X-Version"),
new MediaTypeApiVersionReader("x-version"));
})
.AddMvc(options =>
{
options.Conventions.Add(new VersionByNamespaceConvention());
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'V";
options.SubstituteApiVersionInUrl = true;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGenNewtonsoftSupport();
builder.Services.AddSwaggerGen(opt =>
{
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
opt.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
});
builder.Services.ConfigureOptions<NamedSwaggerGenOptions>();
string connectionString = $"Host={Environment.GetEnvironmentVariable("POSTGRES_HOST") ?? "localhost:5432"}; " +
$"Database={Environment.GetEnvironmentVariable("POSTGRES_DB") ?? "postgres"}; " +
$"Username={Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "postgres"}; " +
$"Password={Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "postgres"}";
builder.Services.AddDbContext<MangaContext>(options =>
options.UseNpgsql(connectionString));
builder.Services.AddDbContext<NotificationsContext>(options =>
options.UseNpgsql(connectionString));
builder.Services.AddDbContext<LibraryContext>(options =>
options.UseNpgsql(connectionString));
builder.Services.AddControllers(options =>
{
options.AllowEmptyInputInBodyModelBinding = true;
});
builder.Services.AddControllers().AddNewtonsoftJson(opts =>
{
opts.SerializerSettings.Converters.Add(new StringEnumConverter());
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
builder.Services.AddScoped<ILog>(_ => LogManager.GetLogger("API"));
builder.WebHost.UseUrls("http://*:6531");
var app = builder.Build();
ApiVersionSet apiVersionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(2))
.ReportApiVersions()
.Build();
app.UseCors("AllowAll");
app.MapControllers()
.WithApiVersionSet(apiVersionSet)
.MapToApiVersion(2);
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint($"/swagger/v2/swagger.json", "v2");
});
app.UseHttpsRedirection();
app.UseMiddleware<RequestTimeMiddleware>();
using (IServiceScope scope = app.Services.CreateScope())
{
MangaContext context = scope.ServiceProvider.GetRequiredService<MangaContext>();
context.Database.Migrate();
if (!context.FileLibraries.Any())
context.FileLibraries.Add(new FileLibrary(Tranga.Settings.DownloadLocation, "Default FileLibrary"));
await context.Sync(CancellationToken.None);
}
using (IServiceScope scope = app.Services.CreateScope())
{
NotificationsContext context = scope.ServiceProvider.GetRequiredService<NotificationsContext>();
context.Database.Migrate();
context.Notifications.RemoveRange(context.Notifications);
string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
context.Notifications.Add(new Notification("Tranga Started", emojis[Random.Shared.Next(0, emojis.Length - 1)], NotificationUrgency.High));
await context.Sync(CancellationToken.None);
}
using (IServiceScope scope = app.Services.CreateScope())
{
LibraryContext context = scope.ServiceProvider.GetRequiredService<LibraryContext>();
context.Database.Migrate();
await context.Sync(CancellationToken.None);
}
Tranga.SetServiceProvider(app.Services);
Tranga.StartLogger(new FileInfo("Log4Net.config.xml"));
Tranga.AddDefaultWorkers();
app.UseCors("AllowAll");
app.Run();

View File

@@ -0,0 +1,47 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:5976",
"sslPort": 44332,
"environmentVariables": {
"POSTGRES_Host": "localhost:5432"
}
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5287",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"POSTGRES_Host": "localhost:5432"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7206;http://localhost:5287",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"POSTGRES_Host": "localhost:5432"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"POSTGRES_Host": "localhost:5432"
}
}
}
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema;
[PrimaryKey("Key")]
public abstract class Identifiable
{
protected Identifiable()
{
this.Key = TokenGen.CreateToken(this.GetType());
}
protected Identifiable(string key)
{
this.Key = key;
}
[Required]
[StringLength(TokenGen.MaximumLength, MinimumLength = TokenGen.MinimumLength)]
public string Key { get; init; }
public override string ToString() => Key;
}

View File

@@ -0,0 +1,114 @@
using System.Text.Json;
using System.Text.Json.Nodes;
namespace API.Schema.LibraryContext.LibraryConnectors;
public class Kavita : LibraryConnector
{
public Kavita(string baseUrl, string auth) : base(LibraryType.Kavita, baseUrl, auth)
{
}
public Kavita(string baseUrl, string username, string password) :
this(baseUrl, GetToken(baseUrl, username, password))
{
}
private static string GetToken(string baseUrl, string username, string password)
{
HttpClient client = new()
{
DefaultRequestHeaders =
{
{ "Accept", "application/json" }
}
};
HttpRequestMessage requestMessage = new ()
{
Method = HttpMethod.Post,
RequestUri = new Uri($"{baseUrl}/api/Account/login"),
Content = new StringContent($"{{\"username\":\"{username}\",\"password\":\"{password}\"}}", System.Text.Encoding.UTF8, "application/json")
};
try
{
HttpResponseMessage response = client.Send(requestMessage);
if (response.IsSuccessStatusCode)
{
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(response.Content.ReadAsStream());
if (result is not null)
return result["token"]!.GetValue<string>();
}
else
{
}
}
catch (HttpRequestException)
{
}
return "";
}
protected override void UpdateLibraryInternal()
{
foreach (KavitaLibrary lib in GetLibraries())
NetClient.MakePost($"{BaseUrl}/api/ToFileLibrary/scan?libraryId={lib.id}", "Bearer", Auth);
}
internal override bool Test()
{
foreach (KavitaLibrary lib in GetLibraries())
if (NetClient.MakePost($"{BaseUrl}/api/ToFileLibrary/scan?libraryId={lib.id}", "Bearer", Auth))
return true;
return false;
}
/// <summary>
/// Fetches all libraries available to the user
/// </summary>
/// <returns>Array of KavitaLibrary</returns>
private IEnumerable<KavitaLibrary> GetLibraries()
{
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/ToFileLibrary/libraries", "Bearer", Auth);
if (data == Stream.Null)
{
Log.Info("No libraries found");
return [];
}
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
if (result is null)
{
Log.Info("No libraries found");
return [];
}
List<KavitaLibrary> ret = new();
foreach (JsonNode? jsonNode in result)
{
JsonObject? jObject = (JsonObject?)jsonNode;
if(jObject is null)
continue;
int libraryId = jObject["id"]!.GetValue<int>();
string libraryName = jObject["name"]!.GetValue<string>();
ret.Add(new KavitaLibrary(libraryId, libraryName));
}
return ret;
}
private struct KavitaLibrary
{
public int id { get; }
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string name { get; }
public KavitaLibrary(int id, string name)
{
this.id = id;
this.name = name;
}
}
}

View File

@@ -0,0 +1,75 @@
using System.Text.Json;
using System.Text.Json.Nodes;
namespace API.Schema.LibraryContext.LibraryConnectors;
public class Komga : LibraryConnector
{
public Komga(string baseUrl, string auth) : base(LibraryType.Komga, baseUrl, auth)
{
}
public Komga(string baseUrl, string username, string password)
: this(baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")))
{
}
protected override void UpdateLibraryInternal()
{
foreach (KomgaLibrary lib in GetLibraries())
NetClient.MakePost($"{BaseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", Auth);
}
internal override bool Test()
{
foreach (KomgaLibrary lib in GetLibraries())
if (NetClient.MakePost($"{BaseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", Auth))
return true;
return false;
}
/// <summary>
/// Fetches all libraries available to the user
/// </summary>
/// <returns>Array of KomgaLibraries</returns>
private IEnumerable<KomgaLibrary> GetLibraries()
{
Stream data = NetClient.MakeRequest($"{BaseUrl}/api/v1/libraries", "Basic", Auth);
if (data == Stream.Null)
{
Log.Info("No libraries found");
return [];
}
JsonArray? result = JsonSerializer.Deserialize<JsonArray>(data);
if (result is null)
{
Log.Info("No libraries found");
return [];
}
HashSet<KomgaLibrary> ret = new();
foreach (JsonNode? jsonNode in result)
{
var jObject = (JsonObject?)jsonNode;
string libraryId = jObject!["id"]!.GetValue<string>();
string libraryName = jObject["name"]!.GetValue<string>();
ret.Add(new KomgaLibrary(libraryId, libraryName));
}
return ret;
}
private struct KomgaLibrary
{
public string id { get; }
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string name { get; }
public KomgaLibrary(string id, string name)
{
this.id = id;
this.name = name;
}
}
}

View File

@@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using log4net;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema.LibraryContext.LibraryConnectors;
[PrimaryKey("Key")]
public abstract class LibraryConnector : Identifiable
{
public LibraryType LibraryType { get; init; }
[StringLength(256)] [Url] public string BaseUrl { get; init; }
[StringLength(256)] public string Auth { get; init; }
[NotMapped] protected ILog Log { get; init; }
protected LibraryConnector(LibraryType libraryType, string baseUrl, string auth)
: base()
{
this.LibraryType = libraryType;
this.BaseUrl = baseUrl;
this.Auth = auth;
this.Log = LogManager.GetLogger(GetType());
}
/// <summary>
/// EF CORE ONLY!!!!
/// </summary>
internal LibraryConnector(string key, LibraryType libraryType, string baseUrl, string auth)
: base(key)
{
this.LibraryType = libraryType;
this.BaseUrl = baseUrl;
this.Auth = auth;
this.Log = LogManager.GetLogger(GetType());
}
public override string ToString() => $"{base.ToString()} {this.LibraryType} {this.BaseUrl}";
protected abstract void UpdateLibraryInternal();
internal abstract bool Test();
}
public enum LibraryType : byte
{
Komga = 0,
Kavita = 1
}

View File

@@ -0,0 +1,73 @@
using System.Net;
using System.Net.Http.Headers;
using log4net;
namespace API.Schema.LibraryContext.LibraryConnectors;
public class NetClient
{
private static ILog Log = LogManager.GetLogger(typeof(NetClient));
public static Stream MakeRequest(string url, string authScheme, string auth)
{
Log.Debug($"Requesting {url}");
HttpClient client = new();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authScheme, auth);
HttpRequestMessage requestMessage = new()
{
Method = HttpMethod.Get,
RequestUri = new Uri(url)
};
try
{
HttpResponseMessage response = client.Send(requestMessage);
if (response.StatusCode is HttpStatusCode.Unauthorized &&
response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth);
else if (response.IsSuccessStatusCode)
return response.Content.ReadAsStream();
else
return Stream.Null;
}
catch (Exception e)
{
switch (e)
{
case HttpRequestException:
Log.Debug(e);
break;
default:
throw;
}
Log.Info("Failed to make request");
return Stream.Null;
}
}
public static bool MakePost(string url, string authScheme, string auth)
{
HttpClient client = new()
{
DefaultRequestHeaders =
{
{ "Accept", "application/json" },
{ "Authorization", new AuthenticationHeaderValue(authScheme, auth).ToString() }
}
};
HttpRequestMessage requestMessage = new ()
{
Method = HttpMethod.Post,
RequestUri = new Uri(url)
};
HttpResponseMessage response = client.Send(requestMessage);
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakePost(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth);
else if (response.IsSuccessStatusCode)
return true;
else
return false;
}
}

View File

@@ -0,0 +1,18 @@
using API.Schema.LibraryContext.LibraryConnectors;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.LibraryContext;
public class LibraryContext(DbContextOptions<LibraryContext> options) : TrangaBaseContext<LibraryContext>(options)
{
public DbSet<LibraryConnector> LibraryConnectors { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//LibraryConnector Types
modelBuilder.Entity<LibraryConnector>()
.HasDiscriminator(l => l.LibraryType)
.HasValue<Komga>(LibraryType.Komga)
.HasValue<Kavita>(LibraryType.Kavita);
}
}

View File

@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
public class AltTitle(string language, string title) : Identifiable(TokenGen.CreateToken("AltTitle"))
{
[StringLength(8)]
[Required]
public string Language { get; init; } = language;
[StringLength(256)]
[Required]
public string Title { get; init; } = title;
public override string ToString() => $"{base.ToString()} {Language} {Title}";
}

View File

@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
public class Author(string authorName) : Identifiable(TokenGen.CreateToken(typeof(Author), authorName))
{
[StringLength(128)]
public string AuthorName { get; init; } = authorName;
public override string ToString() => $"{base.ToString()} {AuthorName}";
}

View File

@@ -0,0 +1,217 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
public class Chapter : Identifiable, IComparable<Chapter>
{
[StringLength(64)] public string ParentMangaId { get; init; } = null!;
public Manga ParentManga = null!;
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors =>
MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
public ICollection<MangaConnectorId<Chapter>> MangaConnectorIds = null!;
public int? VolumeNumber { get; private set; }
[StringLength(10)] public string ChapterNumber { get; private set; }
[StringLength(256)] public string? Title { get; private set; }
[StringLength(256)] public string FileName { get; private set; }
public bool Downloaded { get; internal set; }
[NotMapped] public string FullArchiveFilePath => Path.Join(ParentManga.FullDirectoryPath, FileName);
public Chapter(Manga parentManga, string chapterNumber,
int? volumeNumber, string? title = null)
: base(TokenGen.CreateToken(typeof(Chapter), parentManga.Key, chapterNumber))
{
this.ParentManga = parentManga;
this.MangaConnectorIds = [];
this.VolumeNumber = volumeNumber;
this.ChapterNumber = chapterNumber;
this.Title = title;
this.FileName = GetArchiveFilePath();
this.Downloaded = false;
this.MangaConnectorIds = [];
}
/// <summary>
/// EF ONLY!!!
/// </summary>
internal Chapter(string key, int? volumeNumber, string chapterNumber, string? title, string fileName, bool downloaded)
: base(key)
{
this.VolumeNumber = volumeNumber;
this.ChapterNumber = chapterNumber;
this.Title = title;
this.FileName = fileName;
this.Downloaded = downloaded;
}
public int CompareTo(Chapter? other)
{
if (other is not { } otherChapter)
throw new ArgumentException($"{other} can not be compared to {this}");
return VolumeNumber?.CompareTo(otherChapter.VolumeNumber) switch
{
< 0 => -1,
> 0 => 1,
_ => CompareChapterNumbers(ChapterNumber, otherChapter.ChapterNumber)
};
}
/// <summary>
/// Checks the filesystem if an archive at the ArchiveFilePath exists
/// </summary>
/// <param name="context"></param>
/// <param name="token"></param>
/// <returns>True if archive exists on disk</returns>
/// <exception cref="KeyNotFoundException">Unable to load Chapter, Parent or Library</exception>
public async Task<bool> CheckDownloaded(MangaContext context, CancellationToken? token = null)
{
if(await context.Chapters
.Include(c => c.ParentManga)
.ThenInclude(p => p.Library)
.FirstOrDefaultAsync(c => c.Key == this.Key, token??CancellationToken.None) is not { } chapter)
throw new KeyNotFoundException("Unable to find chapter");
//TODO Log here
this.Downloaded = File.Exists(chapter.FullArchiveFilePath);
await context.Sync(token??CancellationToken.None);
return this.Downloaded;
}
/// Placeholders:
/// %M Obj Name
/// %V Volume
/// %C Chapter
/// %T Title
/// %A Author (first in list)
/// %I Chapter Internal ID
/// %i Obj Internal ID
/// %Y Year (Obj)
private static readonly Regex NullableRex = new(@"\?([a-zA-Z])\(([^\)]*)\)|(.+?)");
private static readonly Regex ReplaceRexx = new(@"%([a-zA-Z])|(.+?)");
private string GetArchiveFilePath()
{
string archiveNamingScheme = Tranga.Settings.ChapterNamingScheme;
StringBuilder stringBuilder = new();
foreach (Match nullable in NullableRex.Matches(archiveNamingScheme))
{
if (nullable.Groups[3].Success)
{
stringBuilder.Append(nullable.Groups[3].Value);
continue;
}
char placeholder = nullable.Groups[1].Value[0];
bool isNull = placeholder switch
{
'M' => ParentManga?.Name is null,
'V' => VolumeNumber is null,
'C' => ChapterNumber is null,
'T' => Title is null,
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName is null,
'Y' => ParentManga?.Year is null,
_ => true
};
if(!isNull)
stringBuilder.Append(nullable.Groups[2].Value);
}
string checkedString = stringBuilder.ToString();
stringBuilder = new();
foreach (Match replace in ReplaceRexx.Matches(checkedString))
{
if (replace.Groups[2].Success)
{
stringBuilder.Append(replace.Groups[2].Value);
continue;
}
char placeholder = replace.Groups[1].Value[0];
string? value = placeholder switch
{
'M' => ParentManga?.Name,
'V' => VolumeNumber?.ToString(),
'C' => ChapterNumber,
'T' => Title,
'A' => ParentManga?.Authors?.FirstOrDefault()?.AuthorName,
'Y' => ParentManga?.Year.ToString(),
_ => null
};
stringBuilder.Append(value);
}
stringBuilder.Append(".cbz");
return stringBuilder.ToString();
}
public class ChapterComparer : IComparer<Chapter>
{
public int Compare(Chapter? x, Chapter? y)
{
if (x is null && y is null)
return 0;
if(x is null)
return -1;
if (y is null)
return 1;
return CompareChapterNumbers(x.ChapterNumber, y.ChapterNumber);
}
}
private static int CompareChapterNumbers(string ch1, string ch2)
{
int[] ch1Arr = ch1.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray();
int[] ch2Arr = ch2.Split('.').Select(c => int.TryParse(c, out int result) ? result : -1).ToArray();
if (ch1Arr.Contains(-1) || ch2Arr.Contains(-1))
throw new ArgumentException("Chapter number is not in correct format");
int i = 0, j = 0;
while (i < ch1Arr.Length && j < ch2Arr.Length)
{
if (ch1Arr[i] < ch2Arr[j])
return -1;
if (ch1Arr[i] > ch2Arr[j])
return 1;
i++;
j++;
}
return 0;
}
internal string GetComicInfoXmlString()
{
XElement comicInfo = new("ComicInfo",
new XElement("Number", ChapterNumber)
);
if(Title is not null)
comicInfo.Add(new XElement("Title", Title));
if(ParentManga.MangaTags.Count > 0)
comicInfo.Add(new XElement("Tags", string.Join(',', ParentManga.MangaTags.Select(tag => tag.Tag))));
if(VolumeNumber is not null)
comicInfo.Add(new XElement("Volume", VolumeNumber));
if(ParentManga.Authors.Count > 0)
comicInfo.Add(new XElement("Writer", string.Join(',', ParentManga.Authors.Select(author => author.AuthorName))));
if(ParentManga.OriginalLanguage is not null)
comicInfo.Add(new XElement("LanguageISO", ParentManga.OriginalLanguage));
if(ParentManga.Description != string.Empty)
comicInfo.Add(new XElement("Summary", ParentManga.Description));
return comicInfo.ToString();
}
public override string ToString() => $"{base.ToString()} Vol.{VolumeNumber} Ch.{ChapterNumber} - {Title}";
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
public class FileLibrary(string basePath, string libraryName)
: Identifiable(TokenGen.CreateToken(typeof(FileLibrary), basePath))
{
[StringLength(256)] public string BasePath { get; internal set; } = basePath;
[StringLength(512)] public string LibraryName { get; internal set; } = libraryName;
public override string ToString() => $"{base.ToString()} {LibraryName} - {BasePath}";
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
public class Link(string linkProvider, string linkUrl) : Identifiable(TokenGen.CreateToken(typeof(Link), linkProvider, linkUrl))
{
[StringLength(64)]
public string LinkProvider { get; init; } = linkProvider;
[StringLength(2048)]
[Url]
public string LinkUrl { get; init; } = linkUrl;
public override string ToString() => $"{base.ToString()} {LinkProvider} {LinkUrl}";
}

View File

@@ -0,0 +1,184 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.InteropServices;
using System.Text;
using API.Workers;
using Microsoft.EntityFrameworkCore;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using static System.IO.UnixFileMode;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
public class Manga : Identifiable
{
[StringLength(512)] public string Name { get; internal set; }
[Required] public string Description { get; internal set; }
[Url] [StringLength(512)] public string CoverUrl { get; internal set; }
public MangaReleaseStatus ReleaseStatus { get; internal set; }
[StringLength(64)] public string? LibraryId { get; private set; }
public FileLibrary? Library = null!;
public ICollection<Author> Authors { get; internal set; } = null!;
public ICollection<MangaTag> MangaTags { get; internal set; } = null!;
public ICollection<Link> Links { get; internal set; } = null!;
public ICollection<AltTitle> AltTitles { get; internal set; } = null!;
public float IgnoreChaptersBefore { get; internal set; }
[StringLength(1024)] [Required] public string DirectoryName { get; private set; }
[StringLength(512)] public string? CoverFileNameInCache { get; internal set; }
public uint? Year { get; internal init; }
[StringLength(8)] public string? OriginalLanguage { get; internal init; }
[NotMapped] public string? FullDirectoryPath => Library is not null ? Path.Join(Library.BasePath, DirectoryName) : null;
[NotMapped] public ICollection<string> ChapterIds => Chapters.Select(c => c.Key).ToList();
public ICollection<Chapter> Chapters = null!;
[NotMapped] public Dictionary<string, string> IdsOnMangaConnectors => MangaConnectorIds.ToDictionary(id => id.MangaConnectorName, id => id.IdOnConnectorSite);
[NotMapped] public ICollection<string> MangaConnectorIdsIds => MangaConnectorIds.Select(id => id.Key).ToList();
public ICollection<MangaConnectorId<Manga>> MangaConnectorIds = null!;
public Manga(string name, string description, string coverUrl, MangaReleaseStatus releaseStatus,
ICollection<Author> authors, ICollection<MangaTag> mangaTags, ICollection<Link> links, ICollection<AltTitle> altTitles,
FileLibrary? library = null, float ignoreChaptersBefore = 0f, uint? year = null, string? originalLanguage = null)
:base(TokenGen.CreateToken(typeof(Manga), name))
{
this.Name = name;
this.Description = description;
this.CoverUrl = coverUrl;
this.ReleaseStatus = releaseStatus;
this.Library = library;
this.Authors = authors;
this.MangaTags = mangaTags;
this.Links = links;
this.AltTitles = altTitles;
this.IgnoreChaptersBefore = ignoreChaptersBefore;
this.DirectoryName = CleanDirectoryName(name);
this.Year = year;
this.OriginalLanguage = originalLanguage;
this.Chapters = [];
this.MangaConnectorIds = [];
}
/// <summary>
/// EF ONLY!!!
/// </summary>
public Manga(string key, string name, string description, string coverUrl,
MangaReleaseStatus releaseStatus,
string directoryName, float ignoreChaptersBefore, string? libraryId, uint? year, string? originalLanguage)
: base(key)
{
this.Name = name;
this.Description = description;
this.CoverUrl = coverUrl;
this.ReleaseStatus = releaseStatus;
this.DirectoryName = directoryName;
this.LibraryId = libraryId;
this.IgnoreChaptersBefore = ignoreChaptersBefore;
this.Year = year;
this.OriginalLanguage = originalLanguage;
}
public string CreatePublicationFolder()
{
string? publicationFolder = FullDirectoryPath;
if (publicationFolder is null)
throw new DirectoryNotFoundException("Publication folder not found");
if(!Directory.Exists(publicationFolder))
Directory.CreateDirectory(publicationFolder);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(publicationFolder, GroupRead | GroupWrite | GroupExecute | OtherRead | OtherWrite | OtherExecute | UserRead | UserWrite | UserExecute);
return publicationFolder;
}
//https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
//less than 32 is control *forbidden*
//34 is " *forbidden*
//42 is * *forbidden*
//47 is / *forbidden*
//58 is : *forbidden*
//60 is < *forbidden*
//62 is > *forbidden*
//63 is ? *forbidden*
//92 is \ *forbidden*
//124 is | *forbidden*
//127 is delete *forbidden*
//Below 127 all except *******
private static readonly int[] ForbiddenCharsBelow127 = [34, 42, 47, 58, 60, 62, 63, 92, 124, 127];
//Above 127 none except *******
private static readonly int[] IncludeCharsAbove127 = [128, 138, 142];
//128 is € include
//138 is Š include
//142 is Ž include
//152 through 255 looks fine except 157, 172, 173, 175 *******
private static readonly int[] ForbiddenCharsAbove152 = [157, 172, 173, 175];
private static string CleanDirectoryName(string name)
{
StringBuilder sb = new ();
foreach (char c in name)
{
if (c >= 32 && c < 127 && ForbiddenCharsBelow127.Contains(c) == false)
sb.Append(c);
else if (c > 127 && c < 152 && IncludeCharsAbove127.Contains(c))
sb.Append(c);
else if(c >= 152 && c <= 255 && ForbiddenCharsAbove152.Contains(c) == false)
sb.Append(c);
}
return sb.ToString();
}
/// <summary>
/// Merges another Manga (MangaConnectorIds and Chapters)
/// </summary>
/// <param name="other">The other <see cref="Manga" /> to merge</param>
/// <param name="context"><see cref="MangaContext"/> to use for Database operations</param>
/// <returns>An array of <see cref="MoveFileOrFolderWorker"/> for moving <see cref="Chapter"/> to new Directory</returns>
public BaseWorker[] MergeFrom(Manga other, MangaContext context)
{
context.Mangas.Remove(other);
List<BaseWorker> newJobs = new();
this.MangaConnectorIds = this.MangaConnectorIds
.UnionBy(other.MangaConnectorIds, id => id.MangaConnectorName)
.ToList();
foreach (Chapter otherChapter in other.Chapters)
{
string oldPath = otherChapter.FullArchiveFilePath;
Chapter newChapter = new(this, otherChapter.ChapterNumber, otherChapter.VolumeNumber,
otherChapter.Title);
this.Chapters.Add(newChapter);
string newPath = newChapter.FullArchiveFilePath;
newJobs.Add(new MoveFileOrFolderWorker(newPath, oldPath));
}
return newJobs.ToArray();
}
public async Task<(MemoryStream stream, FileInfo fileInfo)?> GetCoverImage(string cachePath, CancellationToken ct)
{
string fullPath = Path.Join(cachePath, CoverFileNameInCache);
if (!File.Exists(fullPath))
return null;
FileInfo fileInfo = new(fullPath);
MemoryStream stream = new (await File.ReadAllBytesAsync(fullPath, ct));
return (stream, fileInfo);
}
public override string ToString() => $"{base.ToString()} {Name}";
}
public enum MangaReleaseStatus : byte
{
Continuing = 0,
Completed = 1,
OnHiatus = 2,
Cancelled = 3,
Unreleased = 4
}

View File

@@ -0,0 +1,44 @@
using System.ComponentModel.DataAnnotations;
using API.MangaConnectors;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.MangaContext;
[PrimaryKey("Key")]
public class MangaConnectorId<T> : Identifiable where T : Identifiable
{
public T Obj = null!;
[StringLength(64)] public string ObjId { get; internal set; }
[StringLength(32)] public string MangaConnectorName { get; private set; }
[StringLength(256)] public string IdOnConnectorSite { get; init; }
[Url] [StringLength(512)] public string? WebsiteUrl { get; internal init; }
public bool UseForDownload { get; internal set; }
public MangaConnectorId(T obj, MangaConnector mangaConnector, string idOnConnectorSite, string? websiteUrl, bool useForDownload = false)
: base(TokenGen.CreateToken(typeof(MangaConnectorId<T>), mangaConnector.Name, idOnConnectorSite))
{
this.Obj = obj;
this.ObjId = obj.Key;
this.MangaConnectorName = mangaConnector.Name;
this.IdOnConnectorSite = idOnConnectorSite;
this.WebsiteUrl = websiteUrl;
this.UseForDownload = useForDownload;
}
/// <summary>
/// EF CORE ONLY!!!
/// </summary>
public MangaConnectorId(string key, string objId, string mangaConnectorName, string idOnConnectorSite, bool useForDownload, string? websiteUrl)
: base(key)
{
this.ObjId = objId;
this.MangaConnectorName = mangaConnectorName;
this.IdOnConnectorSite = idOnConnectorSite;
this.WebsiteUrl = websiteUrl;
this.UseForDownload = useForDownload;
}
public override string ToString() => $"{base.ToString()} {Obj}";
}

View File

@@ -0,0 +1,127 @@
using API.MangaConnectors;
using API.Schema.MangaContext.MetadataFetchers;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
namespace API.Schema.MangaContext;
public class MangaContext(DbContextOptions<MangaContext> options) : TrangaBaseContext<MangaContext>(options)
{
public DbSet<Manga> Mangas { get; set; }
public DbSet<FileLibrary> FileLibraries { get; set; }
public DbSet<Chapter> Chapters { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<MangaTag> Tags { get; set; }
public DbSet<MangaConnectorId<Manga>> MangaConnectorToManga { get; set; }
public DbSet<MangaConnectorId<Chapter>> MangaConnectorToChapter { get; set; }
public DbSet<MetadataEntry> MetadataEntries { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//MangaConnector Types
modelBuilder.Entity<MangaConnector>()
.HasDiscriminator(c => c.Name)
.HasValue<Global>("Global")
.HasValue<MangaDex>("MangaDex")
.HasValue<ComickIo>("ComickIo");
//Manga has many Chapters
modelBuilder.Entity<Manga>()
.HasMany<Chapter>(m => m.Chapters)
.WithOne(c => c.ParentManga)
.HasForeignKey(c => c.ParentMangaId)
.OnDelete(DeleteBehavior.Cascade);
//Chapter has MangaConnectorIds
modelBuilder.Entity<Chapter>()
.HasMany<MangaConnectorId<Chapter>>(c => c.MangaConnectorIds)
.WithOne(id => id.Obj)
.HasForeignKey(id => id.ObjId)
.OnDelete(DeleteBehavior.Cascade);
//Manga owns MangaAltTitles
modelBuilder.Entity<Manga>()
.OwnsMany<AltTitle>(m => m.AltTitles)
.WithOwner();
modelBuilder.Entity<Manga>()
.Navigation(m => m.AltTitles)
.AutoInclude();
//Manga owns Links
modelBuilder.Entity<Manga>()
.OwnsMany<Link>(m => m.Links)
.WithOwner();
modelBuilder.Entity<Manga>()
.Navigation(m => m.Links)
.AutoInclude();
//Manga has many Tags associated with many Obj
modelBuilder.Entity<Manga>()
.HasMany<MangaTag>(m => m.MangaTags)
.WithMany()
.UsingEntity("MangaTagToManga",
l=> l.HasOne(typeof(MangaTag)).WithMany().HasForeignKey("MangaTagIds").HasPrincipalKey(nameof(MangaTag.Tag)),
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.Key)),
j => j.HasKey("MangaTagIds", "MangaIds")
);
modelBuilder.Entity<Manga>()
.Navigation(m => m.MangaTags)
.AutoInclude();
//Manga has many Authors associated with many Obj
modelBuilder.Entity<Manga>()
.HasMany<Author>(m => m.Authors)
.WithMany()
.UsingEntity("AuthorToManga",
l=> l.HasOne(typeof(Author)).WithMany().HasForeignKey("AuthorIds").HasPrincipalKey(nameof(Author.Key)),
r => r.HasOne(typeof(Manga)).WithMany().HasForeignKey("MangaIds").HasPrincipalKey(nameof(Manga.Key)),
j => j.HasKey("AuthorIds", "MangaIds")
);
modelBuilder.Entity<Manga>()
.Navigation(m => m.Authors)
.AutoInclude();
//Manga has many MangaIds
modelBuilder.Entity<Manga>()
.HasMany<MangaConnectorId<Manga>>(m => m.MangaConnectorIds)
.WithOne(id => id.Obj)
.HasForeignKey(id => id.ObjId)
.OnDelete(DeleteBehavior.Cascade);
//FileLibrary has many Mangas
modelBuilder.Entity<FileLibrary>()
.HasMany<Manga>()
.WithOne(m => m.Library)
.HasForeignKey(m => m.LibraryId)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<MetadataFetcher>()
.HasDiscriminator<string>(nameof(MetadataEntry))
.HasValue<MyAnimeList>(nameof(MyAnimeList));
//MetadataEntry
modelBuilder.Entity<MetadataEntry>()
.HasOne<Manga>(entry => entry.Manga)
.WithMany()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<MetadataEntry>()
.HasOne<MetadataFetcher>(entry => entry.MetadataFetcher)
.WithMany()
.OnDelete(DeleteBehavior.Cascade);
}
public async Task<Manga?> FindMangaLike(Manga other, CancellationToken token)
{
if (await MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == other.Key, token) is { } f)
return f;
return await MangaIncludeAll()
.FirstOrDefaultAsync(m =>
m.Links.Any(l => l.Key == other.Key) ||
m.AltTitles.Any(t => other.AltTitles.Select(ot => ot.Title).Any(s => s.Equals(t.Title))), token);
}
public IIncludableQueryable<Manga, ICollection<MangaConnectorId<Manga>>> MangaIncludeAll() =>
Mangas
.Include(m => m.Library)
.Include(m => m.Authors)
.Include(m => m.MangaTags)
.Include(m => m.Links)
.Include(m => m.AltTitles)
.Include(m => m.Chapters)
.Include(m => m.MangaConnectorIds);
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.MangaContext;
[PrimaryKey("Tag")]
public class MangaTag(string tag)
{
[StringLength(64)]
[Required]
public string Tag { get; init; } = tag;
public override string ToString() => Tag;
}

View File

@@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema.MangaContext.MetadataFetchers;
[PrimaryKey("MetadataFetcherName", "Identifier")]
public class MetadataEntry
{
[JsonIgnore]
public Manga Manga { get; init; } = null!;
public string MangaId { get; init; }
[JsonIgnore]
public MetadataFetcher MetadataFetcher { get; init; } = null!;
public string MetadataFetcherName { get; init; }
public string Identifier { get; init; }
public MetadataEntry(MetadataFetcher fetcher, Manga manga, string identifier)
{
this.Manga = manga;
this.MangaId = manga.Key;
this.MetadataFetcher = fetcher;
this.MetadataFetcherName = fetcher.Name;
this.Identifier = identifier;
}
/// <summary>
/// EFCORE only!!!!
/// </summary>
internal MetadataEntry(string mangaId, string identifier, string metadataFetcherName)
{
this.MangaId = mangaId;
this.Identifier = identifier;
this.MetadataFetcherName = metadataFetcherName;
}
public override string ToString() => $"{GetType().FullName} {MangaId} {MetadataFetcherName}";
}

View File

@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;
namespace API.Schema.MangaContext.MetadataFetchers;
[PrimaryKey("Name")]
public abstract class MetadataFetcher
{
// ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength
public string Name { get; init; }
protected MetadataFetcher()
{
this.Name = this.GetType().Name;
}
/// <summary>
/// EFCORE ONLY!!!
/// </summary>
internal MetadataFetcher(string name)
{
this.Name = name;
}
internal MetadataEntry CreateMetadataEntry(Manga manga, string identifier) =>
new (this, manga, identifier);
public abstract MetadataSearchResult[] SearchMetadataEntry(Manga manga);
public abstract MetadataSearchResult[] SearchMetadataEntry(string searchTerm);
/// <summary>
/// Updates the Manga linked in the MetadataEntry
/// </summary>
public abstract Task UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext, CancellationToken token);
}

View File

@@ -0,0 +1,3 @@
namespace API.Schema.MangaContext.MetadataFetchers;
public record MetadataSearchResult(string Identifier, string Name, string Url, string? Description = null, string? CoverUrl = null);

View File

@@ -0,0 +1,91 @@
using System.Text.RegularExpressions;
using JikanDotNet;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace API.Schema.MangaContext.MetadataFetchers;
public class MyAnimeList : MetadataFetcher
{
private static readonly Jikan Jikan = new ();
private static readonly Regex GetIdFromUrl = new(@"https?:\/\/myanimelist\.net\/manga\/([0-9]+)\/?.*");
public override MetadataSearchResult[] SearchMetadataEntry(Manga manga)
{
if (manga.Links.Any(link => link.LinkProvider.Equals("MyAnimeList", StringComparison.InvariantCultureIgnoreCase)))
{
string url = manga.Links.First(link => link.LinkProvider.Equals("MyAnimeList", StringComparison.InvariantCultureIgnoreCase)).LinkUrl;
Match m = GetIdFromUrl.Match(url);
if (m.Success && m.Groups[1].Success)
{
long id = long.Parse(m.Groups[1].Value);
JikanDotNet.Manga data = Jikan.GetMangaAsync(id).Result.Data;
return [new MetadataSearchResult(id.ToString(), data.Titles.First().Title, data.Url, data.Synopsis)];
}
}
return SearchMetadataEntry(manga.Name);
}
public override MetadataSearchResult[] SearchMetadataEntry(string searchTerm)
{
ICollection<JikanDotNet.Manga> resultData = Jikan.SearchMangaAsync(searchTerm).Result.Data;
if (resultData.Count < 1)
return [];
return resultData.Select(data =>
new MetadataSearchResult(data.MalId.ToString(), data.Titles.First().Title, data.Url, data.Synopsis))
.ToArray();
}
/// <summary>
/// Updates the Manga linked in the MetadataEntry
/// </summary>
/// <param name="metadataEntry"></param>
/// <param name="dbContext"></param>
/// <param name="token"></param>
/// <exception cref="FormatException"></exception>
/// <exception cref="DbUpdateException"></exception>
public override async Task UpdateMetadata(MetadataEntry metadataEntry, MangaContext dbContext, CancellationToken token)
{
Manga? dbManga = metadataEntry.Manga; //Might be null!
if (dbManga is null)
{
if (await dbContext.Mangas.FirstOrDefaultAsync(m => m.Key == metadataEntry.MangaId, token) is not
{ } update)
throw new DbUpdateException("Manga not found");
dbManga = update;
}
// Load all collections (tags, links, authors)...
foreach (CollectionEntry collectionEntry in dbContext.Entry(dbManga).Collections)
{
if(!collectionEntry.IsLoaded)
await collectionEntry.LoadAsync(token);
}
await dbContext.Entry(dbManga).Reference(m => m.Library).LoadAsync(token);
MangaFull resultData;
try
{
long id = long.Parse(metadataEntry.Identifier);
if(await Jikan.GetMangaFullDataAsync(id, token) is not { } response)
throw new DbUpdateException("Manga Data not found");
resultData = response.Data;
}
catch (Exception)
{
throw new FormatException("ID was not in correct format");
}
dbManga.Name = resultData.Titles.First().Title;
dbManga.Description = resultData.Synopsis;
dbManga.AltTitles.Clear();
dbManga.AltTitles = resultData.Titles.Select(t => new AltTitle(t.Type, t.Title)).ToList();
dbManga.Authors.Clear();
dbManga.Authors = resultData.Authors.Select(a => new Author(a.Name)).ToList();
await dbContext.Sync(token);
}
}

View File

@@ -0,0 +1,56 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.NotificationsContext;
[PrimaryKey(nameof(Key))]
public class Notification : Identifiable
{
[Required]
public NotificationUrgency Urgency { get; init; }
[StringLength(128)]
[Required]
public string Title { get; init; }
[StringLength(512)]
[Required]
public string Message { get; init; }
[Required]
public DateTime Date { get; init; }
public bool IsSent { get; internal set; }
public Notification(string title, string message = "", NotificationUrgency urgency = NotificationUrgency.Normal, DateTime? date = null)
: base(TokenGen.CreateToken("Notification"))
{
this.Title = title;
this.Message = message;
this.Urgency = urgency;
this.Date = date ?? DateTime.UtcNow;
this.IsSent = false;
}
/// <summary>
/// EF ONLY!!!
/// </summary>
public Notification(string key, string title, string message, NotificationUrgency urgency, DateTime date, bool isSent)
: base(key)
{
this.Title = title;
this.Message = message;
this.Urgency = urgency;
this.Date = date;
this.IsSent = isSent;
}
public override string ToString() => $"{base.ToString()} {Urgency} {Title} {Message}";
}
public enum NotificationUrgency : byte
{
Low = 1,
Normal = 3,
High = 5
}

View File

@@ -0,0 +1,59 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
using log4net;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace API.Schema.NotificationsContext.NotificationConnectors;
[PrimaryKey("Name")]
public class NotificationConnector(string name, string url, Dictionary<string, string> headers, string httpMethod, string body)
{
[StringLength(64)] public string Name { get; init; } = name;
[StringLength(2048)] [Url] public string Url { get; internal set; } = url;
[Required] public Dictionary<string, string> Headers { get; internal set; } = headers;
[StringLength(8)] public string HttpMethod { get; internal set; } = httpMethod;
[StringLength(4096)] public string Body { get; internal set; } = body;
[NotMapped] private readonly HttpClient Client = new()
{
DefaultRequestHeaders = { { "User-Agent", Tranga.Settings.UserAgent } }
};
[JsonIgnore] protected ILog Log = LogManager.GetLogger(name);
public void SendNotification(string title, string notificationText)
{
Log.Info($"Sending notification: {title} - {notificationText}");
string formattedUrl = FormatStr(Url, title, notificationText);
string formattedBody = FormatStr(Body, title, notificationText);
Dictionary<string, string> formattedHeaders = Headers.ToDictionary(h => h.Key,
h => FormatStr(h.Value, title, notificationText));
HttpRequestMessage request = new(System.Net.Http.HttpMethod.Parse(HttpMethod), formattedUrl);
foreach (var (key, value) in formattedHeaders)
request.Headers.Add(key, value);
request.Content = new StringContent(formattedBody);
request.Content.Headers.ContentType = new ("application/json");
Log.Debug($"Request: {request}");
HttpResponseMessage response = Client.Send(request);
Log.Debug($"Response status code: {response.StatusCode} {response.Content.ReadAsStringAsync().Result}");
}
private string FormatStr(string str, string title, string text)
{
StringBuilder sb = new (str);
sb.Replace("%title", title);
sb.Replace("%text", text);
return sb.ToString();
}
public override string ToString() => $"{GetType().Name} {Name}";
}

View File

@@ -0,0 +1,10 @@
using API.Schema.NotificationsContext.NotificationConnectors;
using Microsoft.EntityFrameworkCore;
namespace API.Schema.NotificationsContext;
public class NotificationsContext(DbContextOptions<NotificationsContext> options) : TrangaBaseContext<NotificationsContext>(options)
{
public DbSet<NotificationConnector> NotificationConnectors { get; set; }
public DbSet<Notification> Notifications { get; set; }
}

View File

@@ -0,0 +1,40 @@
using log4net;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace API.Schema;
public abstract class TrangaBaseContext<T> : DbContext where T : DbContext
{
private ILog Log { get; init; }
protected TrangaBaseContext(DbContextOptions<T> options) : base(options)
{
this.Log = LogManager.GetLogger(GetType());
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.LogTo(s =>
{
Log.Debug(s);
}, Array.Empty<string>(), LogLevel.Warning, DbContextLoggerOptions.Level | DbContextLoggerOptions.Category | DbContextLoggerOptions.UtcTime);
}
internal async Task<(bool success, string? exceptionMessage)> Sync(CancellationToken token)
{
try
{
await this.SaveChangesAsync(token);
return (true, null);
}
catch (Exception e)
{
Log.Error(null, e);
return (false, e.Message);
}
}
public override string ToString() => $"{GetType().Name} {typeof(T).Name}";
}

37
API/TokenGen.cs Normal file
View File

@@ -0,0 +1,37 @@
using System.Security.Cryptography;
using System.Text;
namespace API;
public static class TokenGen
{
public const int MinimumLength = 16;
public const int MaximumLength = 64;
private const string Chars = "abcdefghijklmnopqrstuvwxyz0123456789";
public static string CreateToken(Type t, params string[] identifiers) => CreateToken(t.Name, identifiers);
public static string CreateToken(string prefix, params string[] identifiers)
{
if (prefix.Length + 1 >= MaximumLength - MinimumLength)
throw new ArgumentException("Prefix to long to create Token of meaningful length.");
int tokenLength = MaximumLength - prefix.Length - 1;
if (identifiers.Length == 0)
{
// No identifier, just create a random token
byte[] rng = new byte[tokenLength];
RandomNumberGenerator.Create().GetBytes(rng);
string key = new(rng.Select(b => Chars[b % Chars.Length]).ToArray());
key = string.Join('-', prefix, key);
return key;
}
// Identifier provided, create a token based on the identifier hashed
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(string.Join("", identifiers)));
string token = Convert.ToHexStringLower(hash);
return string.Join('-', prefix, token);
}
}

227
API/Tranga.cs Normal file
View File

@@ -0,0 +1,227 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using API.MangaConnectors;
using API.Schema.LibraryContext;
using API.Schema.MangaContext;
using API.Schema.MangaContext.MetadataFetchers;
using API.Schema.NotificationsContext;
using API.Workers;
using API.Workers.MaintenanceWorkers;
using log4net;
using log4net.Config;
using Microsoft.EntityFrameworkCore;
namespace API;
public static class Tranga
{
private static IServiceProvider? ServiceProvider;
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
internal static readonly MetadataFetcher[] MetadataFetchers = [new MyAnimeList()];
internal static readonly MangaConnector[] MangaConnectors = [new Global(), new MangaDex(), new ComickIo()];
internal static TrangaSettings Settings = TrangaSettings.Load();
internal static readonly UpdateMetadataWorker UpdateMetadataWorker = new ();
internal static readonly SendNotificationsWorker SendNotificationsWorker = new();
internal static readonly UpdateChaptersDownloadedWorker UpdateChaptersDownloadedWorker = new();
internal static readonly CheckForNewChaptersWorker CheckForNewChaptersWorker = new();
internal static readonly CleanupMangaCoversWorker CleanupMangaCoversWorker = new();
internal static readonly StartNewChapterDownloadsWorker StartNewChapterDownloadsWorker = new();
internal static readonly RemoveOldNotificationsWorker RemoveOldNotificationsWorker = new();
internal static readonly UpdateCoversWorker UpdateCoversWorker = new();
internal static void StartLogger(FileInfo loggerConfigFile)
{
XmlConfigurator.ConfigureAndWatch(loggerConfigFile);
Log.Info("Logger Configured.");
Log.Info(Constants.TRANGA);
}
internal static void AddDefaultWorkers()
{
AddWorker(UpdateMetadataWorker);
AddWorker(SendNotificationsWorker);
AddWorker(UpdateChaptersDownloadedWorker);
AddWorker(CheckForNewChaptersWorker);
AddWorker(CleanupMangaCoversWorker);
AddWorker(StartNewChapterDownloadsWorker);
AddWorker(RemoveOldNotificationsWorker);
AddWorker(UpdateCoversWorker);
}
internal static void SetServiceProvider(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
internal static bool TryGetMangaConnector(string name, [NotNullWhen(true)]out MangaConnector? mangaConnector)
{
mangaConnector =
MangaConnectors.FirstOrDefault(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
return mangaConnector != null;
}
internal static readonly ConcurrentDictionary<IPeriodic, Task> PeriodicWorkers = new ();
public static void AddWorker(BaseWorker worker)
{
Log.Debug($"Adding Worker {worker}");
StartWorker(worker);
if(worker is IPeriodic periodic)
AddPeriodicWorker(worker, periodic);
}
private static void AddPeriodicWorker(BaseWorker worker, IPeriodic periodic)
{
Log.Debug($"Adding Periodic {worker}");
Task periodicTask = PeriodicTask(worker, periodic);
PeriodicWorkers.TryAdd((worker as IPeriodic)!, periodicTask);
periodicTask.Start();
}
private static Task PeriodicTask(BaseWorker worker, IPeriodic periodic) => new (() =>
{
Log.Debug($"Waiting {periodic.Interval} for next run of {worker}");
Thread.Sleep(periodic.Interval);
StartWorker(worker, RefreshTask(worker, periodic));
});
private static Action RefreshTask(BaseWorker worker, IPeriodic periodic) => () =>
{
if (worker.State < WorkerExecutionState.Created) //Failed
return;
Log.Debug($"Refreshing {worker}");
Task periodicTask = PeriodicTask(worker, periodic);
PeriodicWorkers.AddOrUpdate((worker as IPeriodic)!, periodicTask, (_, _) => periodicTask);
periodicTask.Start();
};
public static void AddWorkers(IEnumerable<BaseWorker> workers)
{
foreach (BaseWorker baseWorker in workers)
AddWorker(baseWorker);
}
private static readonly ConcurrentDictionary<BaseWorker, Task<BaseWorker[]>> RunningWorkers = new();
public static BaseWorker[] GetRunningWorkers() => RunningWorkers.Keys.ToArray();
internal static void StartWorker(BaseWorker worker, Action? callback = null)
{
Log.Debug($"Starting {worker}");
if (ServiceProvider is null)
{
Log.Fatal("ServiceProvider is null");
return;
}
Action afterWorkCallback = AfterWork(worker, callback);
while (RunningWorkers.Count > Settings.MaxConcurrentWorkers)
{
Log.Warn($"{worker}: Max worker concurrency reached ({Settings.MaxConcurrentWorkers})! Waiting {Settings.WorkCycleTimeoutMs}ms...");
Thread.Sleep(Settings.WorkCycleTimeoutMs);
}
if (worker is BaseWorkerWithContext<MangaContext> mangaContextWorker)
{
mangaContextWorker.SetScope(ServiceProvider.CreateScope());
RunningWorkers.TryAdd(mangaContextWorker, mangaContextWorker.DoWork(afterWorkCallback));
}else if (worker is BaseWorkerWithContext<NotificationsContext> notificationContextWorker)
{
notificationContextWorker.SetScope(ServiceProvider.CreateScope());
RunningWorkers.TryAdd(notificationContextWorker, notificationContextWorker.DoWork(afterWorkCallback));
}else if (worker is BaseWorkerWithContext<LibraryContext> libraryContextWorker)
{
libraryContextWorker.SetScope(ServiceProvider.CreateScope());
RunningWorkers.TryAdd(libraryContextWorker, libraryContextWorker.DoWork(afterWorkCallback));
}else
RunningWorkers.TryAdd(worker, worker.DoWork(afterWorkCallback));
}
private static Action AfterWork(BaseWorker worker, Action? callback = null) => () =>
{
Log.Debug($"AfterWork {worker}");
if (RunningWorkers.TryGetValue(worker, out Task<BaseWorker[]>? task))
{
Log.Debug($"Waiting for Children to exit {worker}");
task.Wait();
if (task.IsCompleted)
{
try
{
BaseWorker[] newWorkers = task.Result;
Log.Debug($"{worker} created {newWorkers.Length} new Workers.");
AddWorkers(newWorkers);
}
catch (Exception e)
{
Log.Error(e);
}
}else Log.Warn($"Children failed: {worker}");
}
RunningWorkers.Remove(worker, out _);
callback?.Invoke();
};
internal static void StopWorker(BaseWorker worker)
{
Log.Debug($"Stopping {worker}");
if(worker is IPeriodic periodicWorker)
PeriodicWorkers.Remove(periodicWorker, out _);
worker.Cancel();
RunningWorkers.Remove(worker, out _);
}
internal static async Task<bool> AddMangaToContext(this MangaContext context, (Manga, MangaConnectorId<Manga>) addManga, CancellationToken token) =>
await AddMangaToContext(context, addManga.Item1, addManga.Item2, token);
internal static async Task<bool> AddMangaToContext(this MangaContext context, Manga addManga, MangaConnectorId<Manga> addMcId,
CancellationToken token)
{
context.ChangeTracker.Clear();
Manga? manga = await context.FindMangaLike(addManga, token);
if (manga is not null)
{
foreach (MangaConnectorId<Manga> mcId in addManga.MangaConnectorIds)
{
mcId.Obj = manga;
mcId.ObjId = manga.Key;
}
manga.MangaTags = manga.MangaTags.UnionBy(addManga.MangaTags, tag => tag.Tag).ToList();
manga.Authors = manga.Authors.UnionBy(addManga.Authors, author => author.Key).ToList();
manga.Links = manga.Links.UnionBy(addManga.Links, link => link.Key).ToList();
manga.AltTitles = manga.AltTitles.UnionBy(addManga.AltTitles, altTitle => altTitle.Key).ToList();
manga.Chapters = manga.Chapters.UnionBy(addManga.Chapters, chapter => chapter.Key).ToList();
manga.MangaConnectorIds = manga.MangaConnectorIds.UnionBy(addManga.MangaConnectorIds, id => id.MangaConnectorName).ToList();
addManga = manga;
}
else
{
manga = addManga;
IEnumerable<MangaTag> mergedTags = manga.MangaTags.Select(mt =>
{
MangaTag? inDb = context.Tags.Find(mt.Tag);
return inDb ?? mt;
});
manga.MangaTags = mergedTags.ToList();
IEnumerable<Author> mergedAuthors = manga.Authors.Select(ma =>
{
Author? inDb = context.Authors.Find(ma.Key);
return inDb ?? ma;
});
manga.Authors = mergedAuthors.ToList();
context.Mangas.Add(manga);
}
if (await context.Sync(token) is { success: false })
return false;
DownloadCoverFromMangaconnectorWorker downloadCoverWorker = new (addMcId);
AddWorker(downloadCoverWorker);
return true;
}
}

125
API/TrangaSettings.cs Normal file
View File

@@ -0,0 +1,125 @@
using System.Runtime.InteropServices;
using API.MangaDownloadClients;
using Newtonsoft.Json;
namespace API;
public struct TrangaSettings()
{
[JsonIgnore]
public static string workingDirectory => Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
[JsonIgnore]
public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
[JsonIgnore] public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
[JsonIgnore] public static string coverImageCacheOriginal => Path.Join(coverImageCache, "original");
[JsonIgnore] public static string coverImageCacheLarge => Path.Join(coverImageCache, "large");
[JsonIgnore] public static string coverImageCacheMedium => Path.Join(coverImageCache, "medium");
[JsonIgnore] public static string coverImageCacheSmall => Path.Join(coverImageCache, "small");
public string DownloadLocation => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Manga");
[JsonIgnore]
internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})";
public string UserAgent { get; set; } = DefaultUserAgent;
public int ImageCompression{ get; set; } = 40;
public bool BlackWhiteImages { get; set; } = false;
public string FlareSolverrUrl { get; set; } = string.Empty;
/// <summary>
/// Placeholders:
/// %M Obj Name
/// %V Volume
/// %C Chapter
/// %T Title
/// %A Author (first in list)
/// %I Chapter Internal ID
/// %i Obj Internal ID
/// %Y Year (Obj)
///
/// ?_(...) replace _ with a value from above:
/// Everything inside the braces will only be added if the value of %_ is not null
/// </summary>
public string ChapterNamingScheme { get; set; } = "%M - ?V(Vol.%V )Ch.%C?T( - %T)";
public int WorkCycleTimeoutMs { get; set; } = 20000;
[JsonIgnore]
internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
{
{RequestType.MangaInfo, 360},
{RequestType.MangaDexFeed, 360},
{RequestType.MangaDexImage, 60},
{RequestType.MangaImage, 240},
{RequestType.MangaCover, 60},
{RequestType.Default, 360}
};
public Dictionary<RequestType, int> RequestLimits { get; set; } = DefaultRequestLimits;
public string DownloadLanguage { get; set; } = "en";
public int MaxConcurrentDownloads { get; set; } = 5;
public int MaxConcurrentWorkers { get; set; } = 10;
public static TrangaSettings Load()
{
if (!File.Exists(settingsFilePath))
new TrangaSettings().Save();
return JsonConvert.DeserializeObject<TrangaSettings>(File.ReadAllText(settingsFilePath));
}
public void Save()
{
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this, Formatting.Indented));
}
public void SetUserAgent(string value)
{
this.UserAgent = value;
Save();
}
public void SetRequestLimit(RequestType type, int value)
{
this.RequestLimits[type] = value;
Save();
}
public void ResetRequestLimits()
{
this.RequestLimits = DefaultRequestLimits;
Save();
}
public void UpdateImageCompression(int value)
{
this.ImageCompression = value;
Save();
}
public void SetBlackWhiteImageEnabled(bool enabled)
{
this.BlackWhiteImages = enabled;
Save();
}
public void SetChapterNamingScheme(string scheme)
{
this.ChapterNamingScheme = scheme;
Save();
}
public void SetFlareSolverrUrl(string url)
{
this.FlareSolverrUrl = url;
Save();
}
public void SetDownloadLanguage(string language)
{
this.DownloadLanguage = language;
Save();
}
public void SetMaxConcurrentDownloads(int value)
{
this.MaxConcurrentDownloads = value;
Save();
}
}

127
API/Workers/BaseWorker.cs Normal file
View File

@@ -0,0 +1,127 @@
using API.Schema;
using log4net;
namespace API.Workers;
public abstract class BaseWorker : Identifiable
{
/// <summary>
/// Workers this Worker depends on being completed before running.
/// </summary>
private BaseWorker[] DependsOn { get; init; }
/// <summary>
/// Dependencies and dependencies of dependencies. See also <see cref="DependsOn"/>.
/// </summary>
internal IEnumerable<BaseWorker> AllDependencies => DependsOn.Select(d => d.AllDependencies).SelectMany(x => x);
/// <summary>
/// <see cref="AllDependencies"/> and Self.
/// </summary>
internal IEnumerable<BaseWorker> DependenciesAndSelf => AllDependencies.Append(this);
/// <summary>
/// <see cref="DependsOn"/> where <see cref="WorkerExecutionState"/> is less than Completed.
/// </summary>
internal IEnumerable<BaseWorker> MissingDependencies => DependsOn.Where(d => d.State < WorkerExecutionState.Completed);
public bool AllDependenciesFulfilled => DependsOn.All(d => d.State >= WorkerExecutionState.Completed);
internal WorkerExecutionState State { get; private set; }
private CancellationTokenSource _cancellationTokenSource = new ();
protected CancellationToken CancellationToken => _cancellationTokenSource.Token;
protected ILog Log { get; init; }
/// <summary>
/// Stops worker, and marks as <see cref="WorkerExecutionState"/>.Cancelled
/// </summary>
public void Cancel()
{
Log.Debug($"Cancelled {this}");
this.State = WorkerExecutionState.Cancelled;
_cancellationTokenSource.Cancel();
}
/// <summary>
/// Stops worker, and marks as <see cref="WorkerExecutionState"/>.Failed
/// </summary>
protected void Fail()
{
Log.Debug($"Failed {this}");
this.State = WorkerExecutionState.Failed;
_cancellationTokenSource.Cancel();
}
protected BaseWorker(IEnumerable<BaseWorker>? dependsOn = null)
{
this.DependsOn = dependsOn?.ToArray() ?? [];
this.Log = LogManager.GetLogger(GetType());
}
/// <summary>
/// Sets States during worker-run.
/// States:
/// <list type="bullet">
/// <item><see cref="WorkerExecutionState"/>.Waiting when waiting for <see cref="MissingDependencies"/></item>
/// <item><see cref="WorkerExecutionState"/>.Running when running</item>
/// <item><see cref="WorkerExecutionState"/>.Completed after finished</item>
/// </list>
/// </summary>
/// <returns>
/// <list type="bullet">
/// <item>If <see cref="BaseWorker"/> has <see cref="MissingDependencies"/>, missing dependencies.</item>
/// <item>If <see cref="MissingDependencies"/> are <see cref="WorkerExecutionState"/>.Running, itself after waiting for dependencies.</item>
/// <item>If <see cref="BaseWorker"/> has run, additional <see cref="BaseWorker"/>.</item>
/// </list>
/// </returns>
public Task<BaseWorker[]> DoWork(Action? callback = null)
{
// Start the worker
Log.Debug($"Checking {this}");
_cancellationTokenSource = new(TimeSpan.FromMinutes(10));
State = WorkerExecutionState.Waiting;
// Wait for dependencies, start them if necessary
BaseWorker[] missingDependenciesThatNeedStarting = MissingDependencies.Where(d => d.State < WorkerExecutionState.Waiting).ToArray();
if(missingDependenciesThatNeedStarting.Any())
return new (() => missingDependenciesThatNeedStarting);
if (MissingDependencies.Any())
return new (WaitForDependencies);
// Run the actual work
Log.Info($"Running {this}");
DateTime startTime = DateTime.UtcNow;
State = WorkerExecutionState.Running;
Task<BaseWorker[]> task = DoWorkInternal();
task.GetAwaiter().OnCompleted(Finish(startTime, callback));
return task;
}
private Action Finish(DateTime startTime, Action? callback = null) => () =>
{
DateTime endTime = DateTime.UtcNow;
Log.Info($"Completed {this}\n\t{endTime.Subtract(startTime).TotalMilliseconds} ms");
this.State = WorkerExecutionState.Completed;
if(this is IPeriodic periodic)
periodic.LastExecution = DateTime.UtcNow;
callback?.Invoke();
};
protected abstract Task<BaseWorker[]> DoWorkInternal();
private BaseWorker[] WaitForDependencies()
{
Log.Info($"Waiting for {MissingDependencies.Count()} Dependencies {this}:\n\t{string.Join("\n\t", MissingDependencies.Select(d => d.ToString()))}");
while (_cancellationTokenSource.IsCancellationRequested == false && MissingDependencies.Any())
{
Thread.Sleep(Tranga.Settings.WorkCycleTimeoutMs);
}
return [this];
}
}
public enum WorkerExecutionState
{
Failed = 0,
Cancelled = 32,
Created = 64,
Waiting = 96,
Running = 128,
Completed = 192
}

View File

@@ -0,0 +1,24 @@
using System.Configuration;
using Microsoft.EntityFrameworkCore;
namespace API.Workers;
public abstract class BaseWorkerWithContext<T>(IEnumerable<BaseWorker>? dependsOn = null) : BaseWorker(dependsOn) where T : DbContext
{
protected T DbContext = null!;
private IServiceScope? _scope;
public void SetScope(IServiceScope scope)
{
this._scope = scope;
this.DbContext = scope.ServiceProvider.GetRequiredService<T>();
}
/// <exception cref="ConfigurationErrorsException">Scope has not been set. <see cref="SetScope"/></exception>
public new Task<BaseWorker[]> DoWork()
{
if (DbContext is null)
throw new ConfigurationErrorsException("Scope has not been set.");
return base.DoWork();
}
}

9
API/Workers/IPeriodic.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace API.Workers;
public interface IPeriodic
{
internal DateTime LastExecution { get; set; }
public TimeSpan Interval { get; set; }
public DateTime NextExecution => LastExecution.Add(Interval);
public bool IsDue => NextExecution <= DateTime.UtcNow;
}

Some files were not shown because too many files have changed in this diff Show More