699 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
91fb815153 Update README.md
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-29 21:17:22 +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
6ed8ff1d52 webtoons - fix search regex parsing 2025-03-18 10:12:42 -05:00
3324ed6e4a Weebcentral - Fix Search Results Parse 2025-03-17 14:29:09 -05: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
08f26dd21d add referer to DownloadChapterImages 2025-03-14 21:18:51 -05: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
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
ebfa34e386 Update Manganato.cs 2025-03-07 22:33:24 +01:00
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
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
49dab9a670 Referrer policy changed
- Updated: image hosting platform seem to have changed a policy requiring now to send the referrer from the actual site instead of just allowing any connecting regardless of the referrer address
2025-03-07 19:57:27 +01:00
c9bc79fbd5 Update new_connector.yml
Some checks failed
Docker Image CI / build (push) Has been cancelled
2025-03-07 10:19:08 +01:00
83ce315f87 Merge pull request #357 from merlinmarijn/manganato-domain-switch
Some checks are pending
Docker Image CI / build (push) Waiting to run
Update Connector for Manganato connector: Migrate from .com to .gg & Adjust HTML Parsing
#358 @merlinmarijn
2025-03-07 10:06:44 +01:00
59511056d0 added try around getting urls 2025-03-03 23:43:35 +01:00
ed3ca5dba8 removed leftover comment 2025-03-03 23:04:43 +01:00
8df05d7e8a fixed image referrer 2025-03-03 22:59:25 +01:00
95d1e37b47 Update Manganato.cs 2025-03-03 22:27:37 +01:00
b6494ab7f9 Merge pull request #354 from C9Glax/dependabot/github_actions/docker/setup-qemu-action-3.6.0
Some checks failed
Docker Image CI / build (push) Has been cancelled
Bump docker/setup-qemu-action from 3.5.0 to 3.6.0
2025-03-03 13:54:59 +01:00
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
c6bb1c9180 [cuttingedge] fix(Chapter): Minor logic change to account for all chapterName cases 2025-03-02 16:06:21 +01:00
9a066e7ac7 [cuttingedge] fix(Weebcentral): Updated CheckChapterIsDownloaded logic to also consider chapter name if present 2025-03-02 15:57:09 +01:00
4bafffded4 [cuttingedge] feat(Weebcentra): When ordering chapters, order name desc to put special chapters first 2025-03-02 15:56:12 +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
5a4bc1c6de [cuttingedge] fix(Weebcentral): Handle case of chapter name with multiple number parts 2025-03-01 12:06:51 +01:00
71f663ca2f [cuttingedge] fix(Weebcentral): File name also depends on original chapter name 2025-03-01 11:39:12 +01:00
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
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
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
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
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
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
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
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
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
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
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
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
7f13d9b1e6 Fix
- forgotten comma
2025-02-06 15:39:06 +01:00
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
4c8d9bfaf2 [feature/weebcentral] Added Weebcentral to readme 2024-12-14 16:29:43 +01:00
dd988658c0 [feature/weebcentral] Added Weebcentral to connectors 2024-12-14 16:18:15 +01:00
cf4c84a47f [feature/weebcentral] Working download logic 2024-12-14 00:58:52 +01:00
5d9bfc3adf [feature/weebcentral] Get chapters 2024-12-14 00:45:10 +01:00
5a770c8e9f [feature/weebcentral] Working search 2024-12-13 23:42:35 +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
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
747df0bde5 Add Puppeteer Logger 2024-12-12 21:42:21 +01:00
463f360808 Dependency updates 2024-12-12 21:28:58 +01:00
85d7c07b13 Mangaworld add decimal-chapters (686.5) to regex
#289
2024-12-04 19:55:31 +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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
d52ec8d36f NTFY username and password usage instead of auth. 2024-06-15 21:24:28 +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
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
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
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
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
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
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
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
4692cc297a Fix MangaDex linksNode is null 2024-04-26 00:48:55 +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
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
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
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
f729c44f88 Merge branch 'refs/heads/master' into cuttingedge 2024-04-20 18:49:19 +02:00
f4966b0348 Docker Image build 2024-04-20 18:48:51 +02:00
df2fc4a036 Remove README CLI reference 2024-04-20 18:39:49 +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
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
4ba44d3ac3 Merge branch 'C9Glax:cuttingedge' into cuttingedge 2024-04-18 18:04:07 -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
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
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
051eb4a417 Merge pull request #158 from db-2001/cuttingedge
Reimplement Fix for Mangasee
2024-04-14 14:35:06 -04:00
482704af2c Merge remote-tracking branch 'upstream/cuttingedge' into cuttingedge 2024-04-14 14:29:30 -04:00
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
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
d5b6d4e8ee Fixes for https://github.com/C9Glax/tranga/issues/138 and bug fix for MDex 2024-03-29 23:59:16 -04:00
05190bc9e2 Holy moly a fix for Mangasee 2024-03-26 18:16:41 -04:00
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
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
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
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
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
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
4f9749d09e Fix bug with MangaDex, Useragent 2023-08-26 01:51:16 +02:00
64 changed files with 5094 additions and 1583 deletions

View File

@ -22,4 +22,6 @@
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
README.md
Manga
settings

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

View File

@ -11,18 +11,18 @@ jobs:
runs-on: ubuntu-latest
steps:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
# https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.2.0
uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.9.1
uses: docker/setup-buildx-action@v3.11.1
# https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub
@ -33,13 +33,13 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push API
uses: docker/build-push-action@v4.1.1
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
platforms: linux/amd64,linux/arm64
pull: true
push: true
tags: |
glax/tranga-api:cuttingedge
glax/tranga-api:cuttingedge

View File

@ -3,8 +3,6 @@ name: Docker Image CI
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
workflow_dispatch:
jobs:
@ -15,16 +13,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
# https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.2.0
uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.9.1
uses: docker/setup-buildx-action@v3.11.1
# https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub
@ -35,12 +33,12 @@ jobs:
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push API
uses: docker/build-push-action@v4.1.1
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
platforms: linux/amd64,linux/arm64
pull: true
push: true
tags: |

View File

@ -1,8 +1,10 @@
name: Docker Image CI
name: Docker Image CI
on:
push:
branches: [ "postgres-Server-V2" ]
workflow_dispatch:
jobs:
build:
@ -11,16 +13,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
# https://github.com/docker/setup-qemu-action#usage
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.2.0
uses: docker/setup-qemu-action@v3.6.0
# https://github.com/marketplace/actions/docker-setup-buildx
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.9.1
uses: docker/setup-buildx-action@v3.11.1
# https://github.com/docker/login-action#docker-hub
- name: Login to Docker Hub
@ -30,14 +32,14 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
# https://github.com/docker/build-push-action#multi-platform-image
- name: Build and push base
uses: docker/build-push-action@v4.1.1
- name: Build and push API
uses: docker/build-push-action@v6.18.0
with:
context: ./
file: ./Dockerfile-base
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
platforms: linux/amd64,linux/arm64
pull: true
push: true
tags: |
glax/tranga-base:latest
glax/tranga-api:Server-V2

6
.gitignore vendored
View File

@ -18,4 +18,8 @@ riderModule.iml
/dataSources.local.xml
/.idea
cover.jpg
cover.png
cover.png
/.vscode
/Manga
/settings
*.DotSettings.user

19
CLI/CLI.csproj Normal file
View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tranga\Tranga.csproj" />
</ItemGroup>
</Project>

157
CLI/Program.cs Normal file
View File

@ -0,0 +1,157 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Logging;
using Spectre.Console;
using Spectre.Console.Cli;
using Tranga;
var app = new CommandApp<TrangaCli>();
return app.Run(args);
internal sealed class TrangaCli : Command<TrangaCli.Settings>
{
public sealed class Settings : CommandSettings
{
[Description("Directory to which downloaded Manga are saved")]
[CommandOption("-d|--downloadLocation")]
[DefaultValue(null)]
public string? downloadLocation { get; init; }
[Description("Directory in which application-data is saved")]
[CommandOption("-w|--workingDirectory")]
[DefaultValue(null)]
public string? workingDirectory { get; init; }
[Description("Enables the file-logger")]
[CommandOption("-f")]
[DefaultValue(null)]
public bool? fileLogger { get; init; }
[Description("Path to save logfile to")]
[CommandOption("-l|--fPath")]
[DefaultValue(null)]
public string? fileLoggerPath { get; init; }
[Description("Port on which to run API on")]
[CommandOption("-p|--port")]
[DefaultValue(null)]
public int? apiPort { get; init; }
}
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
{
List<Logger.LoggerType> enabledLoggers = new();
if(settings.fileLogger is true)
enabledLoggers.Add(Logger.LoggerType.FileLogger);
string? logFolderPath = settings.fileLoggerPath ?? "";
Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, logFolderPath);
if(settings.workingDirectory is not null)
TrangaSettings.LoadFromWorkingDirectory(settings.workingDirectory);
else
TrangaSettings.CreateOrUpdate();
if(settings.downloadLocation is not null)
TrangaSettings.CreateOrUpdate(downloadDirectory: settings.downloadLocation);
Tranga.Tranga? api = null;
Thread trangaApi = new Thread(() =>
{
api = new(logger);
});
trangaApi.Start();
HttpClient client = new();
bool exit = false;
while (!exit)
{
string menuSelect = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Menu")
.PageSize(10)
.MoreChoicesText("Up/Down")
.AddChoices(new[]
{
"CustomRequest",
"Log",
"Exit"
}));
switch (menuSelect)
{
case "CustomRequest":
HttpMethod requestMethod = AnsiConsole.Prompt(
new SelectionPrompt<HttpMethod>()
.Title("Request Type")
.AddChoices(new[]
{
HttpMethod.Get,
HttpMethod.Delete,
HttpMethod.Post
}));
string requestPath = AnsiConsole.Prompt(
new TextPrompt<string>("Request Path:"));
List<ValueTuple<string, string>> parameters = new();
while (AnsiConsole.Confirm("Add Parameter?"))
{
string name = AnsiConsole.Ask<string>("Parameter Name:");
string value = AnsiConsole.Ask<string>("Parameter Value:");
parameters.Add(new ValueTuple<string, string>(name, value));
}
string requestString = $"http://localhost:{TrangaSettings.apiPortNumber}/{requestPath}";
if (parameters.Any())
{
requestString += "?";
foreach (ValueTuple<string, string> parameter in parameters)
requestString += $"{parameter.Item1}={parameter.Item2}&";
}
HttpRequestMessage request = new (requestMethod, requestString);
AnsiConsole.WriteLine($"Request: {request.Method} {request.RequestUri}");
HttpResponseMessage response;
if (AnsiConsole.Confirm("Send Request?"))
response = client.Send(request);
else break;
AnsiConsole.WriteLine($"Response: {(int)response.StatusCode} {response.StatusCode}");
AnsiConsole.WriteLine(response.Content.ReadAsStringAsync().Result);
break;
case "Log":
List<string> lines = logger.Tail(10).ToList();
Rows rows = new Rows(lines.Select(line => new Text(line)));
AnsiConsole.Live(rows).Start(context =>
{
bool running = true;
while (running)
{
string[] newLines = logger.GetNewLines();
if (newLines.Length > 0)
{
lines.AddRange(newLines);
rows = new Rows(lines.Select(line => new Text(line)));
context.UpdateTarget(rows);
}
Thread.Sleep(100);
if (AnsiConsole.Console.Input.IsKeyAvailable())
{
AnsiConsole.Console.Input.ReadKey(true); //Do not process input
running = false;
}
}
});
break;
case "Exit":
exit = true;
break;
}
}
if (api is not null)
api.keepRunning = false;
return 0;
}
}

View File

@ -1,15 +1,44 @@
# syntax=docker/dockerfile:1
ARG DOTNET=8.0
FROM mcr.microsoft.com/dotnet/sdk:7.0 as build-env
WORKDIR /src
COPY Tranga /src/Tranga
COPY Logging /src/Logging
COPY Tranga.sln /src
RUN dotnet restore /src/Tranga/Tranga.csproj
RUN dotnet publish -c Release -o /publish
FROM glax/tranga-base:latest as runtime
FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/runtime:$DOTNET AS base
WORKDIR /publish
COPY --from=build-env /publish .
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV XDG_CONFIG_HOME=/tmp/.chromium
ENV XDG_CACHE_HOME=/tmp/.chromium
RUN apt-get update \
&& apt-get install -y libx11-6 libx11-xcb1 libatk1.0-0 libgtk-3-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libxshmfence1 libnss3 chromium \
&& apt-get autopurge -y \
&& apt-get autoclean -y
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:$DOTNET AS build-env
WORKDIR /src
COPY Tranga.sln /src
COPY CLI/CLI.csproj /src/CLI/CLI.csproj
COPY Logging/Logging.csproj /src/Logging/Logging.csproj
COPY Tranga/Tranga.csproj /src/Tranga/Tranga.csproj
RUN dotnet restore /src/Tranga.sln
COPY . /src/
RUN dotnet publish -c Release --property:OutputPath=/publish -maxcpucount:1
FROM --platform=$TARGETPLATFORM base AS runtime
EXPOSE 6531
ARG UNAME=tranga
ARG UID=1000
ARG GID=1000
RUN groupadd -g $GID -o $UNAME \
&& useradd -m -u $UID -g $GID -o -s /bin/bash $UNAME \
&& mkdir /usr/share/tranga-api \
&& mkdir /Manga \
&& chown 1000:1000 /usr/share/tranga-api \
&& chown 1000:1000 /Manga
USER $UNAME
WORKDIR /publish
COPY --chown=1000:1000 --from=build-env /publish .
USER 0
ENTRYPOINT ["dotnet", "/publish/Tranga.dll"]
CMD ["-f", "-c", "-l", "/usr/share/tranga-api/logs"]

View File

@ -1,8 +0,0 @@
# syntax=docker/dockerfile:1
#FROM mcr.microsoft.com/dotnet/aspnet:7.0 as runtime
FROM mcr.microsoft.com/dotnet/runtime:7.0 as runtime
WORKDIR /publish
RUN apt-get update
RUN apt-get install -y libx11-6 libx11-xcb1 libatk1.0-0 libgtk-3-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libxshmfence1 libnss3
RUN apt-get autopurge -y
RUN apt-get autoclean -y

View File

@ -4,17 +4,18 @@ namespace Logging;
public class FileLogger : LoggerBase
{
private string logFilePath { get; }
internal string logFilePath { get; }
private const int MaxNumberOfLogFiles = 5;
public FileLogger(string logFilePath, Encoding? encoding = null) : base (encoding)
{
this.logFilePath = logFilePath;
DirectoryInfo dir = Directory.CreateDirectory(new FileInfo(logFilePath).DirectoryName!);
//Remove oldest logfile if more than MaxNumberOfLogFiles
string parentFolderPath = Path.GetDirectoryName(logFilePath)!;
for (int fileCount = new DirectoryInfo(parentFolderPath).EnumerateFiles().Count(); fileCount > MaxNumberOfLogFiles - 1; fileCount--) //-1 because we create own logfile later
File.Delete(new DirectoryInfo(parentFolderPath).EnumerateFiles().MinBy(file => file.LastWriteTime)!.FullName);
for (int fileCount = dir.EnumerateFiles().Count(); fileCount > MaxNumberOfLogFiles - 1; fileCount--) //-1 because we create own logfile later
File.Delete(dir.EnumerateFiles().MinBy(file => file.LastWriteTime)!.FullName);
}
protected override void Write(LogMessage logMessage)

View File

@ -1,6 +1,6 @@
namespace Logging;
public class LogMessage
public readonly struct LogMessage
{
public DateTime logTime { get; }
public string caller { get; }

View File

@ -1,9 +1,14 @@
using System.Text;
using System.Runtime.InteropServices;
using System.Text;
namespace Logging;
public class Logger : TextWriter
{
private static readonly string LogDirectoryPath = RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
? "/var/log/tranga-api"
: Path.Join(Directory.GetCurrentDirectory(), "logs");
public string? logFilePath => _fileLogger?.logFilePath;
public override Encoding Encoding { get; }
public enum LoggerType
{
@ -15,16 +20,18 @@ public class Logger : TextWriter
private readonly FormattedConsoleLogger? _formattedConsoleLogger;
private readonly MemoryLogger _memoryLogger;
public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFilePath)
public Logger(LoggerType[] enabledLoggers, TextWriter? stdOut, Encoding? encoding, string? logFolderPath)
{
this.Encoding = encoding ?? Encoding.ASCII;
if (enabledLoggers.Contains(LoggerType.FileLogger) && logFilePath is not null)
_fileLogger = new FileLogger(logFilePath, encoding);
else if(enabledLoggers.Contains(LoggerType.FileLogger) && logFilePath is null)
this.Encoding = encoding ?? Encoding.UTF8;
DateTime now = DateTime.Now;
if(enabledLoggers.Contains(LoggerType.FileLogger) && (logFolderPath is null || logFolderPath == ""))
{
_fileLogger = null;
throw new ArgumentException($"logFilePath can not be null for LoggerType {LoggerType.FileLogger}");
}
string filePath = Path.Join(LogDirectoryPath,
$"{now.ToShortDateString()}_{now.Hour}-{now.Minute}-{now.Second}.log");
_fileLogger = new FileLogger(filePath, encoding);
}else if (enabledLoggers.Contains(LoggerType.FileLogger) && logFolderPath is not null)
_fileLogger = new FileLogger(Path.Join(logFolderPath, $"{now.ToShortDateString()}_{now.Hour}-{now.Minute}-{now.Second}.log") , encoding);
if (enabledLoggers.Contains(LoggerType.ConsoleLogger) && stdOut is not null)
{
@ -36,6 +43,7 @@ public class Logger : TextWriter
throw new ArgumentException($"stdOut can not be null for LoggerType {LoggerType.ConsoleLogger}");
}
_memoryLogger = new MemoryLogger(encoding);
WriteLine(GetType().ToString(), $"Logfile: {logFilePath}");
}
public void WriteLine(string caller, string? value)
@ -64,4 +72,9 @@ public class Logger : TextWriter
{
return _memoryLogger.GetNewLines();
}
public string[] GetLog()
{
return _memoryLogger.GetLogMessages();
}
}

View File

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
</PropertyGroup>
</Project>

View File

@ -14,11 +14,13 @@ public class MemoryLogger : LoggerBase
protected override void Write(LogMessage value)
{
while(!_logMessages.TryAdd(value.logTime, value))
Thread.Sleep(10);
lock (_logMessages)
{
_logMessages.Add(DateTime.Now, value);
}
}
public string[] GetLogMessage()
public string[] GetLogMessages()
{
return Tail(Convert.ToUInt32(_logMessages.Count));
}
@ -35,7 +37,10 @@ public class MemoryLogger : LoggerBase
for (int retIndex = 0; retIndex < ret.Length; retIndex++)
{
ret[retIndex] = _logMessages.GetValueAtIndex(_logMessages.Count - retLength + retIndex).ToString();
lock (_logMessages)
{
ret[retIndex] = _logMessages.GetValueAtIndex(_logMessages.Count - retLength + retIndex).ToString();
}
}
_lastLogMessageIndex = _logMessages.Count - 1;
@ -45,14 +50,25 @@ public class MemoryLogger : LoggerBase
public string[] GetNewLines()
{
int logMessageCount = _logMessages.Count;
string[] ret = new string[logMessageCount - _lastLogMessageIndex];
List<string> ret = new();
for (int retIndex = 0; retIndex < ret.Length; retIndex++)
int retIndex = 0;
for (; retIndex < logMessageCount - _lastLogMessageIndex; retIndex++)
{
ret[retIndex] = _logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString();
try
{
lock(_logMessages)
{
ret.Add(_logMessages.GetValueAtIndex(_lastLogMessageIndex + retIndex).ToString());
}
}
catch (NullReferenceException)//Called when LogMessage has not finished writing
{
break;
}
}
_lastLogMessageIndex = logMessageCount;
return ret;
_lastLogMessageIndex = _lastLogMessageIndex + retIndex;
return ret.ToArray();
}
}

123
README.md
View File

@ -1,11 +1,6 @@
<!-- PROJECT SHIELDS -->
<!--
*** I'm using markdown "reference style" links for readability.
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
*** See the bottom of this document for the declaration of the reference variables
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
-->
# Testers for V2 wanted!
[Details](https://github.com/C9Glax/tranga/pull/355#issuecomment-2764217944)
<!-- PROJECT LOGO -->
<br />
@ -16,10 +11,11 @@
<p align="center">
Automatic Manga and Metadata downloader
</p>
<p align="center">
This is the API for <a href="https://github.com/C9Glax/tranga-website">Tranga-Website</a>
</p>
</div>
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
@ -30,9 +26,6 @@
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#screenshots">Screenshots</a>
</li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
@ -54,18 +47,40 @@
Tranga can download Chapters and Metadata from "Scanlation" sites such as
- [MangaDex.org](https://mangadex.org/)
- [Manganato.com](https://manganato.com/)
- [Mangasee](https://mangasee123.com/)
- [MangaKatana](https://mangakatana.com)
- ❓ Open an [issue](https://github.com/C9Glax/tranga/issues)
- [MangaDex.org](https://mangadex.org/) (Multilingual)
- [Manganato.com](https://manganato.com/) (en)
- [MangaKatana.com](https://mangakatana.com) (en)
- [Mangaworld.bz](https://www.mangaworld.bz/) (it)
- [Bato.to](https://bato.to/v3x) (en)
- [ManhuaPlus](https://manhuaplus.org/) (en)
- [MangaHere](https://www.mangahere.cc/) (en) (Their covers aren't scrapeable.)
- [Weebcentral](https://weebcentral.com) (en)
- [Webtoons](https://www.webtoons.com/en/)
- ❓ Open an [issue](https://github.com/C9Glax/tranga/issues/new?assignees=&labels=New+Connector&projects=&template=new_connector.yml&title=%5BNew+Connector%5D%3A+)
and trigger a library-scan with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/).
Notifications can be sent to your devices using [Gotify](https://gotify.net/), [LunaSea](https://www.lunasea.app/) or [Ntfy](https://ntfy.sh/
).
### What this does and doesn't do
Tranga (this git-repo) will open a port (standard 6531) and listen for requests to add Jobs to Monitor and/or download specific Manga.
The configuration is all done through HTTP-Requests.
_**For a web-frontend use [tranga-website](https://github.com/C9Glax/tranga-website).**_
This project downloads the images for a Manga from the specified Scanlation-Website and packages them with some metadata - from that same website - in a .cbz-archive (per chapter).
It does this on an interval, and checks for any Chapters (.cbz-Archive) not already existing in your specified Download-Location. (If you rename or move files, it will download those again)
Tranga can (if configured) trigger a scan in Komga or Kavita, however the directory in which the Manga reside has to be available to both Tranga and Komga/Kavita.
The project doesn't manage metadata, and doesn't curate, change or enhance any information that isn't available on the selected Scanlation-Site.
It will blindly use whatever is scrapes (yes this is a glorified Web-scraper).
and automatically import them with [Komga](https://komga.org/) and [Kavita](https://www.kavitareader.com/). Also Notifications will be sent to your devices using [Gotify](https://gotify.net/) and [LunaSea](https://www.lunasea.app/).
### Inspiration:
Because [Kaizoku](https://github.com/oae/kaizoku) was relying on [mangal](https://github.com/metafates/mangal) and mangal
hasn't received bugfixes for it's issues with Titles not showing up, or throwing errors because of illegal characters,
there were no alternatives for automatic downloads. However [Kaizoku](https://github.com/oae/kaizoku) certainly had a great Web-UI.
hasn't received bugfixes for its issues with Titles not showing up, or throwing errors because of illegal characters,
there were no alternatives for automatic downloads. However, [Kaizoku](https://github.com/oae/kaizoku) certainly had a great Web-UI.
That is why I wanted to create my own project, in a language I understand, and that I am able to maintain myself.
@ -77,73 +92,39 @@ That is why I wanted to create my own project, in a language I understand, and t
- Newtonsoft.JSON
- [PuppeteerSharp](https://www.puppeteersharp.com/)
- [Html Agility Pack (HAP)](https://html-agility-pack.net/)
- [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch)
- 💙 Blåhaj 🦈
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Star History
## Screenshots
| ![image](screenshots/overview.png) | ![image](screenshots/addtask.png) |
|-----------------------------------:|:----------------------------------|
| ![image](screenshots/settings.png) | ![image](screenshots/publication-description.png) | ![image](screenshots/progress.png) |
|-----------------------------------:|:-------------------------------------------------:|:-----------------------------------|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<a href="https://star-history.com/#c9glax/tranga&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=c9glax/tranga&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=c9glax/tranga&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=c9glax/tranga&type=Date" />
</picture>
</a>
<!-- GETTING STARTED -->
## Getting Started
There is two release types:
- CLI
- Docker
### CLI
Head over to [releases](https://git.bernloehr.eu/glax/Tranga/releases) and download. The CLI will guide you through setup.
### Docker
Download [docker-compose.yaml](https://git.bernloehr.eu/glax/Tranga/src/branch/master/docker-compose.yaml) and configure to your needs.
Download [docker-compose.yaml](https://git.bernloehr.eu/glax/Tranga/src/branch/master/docker-compose.yaml) and configure to your needs.
Mount `/Manga` to wherever you want your chapters (`.cbz`-Archives) downloaded (where Komga/Kavita can access them).
The `docker-compose` also includes [tranga-website](https://github.com/C9Glax/tranga-website) as frontend. For its configuration refer to the repo README.
Wherever you are mounting `/usr/share/Tranga-API` you also need to mount that same path + `/imageCache` in the webserver container.
### Docker-Website usage
There is two ways to download Mangas:
- Downloading everything and monitor for new Chapters
- Selecting specific Volumes/Chapters
On the website you add new tasks, by selecting the blue '+' field. Next select the connector/site you want to use, and enter a search term.
After clicking 'Search' (or pressing Enter), the results will be presented below - this might, depending on the result-size, take a while because we are already preloading the cover-images.
Next select the publication (by selecting the cover) and a new popup will open with two options:
- "Monitor" - Download all chapters and monitor for new ones
- "Download Chapter" - Download specific chapters only
When selecting `Monitor` you will be presented with a new window and the selection of the interval you want to check for new chapters (Default: Every 3 hours).
When selecting `Download Chapter` a list will open with all available chapters - that have not yet been downloaded - from which you can then select a range (see below).
The syntax for selecting chapters is as follows:
- To download a single Chapter enter either the index number (the number at the very start of the line) or its absolute number like so: `c(h)(apter)[number]`, spaces are allowed.
- To download a range of chapters enter either a range of index numbers (`3-6`) or chapters (`ch 12-23`).
- For volumes the syntax is as follows: `v(ol)[number](-[number])`, again spaces allowed.
Examples: `2-12`, `c1`, `ch 2`, `chapter 3`, `v 2`, `vol3-4`, `v2c4` (note: you can only specify a single chapter with this last syntax).
For compatibility do not execute the compose as root (which you should not do anyways...) but as user that can
access the folder.
### Prerequisites
#### To Build
[.NET-Core 7.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)
[.NET-Core 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
#### To Run
[.NET-Core 7.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) scroll down a bit, should be on the right the second item.
<!-- ROADMAP -->
## Roadmap
- [ ] Docker ARM support
- [ ]
[.NET-Core 8.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) scroll down a bit, should be on the right the second item.
See the [open issues](https://github.com/C9Glax/tranga/issues) for a full list of proposed features (and known issues).

View File

@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tranga", ".\Tranga\Tranga.c
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logging", "Logging\Logging.csproj", "{415BE889-BB7D-426F-976F-8D977876A462}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "CLI\CLI.csproj", "{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -18,5 +20,9 @@ Global
{415BE889-BB7D-426F-976F-8D977876A462}.Debug|Any CPU.Build.0 = Debug|Any CPU
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.ActiveCfg = Release|Any CPU
{415BE889-BB7D-426F-976F-8D977876A462}.Release|Any CPU.Build.0 = Release|Any CPU
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4324C816-F9D2-468F-8ED6-397FE2F0DCB3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -2,9 +2,13 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=altnames/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=authorsartists/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gotify/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=jjob/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Komga/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=lunasea/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mangakatana/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Manganato/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mangasee/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mangaworld/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ntfy/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Taskmanager/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tranga/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -1,5 +1,7 @@
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using static System.IO.UnixFileMode;
namespace Tranga;
@ -7,67 +9,134 @@ namespace Tranga;
/// Has to be Part of a publication
/// Includes the Chapter-Name, -VolumeNumber, -ChapterNumber, the location of the chapter on the internet and the saveName of the local file.
/// </summary>
public readonly struct Chapter
public readonly struct Chapter : IComparable
{
// ReSharper disable once MemberCanBePrivate.Global
public Publication parentPublication { get; }
public Manga parentManga { get; }
public string? name { get; }
public string? volumeNumber { get; }
public string chapterNumber { get; }
public float volumeNumber { get; }
public float chapterNumber { get; }
public string url { get; }
// ReSharper disable once MemberCanBePrivate.Global
public string fileName { get; }
public string? id { get; }
private static readonly Regex LegalCharacters = new (@"([A-z]*[0-9]* *\.*-*,*\]*\[*'*\'*\)*\(*~*!*)*");
private static readonly Regex IllegalStrings = new(@"Vol(ume)?.?", RegexOptions.IgnoreCase);
public Chapter(Publication parentPublication, string? name, string? volumeNumber, string chapterNumber, string url)
{
this.parentPublication = parentPublication;
this.name = name;
this.volumeNumber = volumeNumber;
this.chapterNumber = chapterNumber;
this.url = url;
private static readonly Regex IllegalStrings = new(@"(Vol(ume)?|Ch(apter)?)\.?", RegexOptions.IgnoreCase);
string chapterName = string.Concat(LegalCharacters.Matches(name ?? ""));
string volStr = this.volumeNumber is not null ? $"Vol.{this.volumeNumber} " : "";
string chNumberStr = $"Ch.{chapterNumber} ";
string chNameStr = chapterName.Length > 0 ? $"- {chapterName}" : "";
chNameStr = IllegalStrings.Replace(chNameStr, "");
this.fileName = $"{volStr}{chNumberStr}{chNameStr}";
public Chapter(Manga parentManga, string? name, string? volumeNumber, string chapterNumber, string url, string? id = null)
: this(parentManga, name, float.Parse(volumeNumber??"0", GlobalBase.numberFormatDecimalPoint),
float.Parse(chapterNumber, GlobalBase.numberFormatDecimalPoint), url, id)
{
}
public Chapter(Manga parentManga, string? name, float? volumeNumber, float chapterNumber, string url, string? id = null)
{
this.parentManga = parentManga;
this.name = name;
this.volumeNumber = volumeNumber??0;
this.chapterNumber = chapterNumber;
this.url = url;
this.id = id;
string chapterVolNumStr = $"Vol.{this.volumeNumber} Ch.{chapterNumber}";
if (name is not null && name.Length > 0)
{
string chapterName = IllegalStrings.Replace(string.Concat(LegalCharacters.Matches(name)), "");
this.fileName = chapterName.Length > 0 ? $"{chapterVolNumStr} - {chapterName}" : chapterVolNumStr;
}
else
this.fileName = chapterVolNumStr;
}
public override string ToString()
{
return $"Chapter {parentManga.sortName} {parentManga.internalId} {chapterNumber} {name}";
}
public override bool Equals(object? obj)
{
if (obj is not Chapter)
return false;
return CompareTo(obj) == 0;
}
public int CompareTo(object? obj)
{
if(obj is not Chapter otherChapter)
throw new ArgumentException($"{obj} can not be compared to {this}");
return volumeNumber.CompareTo(otherChapter.volumeNumber) switch
{
<0 => -1,
>0 => 1,
_ => chapterNumber.CompareTo(otherChapter.chapterNumber)
};
}
/// <summary>
/// Checks if a chapter-archive is already present
/// </summary>
/// <returns>true if chapter is present</returns>
internal bool CheckChapterIsDownloaded(string downloadLocation)
internal bool CheckChapterIsDownloaded()
{
string newFilePath = GetArchiveFilePath(downloadLocation);
if (!Directory.Exists(Path.Join(downloadLocation, parentPublication.folderName)))
string mangaDirectory = Path.Join(TrangaSettings.downloadLocation, parentManga.folderName);
if (!Directory.Exists(mangaDirectory))
return false;
FileInfo[] archives = new DirectoryInfo(Path.Join(downloadLocation, parentPublication.folderName)).GetFiles();
Regex chapterInfoRex = new(@"Ch\.[0-9.]+");
Regex chapterRex = new(@"[0-9]+(\.[0-9]+)?");
if (File.Exists(newFilePath))
return true;
string cn = this.chapterNumber;
if (archives.FirstOrDefault(archive => chapterRex.Match(chapterInfoRex.Match(archive.Name).Value).Value == cn) is { } path)
FileInfo? mangaArchive = null;
string markerPath = Path.Join(mangaDirectory, $".{id}");
if (this.id is not null && File.Exists(markerPath))
{
File.Move(path.FullName, newFilePath);
return true;
if(File.Exists(File.ReadAllText(markerPath)))
mangaArchive = new FileInfo(File.ReadAllText(markerPath));
else
File.Delete(markerPath);
}
return false;
if(mangaArchive is null)
{
FileInfo[] archives = new DirectoryInfo(mangaDirectory).GetFiles("*.cbz");
Regex volChRex = new(@"(?:Vol(?:ume)?\.([0-9]+)\D*)?Ch(?:apter)?\.([0-9]+(?:\.[0-9]+)*)(?: - (.*))?.cbz");
Chapter t = this;
mangaArchive = archives.FirstOrDefault(archive =>
{
Match m = volChRex.Match(archive.Name);
/*
* 1. If the volumeNumber is not present in the filename, it is not checked.
* 2. Check the chapterNumber in the chapter against the one in the filename.
* 3. The chpaterName has to either be absent both in the chapter and the filename or match.
*/
return (!m.Groups[1].Success || m.Groups[1].Value == t.volumeNumber.ToString(GlobalBase.numberFormatDecimalPoint)) &&
m.Groups[2].Value == t.chapterNumber.ToString(GlobalBase.numberFormatDecimalPoint) &&
((!m.Groups[3].Success && string.IsNullOrEmpty(t.name)) || m.Groups[3].Value == t.name);
});
}
string correctPath = GetArchiveFilePath();
if(mangaArchive is not null && mangaArchive.FullName != correctPath)
mangaArchive.MoveTo(correctPath, true);
return (mangaArchive is not null);
}
public void CreateChapterMarker()
{
if (this.id is null)
return;
string path = Path.Join(TrangaSettings.downloadLocation, parentManga.folderName, $".{id}");
File.WriteAllText(path, GetArchiveFilePath());
File.SetAttributes(path, FileAttributes.Hidden);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(path, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
}
/// <summary>
/// Creates full file path of chapter-archive
/// </summary>
/// <returns>Filepath</returns>
internal string GetArchiveFilePath(string downloadLocation)
internal string GetArchiveFilePath()
{
return Path.Join(downloadLocation, parentPublication.folderName, $"{parentPublication.folderName} - {this.fileName}.cbz");
return Path.Join(TrangaSettings.downloadLocation, parentManga.folderName, $"{parentManga.folderName} - {this.fileName}.cbz");
}
/// <summary>
@ -78,10 +147,10 @@ public readonly struct Chapter
internal string GetComicInfoXmlString()
{
XElement comicInfo = new XElement("ComicInfo",
new XElement("Tags", string.Join(',', parentPublication.tags)),
new XElement("LanguageISO", parentPublication.originalLanguage),
new XElement("Tags", string.Join(',', parentManga.tags)),
new XElement("LanguageISO", parentManga.originalLanguage),
new XElement("Title", this.name),
new XElement("Writer", string.Join(',', parentPublication.authors)),
new XElement("Writer", string.Join(',', parentManga.authors)),
new XElement("Volume", this.volumeNumber),
new XElement("Number", this.chapterNumber)
);

View File

@ -1,4 +1,6 @@
using Logging;
using System.Globalization;
using System.Text.RegularExpressions;
using Logging;
using Newtonsoft.Json;
using Tranga.LibraryConnectors;
using Tranga.NotificationConnectors;
@ -7,30 +9,53 @@ namespace Tranga;
public abstract class GlobalBase
{
protected Logger? logger { get; init; }
protected TrangaSettings settings { get; init; }
[JsonIgnore]
public Logger? logger { get; init; }
protected HashSet<NotificationConnector> notificationConnectors { get; init; }
protected HashSet<LibraryConnector> libraryConnectors { get; init; }
protected List<Publication> cachedPublications { get; init; }
private Dictionary<string, Manga> cachedPublications { get; init; }
public static readonly NumberFormatInfo numberFormatDecimalPoint = new (){ NumberDecimalSeparator = "." };
protected static readonly Regex baseUrlRex = new(@"https?:\/\/[0-9A-z\.-]+(:[0-9]+)?");
protected GlobalBase(GlobalBase clone)
{
this.logger = clone.logger;
this.settings = clone.settings;
this.notificationConnectors = clone.notificationConnectors;
this.libraryConnectors = clone.libraryConnectors;
this.cachedPublications = clone.cachedPublications;
}
protected GlobalBase(Logger? logger, TrangaSettings settings)
protected GlobalBase(Logger? logger)
{
this.logger = logger;
this.settings = settings;
this.notificationConnectors = settings.LoadNotificationConnectors();
this.libraryConnectors = settings.LoadLibraryConnectors();
this.notificationConnectors = TrangaSettings.LoadNotificationConnectors(this);
this.libraryConnectors = TrangaSettings.LoadLibraryConnectors(this);
this.cachedPublications = new();
}
protected void AddMangaToCache(Manga manga)
{
if (!this.cachedPublications.TryAdd(manga.internalId, manga))
{
Log($"Overwriting Manga {manga.internalId}");
this.cachedPublications[manga.internalId] = manga;
}
}
protected Manga? GetCachedManga(string internalId)
{
return cachedPublications.TryGetValue(internalId, out Manga manga) switch
{
true => manga,
_ => null
};
}
protected IEnumerable<Manga> GetAllCachedManga()
{
return cachedPublications.Values;
}
protected void Log(string message)
{
logger?.WriteLine(this.GetType().Name, message);
@ -41,20 +66,32 @@ public abstract class GlobalBase
Log(string.Format(fStr, replace));
}
protected void SendNotifications(string title, string text)
protected void SendNotifications(string title, string text, bool buffer = false)
{
foreach (NotificationConnector nc in notificationConnectors)
nc.SendNotification(title, text);
nc.SendNotification(title, text, buffer);
}
protected void AddNotificationConnector(NotificationConnector notificationConnector)
{
notificationConnectors.RemoveWhere(nc => nc.GetType() == notificationConnector.GetType());
Log($"Adding {notificationConnector}");
notificationConnectors.RemoveWhere(nc => nc.notificationConnectorType == notificationConnector.notificationConnectorType);
notificationConnectors.Add(notificationConnector);
while(IsFileInUse(settings.notificationConnectorsFilePath))
while(IsFileInUse(TrangaSettings.notificationConnectorsFilePath))
Thread.Sleep(100);
File.WriteAllText(settings.notificationConnectorsFilePath, JsonConvert.SerializeObject(notificationConnectors));
Log("Exporting notificationConnectors");
File.WriteAllText(TrangaSettings.notificationConnectorsFilePath, JsonConvert.SerializeObject(notificationConnectors));
}
protected void DeleteNotificationConnector(NotificationConnector.NotificationConnectorType notificationConnectorType)
{
Log($"Removing {notificationConnectorType}");
notificationConnectors.RemoveWhere(nc => nc.notificationConnectorType == notificationConnectorType);
while(IsFileInUse(TrangaSettings.notificationConnectorsFilePath))
Thread.Sleep(100);
Log("Exporting notificationConnectors");
File.WriteAllText(TrangaSettings.notificationConnectorsFilePath, JsonConvert.SerializeObject(notificationConnectors));
}
protected void UpdateLibraries()
@ -65,15 +102,29 @@ public abstract class GlobalBase
protected void AddLibraryConnector(LibraryConnector libraryConnector)
{
libraryConnectors.RemoveWhere(lc => lc.GetType() == libraryConnector.GetType());
Log($"Adding {libraryConnector}");
libraryConnectors.RemoveWhere(lc => lc.libraryType == libraryConnector.libraryType);
libraryConnectors.Add(libraryConnector);
while(IsFileInUse(settings.libraryConnectorsFilePath))
while(IsFileInUse(TrangaSettings.libraryConnectorsFilePath))
Thread.Sleep(100);
File.WriteAllText(settings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors));
Log("Exporting libraryConnectors");
File.WriteAllText(TrangaSettings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors, Formatting.Indented));
}
protected bool IsFileInUse(string filePath)
protected void DeleteLibraryConnector(LibraryConnector.LibraryType libraryType)
{
Log($"Removing {libraryType}");
libraryConnectors.RemoveWhere(lc => lc.libraryType == libraryType);
while(IsFileInUse(TrangaSettings.libraryConnectorsFilePath))
Thread.Sleep(100);
Log("Exporting libraryConnectors");
File.WriteAllText(TrangaSettings.libraryConnectorsFilePath, JsonConvert.SerializeObject(libraryConnectors, Formatting.Indented));
}
protected bool IsFileInUse(string filePath) => IsFileInUse(filePath, this.logger);
public static bool IsFileInUse(string filePath, Logger? logger)
{
if (!File.Exists(filePath))
return false;
@ -85,7 +136,7 @@ public abstract class GlobalBase
}
catch (IOException)
{
Log($"File is in use {filePath}");
logger?.WriteLine($"File is in use {filePath}");
return true;
}
}

View File

@ -1,4 +1,4 @@
using System.Text;
using System.Net;
using Tranga.MangaConnectors;
namespace Tranga.Jobs;
@ -6,26 +6,49 @@ namespace Tranga.Jobs;
public class DownloadChapter : Job
{
public Chapter chapter { get; init; }
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, connector, lastExecution, parentJobId: parentJobId)
{
this.chapter = chapter;
}
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter) : base(clone, connector)
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, connector, parentJobId: parentJobId)
{
this.chapter = chapter;
}
protected override string GetId()
{
return Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Concat(this.GetType().ToString(), chapter.parentPublication.internalId, chapter.chapterNumber)));
return $"{GetType()}-{chapter.parentManga.internalId}-{chapter.chapterNumber}";
}
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal()
public override string ToString()
{
return $"{id} Chapter: {chapter}";
}
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss)
{
Task downloadTask = new(delegate
{
mangaConnector.DownloadChapter(chapter, this.progressToken);
UpdateLibraries();
SendNotifications("Chapter downloaded", $"{chapter.parentPublication.sortName} - {chapter.chapterNumber}");
mangaConnector.CopyCoverFromCacheToDownloadLocation(chapter.parentManga);
HttpStatusCode success = mangaConnector.DownloadChapter(chapter, this.progressToken);
chapter.parentManga.UpdateLatestDownloadedChapter(chapter);
if (success == HttpStatusCode.OK)
{
UpdateLibraries();
SendNotifications("Chapter downloaded", $"{chapter.parentManga.sortName} - {chapter.chapterNumber}", true);
}
});
downloadTask.Start();
return Array.Empty<Job>();
}
public override bool Equals(object? obj)
{
if (obj is not DownloadChapter otherJob)
return false;
return otherJob.mangaConnector == this.mangaConnector &&
otherJob.chapter.Equals(this.chapter);
}
}

View File

@ -1,33 +1,59 @@
using System.Text;
using Tranga.MangaConnectors;
using Tranga.MangaConnectors;
namespace Tranga.Jobs;
public class DownloadNewChapters : Job
{
public Publication publication { get; init; }
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Publication publication, bool recurring = false) : base (clone, connector, recurring)
public Manga manga { get; set; }
public string translatedLanguage { get; init; }
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, DateTime lastExecution,
bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, connector, lastExecution, recurring,
recurrence, parentJobId)
{
this.publication = publication;
this.manga = manga;
this.translatedLanguage = translatedLanguage;
}
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base (clone, JobType.DownloadNewChaptersJob, connector, recurring, recurrence, parentJobId)
{
this.manga = manga;
this.translatedLanguage = translatedLanguage;
}
protected override string GetId()
{
return Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Concat(this.GetType().ToString(), publication.internalId)));
return $"{GetType()}-{manga.internalId}";
}
public override string ToString()
{
return $"{id} Manga: {manga}";
}
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal()
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss)
{
Chapter[] chapters = mangaConnector.GetNewChapters(publication);
manga.SaveSeriesInfoJson();
Chapter[] chapters = mangaConnector.GetNewChapters(manga, this.translatedLanguage);
this.progressToken.increments = chapters.Length;
List<Job> subJobs = new();
List<Job> jobs = new();
mangaConnector.CopyCoverFromCacheToDownloadLocation(manga);
foreach (Chapter chapter in chapters)
{
DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter);
subJobs.Add(downloadChapterJob);
DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id);
jobs.Add(downloadChapterJob);
}
UpdateMetadata updateMetadataJob = new(this, this.mangaConnector, this.manga, parentJobId: this.id);
jobs.Add(updateMetadataJob);
progressToken.Complete();
return subJobs;
return jobs;
}
public override bool Equals(object? obj)
{
if (obj is not DownloadNewChapters otherJob)
return false;
return otherJob.mangaConnector == this.mangaConnector &&
otherJob.manga.publicationId == this.manga.publicationId;
}
}

View File

@ -11,66 +11,88 @@ public abstract class Job : GlobalBase
public DateTime? lastExecution { get; private set; }
public DateTime nextExecution => NextExecution();
public string id => GetId();
internal IEnumerable<Job>? subJobs { get; private set; }
public string? parentJobId { get; init; }
public enum JobType : byte { DownloadChapterJob, DownloadNewChaptersJob, UpdateMetaDataJob }
public Job(GlobalBase clone, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null) : base(clone)
public JobType jobType;
internal Job(GlobalBase clone, JobType jobType, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
{
this.jobType = jobType;
this.mangaConnector = connector;
this.progressToken = new ProgressToken(0);
this.recurring = recurring;
if (recurring && recurrenceTime is null)
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
this.recurrenceTime = recurrenceTime;
else if(recurring && recurrenceTime is not null)
this.lastExecution = DateTime.Now.Subtract((TimeSpan)recurrenceTime);
this.recurrenceTime = recurrenceTime ?? TimeSpan.Zero;
this.parentJobId = parentJobId;
}
internal Job(GlobalBase clone, JobType jobType, MangaConnector connector, DateTime lastExecution, bool recurring = false,
TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
{
this.jobType = jobType;
this.mangaConnector = connector;
this.progressToken = new ProgressToken(0);
this.recurring = recurring;
if (recurring && recurrenceTime is null)
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
this.lastExecution = lastExecution;
this.recurrenceTime = recurrenceTime ?? TimeSpan.Zero;
this.parentJobId = parentJobId;
}
protected abstract string GetId();
public Job(GlobalBase clone, MangaConnector connector, ProgressToken progressToken, bool recurring = false, TimeSpan? recurrenceTime = null) : base(clone)
public void AddSubJob(Job job)
{
this.mangaConnector = connector;
this.progressToken = progressToken;
this.recurring = recurring;
if (recurring && recurrenceTime is null)
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
this.recurrenceTime = recurrenceTime;
}
public Job(GlobalBase clone, MangaConnector connector, int taskIncrements, bool recurring = false, TimeSpan? recurrenceTime = null) : base(clone)
{
this.mangaConnector = connector;
this.progressToken = new ProgressToken(taskIncrements);
this.recurring = recurring;
if (recurring && recurrenceTime is null)
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
this.recurrenceTime = recurrenceTime;
subJobs ??= new List<Job>();
subJobs = subJobs.Append(job);
}
private DateTime NextExecution()
{
if(recurring && recurrenceTime.HasValue && lastExecution.HasValue)
if(recurrenceTime.HasValue && lastExecution.HasValue)
return lastExecution.Value.Add(recurrenceTime.Value);
if(recurring && recurrenceTime.HasValue && !lastExecution.HasValue)
if(recurrenceTime.HasValue && !lastExecution.HasValue)
return DateTime.Now;
return DateTime.MaxValue;
}
public void Reset()
public void ResetProgress()
{
this.progressToken = new ProgressToken(this.progressToken.increments);
this.progressToken.increments -= progressToken.incrementsCompleted;
this.lastExecution = DateTime.Now;
this.progressToken.Waiting();
}
public void ExecutionEnqueue()
{
this.progressToken.increments -= progressToken.incrementsCompleted;
this.progressToken.Standby();
}
public void Cancel()
{
Log($"Cancelling {this}");
this.progressToken.cancellationRequested = true;
this.progressToken.Complete();
this.progressToken.Cancel();
this.lastExecution = DateTime.Now;
if(subJobs is not null)
foreach(Job subJob in subJobs)
subJob.Cancel();
}
public IEnumerable<Job> ExecuteReturnSubTasks()
public IEnumerable<Job> ExecuteReturnSubTasks(JobBoss jobBoss)
{
progressToken.Start();
IEnumerable<Job> ret = ExecuteReturnSubTasksInternal();
subJobs = ExecuteReturnSubTasksInternal(jobBoss);
lastExecution = DateTime.Now;
return ret;
return subJobs;
}
protected abstract IEnumerable<Job> ExecuteReturnSubTasksInternal();
protected abstract IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss);
}

View File

@ -1,4 +1,9 @@
using Tranga.MangaConnectors;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Tranga.MangaConnectors;
using static System.IO.UnixFileMode;
namespace Tranga.Jobs;
@ -7,57 +12,289 @@ public class JobBoss : GlobalBase
public HashSet<Job> jobs { get; init; }
private Dictionary<MangaConnector, Queue<Job>> mangaConnectorJobQueue { get; init; }
public JobBoss(GlobalBase clone) : base(clone)
public JobBoss(GlobalBase clone, HashSet<MangaConnector> connectors) : base(clone)
{
this.jobs = new();
LoadJobsList(connectors);
this.mangaConnectorJobQueue = new();
Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}");
}
public void AddJob(Job job)
public bool AddJob(Job job, string? jobFile = null)
{
this.jobs.Add(job);
if (ContainsJobLike(job))
{
Log($"Already Contains Job {job}");
return false;
}
else
{
if (!this.jobs.Add(job))
return false;
Log($"Added {job}");
UpdateJobFile(job, jobFile);
}
return true;
}
public void AddJobs(IEnumerable<Job> jobsToAdd)
{
foreach (Job job in jobsToAdd)
AddJob(job);
}
/// <summary>
/// Compares contents of the provided job and all current jobs
/// Does not check if objects are the same
/// </summary>
public bool ContainsJobLike(Job job)
{
return this.jobs.Any(existingJob => existingJob.Equals(job));
}
public void RemoveJob(Job job)
{
Log($"Removing {job}");
job.Cancel();
this.jobs.Remove(job);
if(job.subJobs is not null && job.subJobs.Any())
RemoveJobs(job.subJobs);
UpdateJobFile(job);
}
public void RemoveJobs(IEnumerable<Job?> jobsToRemove)
{
List<Job?> toRemove = jobsToRemove.ToList(); //Prevent multiple enumeration
Log($"Removing {toRemove.Count()} jobs.");
foreach (Job? job in toRemove)
if(job is not null)
RemoveJob(job);
}
public IEnumerable<Job> GetJobsLike(string? connectorName = null, string? internalId = null, float? chapterNumber = null)
{
IEnumerable<Job> ret = this.jobs;
if (connectorName is not null)
ret = ret.Where(job => job.mangaConnector.name == connectorName);
if (internalId is not null && chapterNumber is not null)
ret = ret.Where(jjob =>
{
if (jjob is not DownloadChapter job)
return false;
return job.chapter.parentManga.internalId == internalId &&
job.chapter.chapterNumber.Equals(chapterNumber);
});
else if (internalId is not null)
ret = ret.Where(jjob =>
{
if (jjob is not DownloadNewChapters job)
return false;
return job.manga.internalId == internalId;
});
return ret;
}
public IEnumerable<Job> GetJobsLike(MangaConnector? mangaConnector = null, Manga? publication = null,
Chapter? chapter = null)
{
if (chapter is not null)
return GetJobsLike(mangaConnector?.name, chapter.Value.parentManga.internalId, chapter.Value.chapterNumber);
else
return GetJobsLike(mangaConnector?.name, publication?.internalId);
}
public Job? GetJobById(string jobId)
{
if (this.jobs.FirstOrDefault(jjob => jjob.id == jobId) is { } job)
return job;
return null;
}
public bool TryGetJobById(string jobId, out Job? job)
{
if (this.jobs.FirstOrDefault(jjob => jjob.id == jobId) is { } ret)
{
job = ret;
return true;
}
job = null;
return false;
}
private bool QueueContainsJob(Job job)
{
mangaConnectorJobQueue.TryAdd(job.mangaConnector, new Queue<Job>());
if (mangaConnectorJobQueue.TryAdd(job.mangaConnector, new Queue<Job>()))//If we can add the queue, there is certainly no job in it
return true;
return mangaConnectorJobQueue[job.mangaConnector].Contains(job);
}
private void AddJobToQueue(Job job)
public void AddJobToQueue(Job job)
{
Log($"Adding Job to Queue. {job}");
mangaConnectorJobQueue.TryAdd(job.mangaConnector, new Queue<Job>());
Queue<Job> connectorJobQueue = mangaConnectorJobQueue[job.mangaConnector];
if(!connectorJobQueue.Contains(job))
connectorJobQueue.Enqueue(job);
if(!QueueContainsJob(job))
mangaConnectorJobQueue[job.mangaConnector].Enqueue(job);
job.ExecutionEnqueue();
}
public void AddJobsToQueue(IEnumerable<Job> jobs)
private void AddJobsToQueue(IEnumerable<Job> newJobs)
{
foreach(Job job in jobs)
foreach(Job job in newJobs)
AddJobToQueue(job);
}
private void LoadJobsList(HashSet<MangaConnector> connectors)
{
if (!Directory.Exists(TrangaSettings.jobsFolderPath)) //No jobs to load
{
Directory.CreateDirectory(TrangaSettings.jobsFolderPath);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(TrangaSettings.jobsFolderPath, UserRead | UserWrite | UserExecute | GroupRead | OtherRead);
return;
}
//Load json-job-files
foreach (FileInfo file in Directory.GetFiles(TrangaSettings.jobsFolderPath, "*.json").Select(f => new FileInfo(f)))
{
Log($"Adding {file.Name}");
try
{
Job? job = JsonConvert.DeserializeObject<Job>(File.ReadAllText(file.FullName),
new JobJsonConverter(this, new MangaConnectorJsonConverter(this, connectors)));
if (job is null) throw new NullReferenceException();
Log($"Adding Job {job}");
if (!AddJob(job, file.FullName)) //If we detect a duplicate, delete the file.
{
//string path = string.Concat(file.FullName, ".duplicate");
//file.MoveTo(path);
//Log($"Duplicate detected or otherwise not able to add job to list.\nMoved job {job} to {path}");
Log($"Duplicate detected or otherwise not able to add job to list. Removed the file {file.FullName} {job}");
}
}
catch (Exception e)
{
if (e is not UnreachableException or NullReferenceException)
throw;
Log(e.Message);
string newName = file.FullName + ".failed";
Log($"Failed loading file {file.Name}.\nMoving to {newName}.\n" +
$"If you think this is a bug, upload contents of the file to the Bugreport!");
File.Move(file.FullName, newName);
continue;
}
}
//Connect jobs to parent-jobs and add Publications to cache
foreach (Job job in this.jobs)
{
Log($"Loading Job {job}");
Job? parentJob = this.jobs.FirstOrDefault(jjob => jjob.id == job.parentJobId);
if (parentJob is not null)
{
parentJob.AddSubJob(job);
Log($"Parent Job {parentJob}");
}
if (job is DownloadNewChapters dncJob)
AddMangaToCache(dncJob.manga);
}
string[] coverFiles = Directory.GetFiles(TrangaSettings.coverImageCache);
foreach(string fileName in coverFiles.Where(fileName => !GetAllCachedManga().Any(manga => manga.coverFileNameInCache == fileName)))
File.Delete(fileName);
}
internal void UpdateJobFile(Job job, string? oldFile = null)
{
string newJobFilePath = Path.Join(TrangaSettings.jobsFolderPath, $"{job.id}.json");
string oldFilePath = oldFile??Path.Join(TrangaSettings.jobsFolderPath, $"{job.id}.json");
//Delete old file
if (File.Exists(oldFilePath))
{
Log($"Deleting Job-file {oldFilePath}");
try
{
while(IsFileInUse(oldFilePath))
Thread.Sleep(10);
File.Delete(oldFilePath);
}
catch (Exception e)
{
Log($"Error deleting {oldFilePath} job {job.id}\n{e}");
return; //Don't export a new file when we haven't actually deleted the old one
}
}
//Export job (in new file) if it is still in our jobs list
if (GetJobById(job.id) is not null)
{
Log($"Exporting Job {newJobFilePath}");
string jobStr = JsonConvert.SerializeObject(job, Formatting.Indented);
while(IsFileInUse(newJobFilePath))
Thread.Sleep(10);
File.WriteAllText(newJobFilePath, jobStr);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(newJobFilePath, UserRead | UserWrite | GroupRead | OtherRead);
}
}
private void UpdateAllJobFiles()
{
Log("Exporting Jobs");
foreach (Job job in this.jobs)
UpdateJobFile(job);
//Remove files with jobs not in this.jobs-list
Regex idRex = new (@"(.*)\.json");
foreach (FileInfo file in new DirectoryInfo(TrangaSettings.jobsFolderPath).EnumerateFiles())
{
if (idRex.IsMatch(file.Name))
{
string id = idRex.Match(file.Name).Groups[1].Value;
if (!this.jobs.Any(job => job.id == id))
{
try
{
file.Delete();
}
catch (Exception e)
{
Log(e.ToString());
}
}
}
}
}
public void CheckJobs()
{
foreach (Job job in jobs.Where(job => job.nextExecution < DateTime.Now && !QueueContainsJob(job)).OrderBy(job => job.nextExecution))
AddJobToQueue(job);
AddJobsToQueue(jobs.Where(job => job.progressToken.state == ProgressToken.State.Waiting && job.nextExecution < DateTime.Now && !QueueContainsJob(job)).OrderBy(job => job.nextExecution));
foreach (Queue<Job> jobQueue in mangaConnectorJobQueue.Values)
{
if(jobQueue.Count < 1)
continue;
Job queueHead = jobQueue.Peek();
if (queueHead.progressToken.state == ProgressToken.State.Complete)
if (queueHead.progressToken.state is ProgressToken.State.Complete or ProgressToken.State.Cancelled)
{
if(queueHead.recurring)
queueHead.Reset();
if(!queueHead.recurring)
RemoveJob(queueHead);
else
queueHead.ResetProgress();
jobQueue.Dequeue();
AddJobsToQueue(jobQueue.Peek().ExecuteReturnSubTasks());
Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}");
}else if (queueHead.progressToken.state is ProgressToken.State.Standby)
{
Job eJob = jobQueue.Peek();
Job[] subJobs = eJob.ExecuteReturnSubTasks(this).ToArray();
UpdateJobFile(eJob);
AddJobs(subJobs);
AddJobsToQueue(subJobs);
}else if (queueHead.progressToken.state is ProgressToken.State.Running && DateTime.Now.Subtract(queueHead.progressToken.lastUpdate) > TimeSpan.FromMinutes(5))
{
Log($"{queueHead} inactive for more than 5 minutes. Cancelling.");
queueHead.Cancel();
}
}
}

View File

@ -0,0 +1,84 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Tranga.MangaConnectors;
namespace Tranga.Jobs;
public class JobJsonConverter : JsonConverter
{
private GlobalBase _clone;
private MangaConnectorJsonConverter _mangaConnectorJsonConverter;
internal JobJsonConverter(GlobalBase clone, MangaConnectorJsonConverter mangaConnectorJsonConverter)
{
this._clone = clone;
this._mangaConnectorJsonConverter = mangaConnectorJsonConverter;
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(Job));
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
if (jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.UpdateMetaDataJob)
{
return new UpdateMetadata(this._clone,
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
{
Converters =
{
this._mangaConnectorJsonConverter
}
}))!,
jo.GetValue("manga")!.ToObject<Manga>(),
jo.GetValue("parentJobId")!.Value<string?>());
}else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.DownloadNewChaptersJob) || jo.ContainsKey("translatedLanguage"))//TODO change to jobType
{
DateTime lastExecution = jo.GetValue("lastExecution") is {} le
? le.ToObject<DateTime>()
: DateTime.UnixEpoch; //TODO do null checks on all variables
return new DownloadNewChapters(this._clone,
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
{
Converters =
{
this._mangaConnectorJsonConverter
}
}))!,
jo.GetValue("manga")!.ToObject<Manga>(),
lastExecution,
jo.GetValue("recurring")!.Value<bool>(),
jo.GetValue("recurrenceTime")!.ToObject<TimeSpan?>(),
jo.GetValue("parentJobId")!.Value<string?>());
}else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value<byte>() == (byte)Job.JobType.DownloadChapterJob) || jo.ContainsKey("chapter"))//TODO change to jobType
{
return new DownloadChapter(this._clone,
jo.GetValue("mangaConnector")!.ToObject<MangaConnector>(JsonSerializer.Create(new JsonSerializerSettings()
{
Converters =
{
this._mangaConnectorJsonConverter
}
}))!,
jo.GetValue("chapter")!.ToObject<Chapter>(),
DateTime.UnixEpoch,
jo.GetValue("parentJobId")!.Value<string?>());
}
throw new Exception();
}
public override bool CanWrite => false;
/// <summary>
/// Don't call this
/// </summary>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new Exception("Dont call this");
}
}

View File

@ -6,8 +6,11 @@ public class ProgressToken
public int increments { get; set; }
public int incrementsCompleted { get; set; }
public float progress => GetProgress();
public DateTime lastUpdate { get; private set; }
public DateTime executionStarted { get; private set; }
public TimeSpan timeRemaining => GetTimeRemaining();
public enum State { Running, Complete, Standby }
public enum State { Running, Complete, Standby, Cancelled, Waiting }
public State state { get; private set; }
public ProgressToken(int increments)
@ -15,30 +18,61 @@ public class ProgressToken
this.cancellationRequested = false;
this.increments = increments;
this.incrementsCompleted = 0;
this.state = State.Standby;
this.state = State.Waiting;
this.executionStarted = DateTime.UnixEpoch;
this.lastUpdate = DateTime.UnixEpoch;
}
private float GetProgress()
{
if(increments > 0 && incrementsCompleted > 0)
return (float)incrementsCompleted / (float)increments;
return incrementsCompleted / (float)increments;
return 0;
}
private TimeSpan GetTimeRemaining()
{
if (increments > 0 && incrementsCompleted > 0)
return DateTime.Now.Subtract(this.executionStarted).Divide(incrementsCompleted).Multiply(increments - incrementsCompleted);
return TimeSpan.MaxValue;
}
public void Increment()
{
this.lastUpdate = DateTime.Now;
this.incrementsCompleted++;
if (incrementsCompleted > increments)
state = State.Complete;
}
public void Standby()
{
this.lastUpdate = DateTime.Now;
state = State.Standby;
}
public void Start()
{
this.lastUpdate = DateTime.Now;
state = State.Running;
this.executionStarted = DateTime.Now;
}
public void Complete()
{
this.lastUpdate = DateTime.Now;
state = State.Complete;
}
public void Cancel()
{
this.lastUpdate = DateTime.Now;
state = State.Cancelled;
}
public void Waiting()
{
this.lastUpdate = DateTime.Now;
state = State.Waiting;
}
}

View File

@ -0,0 +1,76 @@
using Tranga.MangaConnectors;
namespace Tranga.Jobs;
public class UpdateMetadata : Job
{
public Manga manga { get; set; }
public UpdateMetadata(GlobalBase clone, MangaConnector connector, Manga manga, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, connector, parentJobId: parentJobId)
{
this.manga = manga;
}
protected override string GetId()
{
return $"{GetType()}-{manga.internalId}";
}
public override string ToString()
{
return $"{id} Manga: {manga}";
}
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal(JobBoss jobBoss)
{
//Retrieve new Metadata
Manga? possibleUpdatedManga = mangaConnector.GetMangaFromId(manga.publicationId);
if (possibleUpdatedManga is { } updatedManga)
{
if (updatedManga.Equals(this.manga)) //Check if anything changed
{
this.progressToken.Complete();
return Array.Empty<Job>();
}
this.manga = manga.WithMetadata(updatedManga);
this.manga.SaveSeriesInfoJson(true);
this.mangaConnector.CopyCoverFromCacheToDownloadLocation(manga);
foreach (Job job in jobBoss.GetJobsLike(publication: this.manga))
{
string oldFile;
if (job is DownloadNewChapters dc)
{
oldFile = dc.id;
dc.manga = this.manga;
}
else if (job is UpdateMetadata um)
{
oldFile = um.id;
um.manga = this.manga;
}
else
continue;
jobBoss.UpdateJobFile(job, oldFile);
}
this.progressToken.Complete();
}
else
{
Log($"Could not find Manga {manga}");
this.progressToken.Cancel();
return Array.Empty<Job>();
}
this.progressToken.Cancel();
return Array.Empty<Job>();
}
public override bool Equals(object? obj)
{
if (obj is not UpdateMetadata otherJob)
return false;
return otherJob.mangaConnector == this.mangaConnector &&
otherJob.manga.publicationId == this.manga.publicationId;
}
}

View File

@ -1,4 +1,5 @@
using System.Text.Json.Nodes;
using Logging;
using Newtonsoft.Json;
using JsonSerializer = System.Text.Json.JsonSerializer;
@ -7,17 +8,22 @@ namespace Tranga.LibraryConnectors;
public class Kavita : LibraryConnector
{
public Kavita(string baseUrl, string username, string password, GlobalBase clone) :
base(baseUrl, GetToken(baseUrl, username, password), LibraryType.Kavita, clone)
public Kavita(GlobalBase clone, string baseUrl, string username, string password) :
base(clone, baseUrl, GetToken(baseUrl, username, password, clone.logger), LibraryType.Kavita)
{
}
[JsonConstructor]
public Kavita(string baseUrl, string auth, GlobalBase clone) : base(baseUrl, auth, LibraryType.Kavita, clone)
public Kavita(GlobalBase clone, string baseUrl, string auth) : base(clone, baseUrl, auth, LibraryType.Kavita)
{
}
private static string GetToken(string baseUrl, string username, string password)
public override string ToString()
{
return $"Kavita {baseUrl}";
}
private static string GetToken(string baseUrl, string username, string password, Logger? logger = null)
{
HttpClient client = new()
{
@ -32,21 +38,44 @@ public class Kavita : LibraryConnector
RequestUri = new Uri($"{baseUrl}/api/Account/login"),
Content = new StringContent($"{{\"username\":\"{username}\",\"password\":\"{password}\"}}", System.Text.Encoding.UTF8, "application/json")
};
HttpResponseMessage response = client.Send(requestMessage);
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(response.Content.ReadAsStream());
if (result is not null)
return result["token"]!.GetValue<string>();
else return "";
try
{
HttpResponseMessage response = client.Send(requestMessage);
logger?.WriteLine($"Kavita | GetToken {requestMessage.RequestUri} -> {response.StatusCode}");
if (response.IsSuccessStatusCode)
{
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(response.Content.ReadAsStream());
if (result is not null)
return result["token"]!.GetValue<string>();
}
else
{
logger?.WriteLine($"Kavita | {response.Content}");
}
}
catch (HttpRequestException e)
{
logger?.WriteLine($"Kavita | Unable to retrieve token:\n\r{e}");
}
logger?.WriteLine("Kavita | Did not receive token.");
return "";
}
public override void UpdateLibrary()
protected override void UpdateLibraryInternal()
{
Log("Updating libraries.");
foreach (KavitaLibrary lib in GetLibraries())
NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger);
}
internal override bool Test()
{
foreach (KavitaLibrary lib in GetLibraries())
if (NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger))
return true;
return false;
}
/// <summary>
/// Fetches all libraries available to the user
/// </summary>
@ -54,7 +83,7 @@ public class Kavita : LibraryConnector
private IEnumerable<KavitaLibrary> GetLibraries()
{
Log("Getting libraries.");
Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library", "Bearer", auth, logger);
Stream data = NetClient.MakeRequest($"{baseUrl}/api/Library/libraries", "Bearer", auth, logger);
if (data == Stream.Null)
{
Log("No libraries returned");
@ -67,11 +96,13 @@ public class Kavita : LibraryConnector
return Array.Empty<KavitaLibrary>();
}
HashSet<KavitaLibrary> ret = new();
List<KavitaLibrary> ret = new();
foreach (JsonNode? jsonNode in result)
{
var jObject = (JsonObject?)jsonNode;
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));

View File

@ -10,23 +10,36 @@ namespace Tranga.LibraryConnectors;
/// </summary>
public class Komga : LibraryConnector
{
public Komga(string baseUrl, string username, string password, GlobalBase clone)
: base(baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), LibraryType.Komga, clone)
public Komga(GlobalBase clone, string baseUrl, string username, string password)
: base(clone, baseUrl, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{username}:{password}")), LibraryType.Komga)
{
}
[JsonConstructor]
public Komga(string baseUrl, string auth, GlobalBase clone) : base(baseUrl, auth, LibraryType.Komga, clone)
public Komga(GlobalBase clone, string baseUrl, string auth) : base(clone, baseUrl, auth, LibraryType.Komga)
{
}
public override void UpdateLibrary()
public override string ToString()
{
return $"Komga {baseUrl}";
}
protected override void UpdateLibraryInternal()
{
Log("Updating libraries.");
foreach (KomgaLibrary lib in GetLibraries())
NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger);
}
internal override bool Test()
{
foreach (KomgaLibrary lib in GetLibraries())
if (NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger))
return true;
return false;
}
/// <summary>
/// Fetches all libraries available to the user
/// </summary>

View File

@ -17,14 +17,62 @@ public abstract class LibraryConnector : GlobalBase
public string baseUrl { get; }
// ReSharper disable once MemberCanBeProtected.Global
public string auth { get; } //Base64 encoded, if you use your password everywhere, you have problems
private DateTime? _updateLibraryRequested = null;
private readonly Thread? _libraryBufferThread = null;
private const int NoChangeTimeout = 2, BiggestInterval = 20;
protected LibraryConnector(string baseUrl, string auth, LibraryType libraryType, GlobalBase clone) : base(clone)
protected LibraryConnector(GlobalBase clone, string baseUrl, string auth, LibraryType libraryType) : base(clone)
{
this.baseUrl = baseUrl;
Log($"Creating libraryConnector {Enum.GetName(libraryType)}");
if (!baseUrlRex.IsMatch(baseUrl))
throw new ArgumentException("Base url does not match pattern");
if(auth == "")
throw new ArgumentNullException(nameof(auth), "Auth can not be empty");
this.baseUrl = baseUrlRex.Match(baseUrl).Value;
this.auth = auth;
this.libraryType = libraryType;
if (TrangaSettings.bufferLibraryUpdates)
{
_libraryBufferThread = new(CheckLibraryBuffer);
_libraryBufferThread.Start();
}
}
public abstract void UpdateLibrary();
private void CheckLibraryBuffer()
{
while (true)
{
if (_updateLibraryRequested is not null && DateTime.Now.Subtract((DateTime)_updateLibraryRequested) > TimeSpan.FromMinutes(NoChangeTimeout)) //If no updates have been requested for NoChangeTimeout minutes, update library
{
UpdateLibraryInternal();
_updateLibraryRequested = null;
}
Thread.Sleep(100);
}
}
public void UpdateLibrary()
{
_updateLibraryRequested ??= DateTime.Now;
if (!TrangaSettings.bufferLibraryUpdates)
{
UpdateLibraryInternal();
return;
}else if (_updateLibraryRequested is not null &&
DateTime.Now.Subtract((DateTime)_updateLibraryRequested) > TimeSpan.FromMinutes(BiggestInterval)) //If the last update has been more than BiggestInterval minutes ago, update library
{
UpdateLibraryInternal();
_updateLibraryRequested = null;
}
else if(_updateLibraryRequested is not null)
{
Log($"Buffering Library Updates (Updates in latest {((DateTime)_updateLibraryRequested).Add(TimeSpan.FromMinutes(BiggestInterval)).Subtract(DateTime.Now)} or {((DateTime)_updateLibraryRequested).Add(TimeSpan.FromMinutes(NoChangeTimeout)).Subtract(DateTime.Now)})");
}
}
protected abstract void UpdateLibraryInternal();
internal abstract bool Test();
protected static class NetClient
{
@ -38,15 +86,33 @@ public abstract class LibraryConnector : GlobalBase
Method = HttpMethod.Get,
RequestUri = new Uri(url)
};
HttpResponseMessage response = client.Send(requestMessage);
logger?.WriteLine("LibraryManager.NetClient", $"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
if(response.StatusCode is HttpStatusCode.Unauthorized && response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
else if (response.IsSuccessStatusCode)
return response.Content.ReadAsStream();
else
try
{
HttpResponseMessage response = client.Send(requestMessage);
logger?.WriteLine("LibraryManager.NetClient",
$"GET {url} -> {(int)response.StatusCode}: {response.ReasonPhrase}");
if (response.StatusCode is HttpStatusCode.Unauthorized &&
response.RequestMessage!.RequestUri!.AbsoluteUri != url)
return MakeRequest(response.RequestMessage!.RequestUri!.AbsoluteUri, authScheme, auth, logger);
else if (response.IsSuccessStatusCode)
return response.Content.ReadAsStream();
else
return Stream.Null;
}
catch (Exception e)
{
switch (e)
{
case HttpRequestException:
logger?.WriteLine("LibraryManager.NetClient", $"Failed to make Request:\n\r{e}\n\rContinuing.");
break;
default:
throw;
}
return Stream.Null;
}
}
public static bool MakePost(string url, string authScheme, string auth, Logger? logger)

View File

@ -5,6 +5,13 @@ namespace Tranga.LibraryConnectors;
public class LibraryManagerJsonConverter : JsonConverter
{
private readonly GlobalBase _clone;
internal LibraryManagerJsonConverter(GlobalBase clone)
{
this._clone = clone;
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(LibraryConnector));
@ -13,11 +20,15 @@ public class LibraryManagerJsonConverter : JsonConverter
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
if (jo["libraryType"]!.Value<Int64>() == (Int64)LibraryConnector.LibraryType.Komga)
return jo.ToObject<Komga>(serializer)!;
if (jo["libraryType"]!.Value<byte>() == (byte)LibraryConnector.LibraryType.Komga)
return new Komga(this._clone,
jo.GetValue("baseUrl")!.Value<string>()!,
jo.GetValue("auth")!.Value<string>()!);
if (jo["libraryType"]!.Value<Int64>() == (Int64)LibraryConnector.LibraryType.Kavita)
return jo.ToObject<Kavita>(serializer)!;
if (jo["libraryType"]!.Value<byte>() == (byte)LibraryConnector.LibraryType.Kavita)
return new Kavita(this._clone,
jo.GetValue("baseUrl")!.Value<string>()!,
jo.GetValue("auth")!.Value<string>()!);
throw new Exception();
}

222
Tranga/Manga.cs Normal file
View File

@ -0,0 +1,222 @@
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Newtonsoft.Json;
using static System.IO.UnixFileMode;
namespace Tranga;
/// <summary>
/// Contains information on a Publication (Manga)
/// </summary>
public struct Manga
{
public string sortName { get; private set; }
public List<string> authors { get; private set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public Dictionary<string,string> altTitles { get; private set; }
// ReSharper disable once MemberCanBePrivate.Global
public string? description { get; private set; }
public string[] tags { get; private set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public string? coverUrl { get; private set; }
public string? coverFileNameInCache { get; private set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public Dictionary<string,string> links { get; }
// ReSharper disable once MemberCanBePrivate.Global
public int? year { get; private set; }
public string? originalLanguage { get; }
// ReSharper disable twice MemberCanBePrivate.Global
public string status { get; private set; }
public ReleaseStatusByte releaseStatus { get; private set; }
public enum ReleaseStatusByte : byte
{
Continuing = 0,
Completed = 1,
OnHiatus = 2,
Cancelled = 3,
Unreleased = 4
};
public string folderName { get; private set; }
public string publicationId { get; }
public string internalId { get; }
public float ignoreChaptersBelow { get; set; }
public float latestChapterDownloaded { get; set; }
public float latestChapterAvailable { get; set; }
public string? websiteUrl { get; private set; }
private static readonly Regex LegalCharacters = new (@"[A-Za-zÀ-ÖØ-öø-ÿ0-9 \.\-,'\'\)\(~!\+]*");
[JsonConstructor]
public Manga(string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string publicationId, ReleaseStatusByte releaseStatus, string? websiteUrl = null, string? folderName = null, float? ignoreChaptersBelow = 0)
{
this.sortName = HttpUtility.HtmlDecode(sortName);
this.authors = authors.Select(HttpUtility.HtmlDecode).ToList()!;
this.description = HttpUtility.HtmlDecode(description);
this.altTitles = altTitles.ToDictionary(a => HttpUtility.HtmlDecode(a.Key), a => HttpUtility.HtmlDecode(a.Value));
this.tags = tags.Select(HttpUtility.HtmlDecode).ToArray()!;
this.coverFileNameInCache = coverFileNameInCache;
this.coverUrl = coverUrl;
this.links = links ?? new Dictionary<string, string>();
this.year = year;
this.originalLanguage = originalLanguage;
this.publicationId = publicationId;
this.folderName = folderName ?? string.Concat(LegalCharacters.Matches(HttpUtility.HtmlDecode(sortName)));
while (this.folderName.EndsWith('.'))
this.folderName = this.folderName.Substring(0, this.folderName.Length - 1);
string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter));
this.internalId = DateTime.Now.Ticks.ToString();
this.ignoreChaptersBelow = ignoreChaptersBelow ?? 0f;
this.latestChapterDownloaded = 0;
this.latestChapterAvailable = 0;
this.releaseStatus = releaseStatus;
this.status = Enum.GetName(releaseStatus) ?? "";
this.websiteUrl = websiteUrl;
}
public Manga WithMetadata(Manga newManga)
{
return this with
{
sortName = newManga.sortName,
description = newManga.description,
coverUrl = newManga.coverUrl,
authors = authors.Union(newManga.authors).ToList(),
altTitles = altTitles.UnionBy(newManga.altTitles, kv => kv.Key).ToDictionary(x => x.Key, x => x.Value),
tags = tags.Union(newManga.tags).ToArray(),
status = newManga.status,
releaseStatus = newManga.releaseStatus,
websiteUrl = newManga.websiteUrl,
year = newManga.year,
coverFileNameInCache = newManga.coverFileNameInCache
};
}
public override bool Equals(object? obj)
{
if (obj is not Manga compareManga)
return false;
return this.description == compareManga.description &&
this.year == compareManga.year &&
this.status == compareManga.status &&
this.releaseStatus == compareManga.releaseStatus &&
this.sortName == compareManga.sortName &&
this.latestChapterAvailable.Equals(compareManga.latestChapterAvailable) &&
this.authors.All(a => compareManga.authors.Contains(a)) &&
(this.coverFileNameInCache??"").Equals(compareManga.coverFileNameInCache) &&
(this.websiteUrl??"").Equals(compareManga.websiteUrl) &&
this.tags.All(t => compareManga.tags.Contains(t));
}
public override string ToString()
{
return $"Publication {sortName} {internalId}";
}
public string CreatePublicationFolder(string downloadDirectory)
{
string publicationFolder = Path.Join(downloadDirectory, this.folderName);
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;
}
public void MovePublicationFolder(string downloadDirectory, string newFolderName)
{
string oldPath = Path.Join(downloadDirectory, this.folderName);
this.folderName = newFolderName;//Create new Path with the new folderName
string newPath = CreatePublicationFolder(downloadDirectory);
if (Directory.Exists(oldPath))
{
if (Directory.Exists(newPath)) //Move/Overwrite old Files, Delete old Directory
{
IEnumerable<string> newPathFileNames = new DirectoryInfo(newPath).GetFiles().Select(fi => fi.Name);
foreach(FileInfo fileInfo in new DirectoryInfo(oldPath).GetFiles().Where(fi => newPathFileNames.Contains(fi.Name) == false))
File.Move(fileInfo.FullName, Path.Join(newPath, fileInfo.Name), true);
Directory.Delete(oldPath);
}else
Directory.Move(oldPath, newPath);
}
}
public void UpdateLatestDownloadedChapter(Chapter chapter)//TODO check files if chapters are all downloaded
{
float chapterNumber = Convert.ToSingle(chapter.chapterNumber, GlobalBase.numberFormatDecimalPoint);
latestChapterDownloaded = latestChapterDownloaded < chapterNumber ? chapterNumber : latestChapterDownloaded;
}
public void SaveSeriesInfoJson(bool overwrite = false)
{
string publicationFolder = CreatePublicationFolder(TrangaSettings.downloadLocation);
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
if(overwrite || (!overwrite && !File.Exists(seriesInfoPath)))
File.WriteAllText(seriesInfoPath,this.GetSeriesInfoJson());
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(seriesInfoPath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
}
/// <returns>Serialized JSON String for series.json</returns>
private string GetSeriesInfoJson()
{
SeriesInfo si = new (new Metadata(this));
return System.Text.Json.JsonSerializer.Serialize(si);
}
//Only for series.json
private struct SeriesInfo
{
// ReSharper disable once UnusedAutoPropertyAccessor.Local we need it, trust
[JsonRequired]public Metadata metadata { get; }
public SeriesInfo(Metadata metadata) => this.metadata = metadata;
}
//Only for series.json what an abomination, why are all the fields not-null????
private struct Metadata
{
// ReSharper disable UnusedAutoPropertyAccessor.Local we need them all, trust me
[JsonRequired] public string type { get; }
[JsonRequired] public string publisher { get; }
// ReSharper disable twice IdentifierTypo
[JsonRequired] public int comicid { get; }
[JsonRequired] public string booktype { get; }
// ReSharper disable InconsistentNaming This one property is capitalized. Why?
[JsonRequired] public string ComicImage { get; }
[JsonRequired] public int total_issues { get; }
[JsonRequired] public string publication_run { get; }
[JsonRequired]public string name { get; }
[JsonRequired]public string year { get; }
[JsonRequired]public string status { get; }
[JsonRequired]public string description_text { get; }
public Metadata(Manga manga) : this(manga.sortName, manga.year.ToString() ?? string.Empty, manga.releaseStatus, manga.description ?? "")
{
}
public Metadata(string name, string year, ReleaseStatusByte status, string description_text)
{
this.name = name;
this.year = year;
this.status = status switch
{
ReleaseStatusByte.Continuing => "Continuing",
ReleaseStatusByte.Completed => "Ended",
_ => Enum.GetName(status) ?? "Ended"
};
this.description_text = description_text;
//kill it with fire, but otherwise Komga will not parse
type = "Manga";
publisher = "";
comicid = 0;
booktype = "";
ComicImage = "";
total_issues = 0;
publication_run = "";
}
}
}

View File

@ -0,0 +1,217 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.Jobs;
namespace Tranga.MangaConnectors;
public class AsuraToon : MangaConnector
{
public AsuraToon(GlobalBase clone) : base(clone, "AsuraToon", ["en"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
public override Manga[] GetManga(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
string requestUrl = $"https://asuracomic.net/series?name={sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Manga>();
if (requestResult.htmlDocument is null)
{
Log($"Failed to retrieve site");
return Array.Empty<Manga>();
}
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
public override Manga? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://asuracomic.net/series/{publicationId}");
}
public override Manga? GetMangaFromUrl(string url)
{
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
if (requestResult.htmlDocument is null)
{
Log($"Failed to retrieve site");
return null;
}
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
}
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
{
HtmlNodeCollection mangaList = document.DocumentNode.SelectNodes("//a[starts-with(@href,'series')]");
if (mangaList is null || mangaList.Count < 1)
return [];
IEnumerable<string> urls = mangaList.Select(a => $"https://asuracomic.net/{a.GetAttributeValue("href", "")}");
List<Manga> ret = new();
foreach (string url in urls)
{
Manga? manga = GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
}
return ret.ToArray();
}
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
string? originalLanguage = null;
Dictionary<string, string> altTitles = new(), links = new();
HtmlNodeCollection genreNodes = document.DocumentNode.SelectNodes("//h3[text()='Genres']/../div/button");
string[] tags = genreNodes.Select(b => b.InnerText).ToArray();
HtmlNode statusNode = document.DocumentNode.SelectSingleNode("//h3[text()='Status']/../h3[2]");
Manga.ReleaseStatusByte releaseStatus = statusNode.InnerText.ToLower() switch
{
"ongoing" => Manga.ReleaseStatusByte.Continuing,
"hiatus" => Manga.ReleaseStatusByte.OnHiatus,
"completed" => Manga.ReleaseStatusByte.Completed,
"dropped" => Manga.ReleaseStatusByte.Cancelled,
"season end" => Manga.ReleaseStatusByte.Continuing,
"coming soon" => Manga.ReleaseStatusByte.Unreleased,
_ => Manga.ReleaseStatusByte.Unreleased
};
HtmlNode coverNode =
document.DocumentNode.SelectSingleNode("//img[@alt='poster']");
string coverUrl = coverNode.GetAttributeValue("src", "");
string coverFileNameInCache = SaveCoverImageToCache(coverUrl, publicationId, RequestType.MangaCover);
HtmlNode titleNode =
document.DocumentNode.SelectSingleNode("//title");
string sortName = Regex.Match(titleNode.InnerText, @"(.*) - Asura Scans").Groups[1].Value;
HtmlNode descriptionNode =
document.DocumentNode.SelectSingleNode("//h3[starts-with(text(),'Synopsis')]/../span");
string description = descriptionNode?.InnerText??"";
HtmlNodeCollection authorNodes = document.DocumentNode.SelectNodes("//h3[text()='Author']/../h3[not(text()='Author' or text()='_')]");
HtmlNodeCollection artistNodes = document.DocumentNode.SelectNodes("//h3[text()='Artist']/../h3[not(text()='Artist' or text()='_')]");
IEnumerable<string> authorNames = authorNodes is null ? [] : authorNodes.Select(a => a.InnerText);
IEnumerable<string> artistNames = artistNodes is null ? [] : artistNodes.Select(a => a.InnerText);
List<string> authors = authorNames.Concat(artistNames).ToList();
HtmlNode? firstChapterNode = document.DocumentNode.SelectSingleNode("//a[contains(@href, 'chapter/1')]/../following-sibling::h3");
int? year = int.Parse(firstChapterNode?.InnerText.Split(' ')[^1] ?? "2000");
Manga manga = new (sortName, authors, description, altTitles, tags, coverUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
Log($"Getting chapters {manga}");
string requestUrl = $"https://asuracomic.net/series/{manga.publicationId}";
// Leaving this in for verification if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>();
//Return Chapters ordered by Chapter-Number
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
Log($"Got {chapters.Count} chapters. {manga}");
return chapters.Order().ToArray();
}
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
{
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
{
Log("Failed to load site");
return new List<Chapter>();
}
List<Chapter> ret = new();
HtmlNodeCollection chapterURLNodes = result.htmlDocument.DocumentNode.SelectNodes("//a[contains(@href, '/chapter/')]");
Regex infoRex = new(@"Chapter ([0-9]+)(.*)?");
foreach (HtmlNode chapterInfo in chapterURLNodes)
{
string chapterUrl = chapterInfo.GetAttributeValue("href", "");
Match match = infoRex.Match(chapterInfo.InnerText);
string chapterNumber = match.Groups[1].Value;
string? chapterName = match.Groups[2].Success && match.Groups[2].Length > 1 ? match.Groups[2].Value : null;
string url = $"https://asuracomic.net/series/{chapterUrl}";
try
{
ret.Add(new Chapter(manga, chapterName, null, chapterNumber, url));
}
catch (Exception e)
{
Log($"Failed to load chapter {chapterNumber}: {e.Message}");
}
}
return ret;
}
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Manga chapterParentManga = chapter.parentManga;
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
string requestUrl = chapter.url;
// Leaving this in to check if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
progressToken?.Cancel();
return requestResult.statusCode;
}
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(string mangaUrl)
{
RequestResult requestResult =
downloadClient.MakeRequest(mangaUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
return Array.Empty<string>();
}
if (requestResult.htmlDocument is null)
{
Log($"Failed to retrieve site");
return Array.Empty<string>();
}
HtmlNodeCollection images =
requestResult.htmlDocument.DocumentNode.SelectNodes("//img[contains(@alt, 'chapter page')]");
return images.Select(i => i.GetAttributeValue("src", "")).ToArray();
}
}

View File

@ -0,0 +1,230 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.Jobs;
namespace Tranga.MangaConnectors;
public class Bato : MangaConnector
{
public Bato(GlobalBase clone) : base(clone, "Bato", ["en"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
public override Manga[] GetManga(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
string requestUrl = $"https://bato.to/v3x-search?word={sanitizedTitle}&lang=en";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Manga>();
if (requestResult.htmlDocument is null)
{
Log($"Failed to retrieve site");
return Array.Empty<Manga>();
}
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
public override Manga? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://bato.to/title/{publicationId}");
}
public override Manga? GetMangaFromUrl(string url)
{
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
if (requestResult.htmlDocument is null)
{
Log($"Failed to retrieve site");
return null;
}
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
}
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
{
HtmlNode mangaList = document.DocumentNode.SelectSingleNode("//div[@data-hk='0-0-2']");
if (!mangaList.ChildNodes.Any(node => node.Name == "div"))
return Array.Empty<Manga>();
List<string> urls = mangaList.ChildNodes
.Select(node => $"https://bato.to{node.Descendants("div").First().FirstChild.GetAttributeValue("href", "")}").ToList();
HashSet<Manga> ret = new();
foreach (string url in urls)
{
Manga? manga = GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
}
return ret.ToArray();
}
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
HtmlNode infoNode = document.DocumentNode.SelectSingleNode("/html/body/div/main/div[1]/div[2]");
string sortName = infoNode.Descendants("h3").First().InnerText;
string description = document.DocumentNode
.SelectSingleNode("//div[contains(concat(' ',normalize-space(@class),' '),'prose')]").InnerText;
string[] altTitlesList = infoNode.ChildNodes[1].ChildNodes[2].InnerText.Split('/');
int i = 0;
Dictionary<string, string> altTitles = altTitlesList.ToDictionary(s => i++.ToString(), s => s);
string posterUrl = document.DocumentNode.SelectNodes("//img")
.First(child => child.GetAttributeValue("data-hk", "") == "0-1-0").GetAttributeValue("src", "").Replace("&amp;", "&");
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover);
List<HtmlNode> genreNodes = document.DocumentNode.SelectSingleNode("//b[text()='Genres:']/..").SelectNodes("span").ToList();
string[] tags = genreNodes.Select(node => node.FirstChild.InnerText).ToArray();
List<HtmlNode> authorsNodes = infoNode.ChildNodes[1].ChildNodes[3].Descendants("a").ToList();
List<string> authors = authorsNodes.Select(node => node.InnerText.Replace("amp;", "")).ToList();
HtmlNode? originalLanguageNode = document.DocumentNode.SelectSingleNode("//span[text()='Tr From']/..");
string originalLanguage = originalLanguageNode is not null ? originalLanguageNode.LastChild.InnerText : "";
if (!int.TryParse(
document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..").LastChild.InnerText.Split('-')[0],
out int year))
year = DateTime.Now.Year;
string status = document.DocumentNode.SelectSingleNode("//span[text()='Original Publication:']/..")
.ChildNodes[2].InnerText;
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
switch (status.ToLower())
{
case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break;
case "completed": releaseStatus = Manga.ReleaseStatusByte.Completed; break;
case "hiatus": releaseStatus = Manga.ReleaseStatusByte.OnHiatus; break;
case "cancelled": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
case "pending": releaseStatus = Manga.ReleaseStatusByte.Unreleased; break;
}
Manga manga = new (sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary<string, string>(),
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
return manga;
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
Log($"Getting chapters {manga}");
string requestUrl = $"https://bato.to/title/{manga.publicationId}";
// Leaving this in for verification if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>();
//Return Chapters ordered by Chapter-Number
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
Log($"Got {chapters.Count} chapters. {manga}");
return chapters.Order().ToArray();
}
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
{
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
{
Log("Failed to load site");
return new List<Chapter>();
}
List<Chapter> ret = new();
HtmlNode chapterList =
result.htmlDocument.DocumentNode.SelectSingleNode("/html/body/div/main/div[3]/astro-island/div/div[2]/div/div/astro-slot");
Regex numberRex = new(@"\/title\/.+\/([0-9])+(?:-vol_([0-9]+))?-ch_([0-9\.]+)");
foreach (HtmlNode chapterInfo in chapterList.SelectNodes("div"))
{
HtmlNode infoNode = chapterInfo.FirstChild.FirstChild;
string chapterUrl = infoNode.GetAttributeValue("href", "");
Match match = numberRex.Match(chapterUrl);
string id = match.Groups[1].Value;
string? volumeNumber = match.Groups[2].Success ? match.Groups[2].Value : null;
string chapterNumber = match.Groups[3].Value;
string chapterName = chapterNumber;
string url = $"https://bato.to{chapterUrl}?load=2";
try
{
ret.Add(new Chapter(manga, chapterName, volumeNumber, chapterNumber, url));
}
catch (Exception e)
{
Log($"Failed to load chapter {chapterNumber}: {e.Message}");
}
}
return ret;
}
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Manga chapterParentManga = chapter.parentManga;
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
string requestUrl = chapter.url;
// Leaving this in to check if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
progressToken?.Cancel();
return requestResult.statusCode;
}
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(string mangaUrl)
{
RequestResult requestResult =
downloadClient.MakeRequest(mangaUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
return Array.Empty<string>();
}
if (requestResult.htmlDocument is null)
{
Log($"Failed to retrieve site");
return Array.Empty<string>();
}
HtmlDocument document = requestResult.htmlDocument;
HtmlNode images = document.DocumentNode.SelectNodes("//astro-island").First(node =>
node.GetAttributeValue("component-url", "").Contains("/_astro/ImageList."));
string weirdString = images.OuterHtml;
string weirdString2 = Regex.Match(weirdString, @"props=\""(.*)}\""").Groups[1].Value;
string[] urls = Regex.Matches(weirdString2, @"(https:\/\/[A-z\-0-9\.\?\&\;\=\/]+)\\")
.Select(match => match.Groups[1].Value.Replace("&amp;", "&")).ToArray();
return urls;
}
}

View File

@ -0,0 +1,117 @@
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Microsoft.Extensions.Logging;
using PuppeteerSharp;
namespace Tranga.MangaConnectors;
internal class ChromiumDownloadClient : DownloadClient
{
private static IBrowser? _browser;
private readonly HttpDownloadClient _httpDownloadClient;
private static async Task<IBrowser> StartBrowser(Logging.Logger? logger = null)
{
logger?.WriteLine("Starting ChromiumDownloadClient Puppeteer");
return await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true,
Args = new [] {
"--disable-gpu",
"--disable-dev-shm-usage",
"--disable-setuid-sandbox",
"--no-sandbox"},
Timeout = TrangaSettings.ChromiumStartupTimeoutMs
}, new LoggerFactory([new LogProvider(logger)]));
}
private class LogProvider : GlobalBase, ILoggerProvider
{
public LogProvider(Logging.Logger? logger) : base(logger) { }
public void Dispose() { }
public ILogger CreateLogger(string categoryName) => new Logger(logger);
}
private class Logger : GlobalBase, ILogger
{
public Logger(Logging.Logger? logger) : base(logger) { }
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (logLevel <= LogLevel.Information)
return;
logger?.WriteLine("Puppeteer", formatter.Invoke(state, exception));
}
public bool IsEnabled(LogLevel logLevel) => true;
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
}
public ChromiumDownloadClient(GlobalBase clone) : base(clone)
{
_httpDownloadClient = new(this);
if(_browser is null)
_browser = StartBrowser(this.logger).Result;
}
private readonly Regex _imageUrlRex = new(@"https?:\/\/.*\.(?:p?jpe?g|gif|a?png|bmp|avif|webp)(\?.*)?");
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
{
return _imageUrlRex.IsMatch(url)
? _httpDownloadClient.MakeRequestInternal(url, referrer)
: MakeRequestBrowser(url, referrer, clickButton);
}
private RequestResult MakeRequestBrowser(string url, string? referrer = null, string? clickButton = null)
{
if (_browser is null)
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
IPage page = _browser.NewPageAsync().Result;
page.DefaultTimeout = TrangaSettings.ChromiumPageTimeoutMs;
page.SetExtraHttpHeadersAsync(new() { { "Referer", referrer } });
IResponse response;
try
{
response = page.GoToAsync(url, WaitUntilNavigation.Networkidle0).Result;
Log($"Page loaded. {url}");
}
catch (Exception e)
{
Log($"Could not load Page {url}\n{e.Message}");
page.CloseAsync();
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
}
Stream stream = Stream.Null;
HtmlDocument? document = null;
if (response.Headers.TryGetValue("Content-Type", out string? content))
{
if (content.Contains("text/html"))
{
if (clickButton is not null && page.QuerySelectorAsync(clickButton).Result is not null)
page.ClickAsync(clickButton).Wait();
string htmlString = page.GetContentAsync().Result;
stream = new MemoryStream(Encoding.Default.GetBytes(htmlString));
document = new ();
document.LoadHtml(htmlString);
}else if (content.Contains("image"))
{
stream = new MemoryStream(response.BufferAsync().Result);
}
}
else
{
page.CloseAsync();
return new RequestResult(HttpStatusCode.InternalServerError, null, Stream.Null);
}
page.CloseAsync();
return new RequestResult(response.Status, document, stream, false, "");
}
}

View File

@ -1,106 +1,44 @@
using System.Net;
using System.Net.Http.Headers;
using HtmlAgilityPack;
namespace Tranga.MangaConnectors;
internal class DownloadClient : GlobalBase
internal abstract class DownloadClient : GlobalBase
{
private readonly Dictionary<RequestType, DateTime> _lastExecutedRateLimit;
protected DownloadClient(GlobalBase clone) : base(clone)
{
private static readonly HttpClient Client = new()
this._lastExecutedRateLimit = new();
}
public RequestResult MakeRequest(string url, RequestType requestType, string? referrer = null, string? clickButton = null)
{
if (!TrangaSettings.requestLimits.ContainsKey(requestType))
{
Timeout = TimeSpan.FromSeconds(60),
DefaultRequestHeaders =
{
UserAgent =
{
new ProductInfoHeaderValue("Tranga", "0.1")
}
}
};
private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit;
private readonly Dictionary<byte, TimeSpan> _rateLimit;
public DownloadClient(Dictionary<byte, int> rateLimitRequestsPerMinute, GlobalBase clone) : base(clone)
{
_lastExecutedRateLimit = new();
_rateLimit = new();
foreach(KeyValuePair<byte, int> limit in rateLimitRequestsPerMinute)
_rateLimit.Add(limit.Key, TimeSpan.FromMinutes(1).Divide(limit.Value));
Log("RequestType not configured for rate-limit.");
return new RequestResult(HttpStatusCode.NotAcceptable, null, Stream.Null);
}
/// <summary>
/// Request Webpage
/// </summary>
/// <param name="url"></param>
/// <param name="requestType">For RateLimits: Same Endpoints use same type</param>
/// <param name="referrer">Used in http request header</param>
/// <returns>RequestResult with StatusCode and Stream of received data</returns>
public RequestResult MakeRequest(string url, byte requestType, string? referrer = null)
int rateLimit = TrangaSettings.userAgent == TrangaSettings.DefaultUserAgent
? TrangaSettings.DefaultRequestLimits[requestType]
: TrangaSettings.requestLimits[requestType];
TimeSpan timeBetweenRequests = TimeSpan.FromMinutes(1).Divide(rateLimit);
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(timeBetweenRequests));
TimeSpan rateLimitTimeout = timeBetweenRequests.Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType]));
if (rateLimitTimeout > TimeSpan.Zero)
{
if (_rateLimit.TryGetValue(requestType, out TimeSpan value))
_lastExecutedRateLimit.TryAdd(requestType, DateTime.Now.Subtract(value));
else
{
Log("RequestType not configured for rate-limit.");
return new RequestResult(HttpStatusCode.NotAcceptable, Stream.Null);
}
TimeSpan rateLimitTimeout = _rateLimit[requestType]
.Subtract(DateTime.Now.Subtract(_lastExecutedRateLimit[requestType]));
if(rateLimitTimeout > TimeSpan.Zero)
Thread.Sleep(rateLimitTimeout);
HttpResponseMessage? response = null;
while (response is null)
{
try
{
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
if(referrer is not null)
requestMessage.Headers.Referrer = new Uri(referrer);
_lastExecutedRateLimit[requestType] = DateTime.Now;
response = Client.Send(requestMessage);
}
catch (HttpRequestException e)
{
Log("Exception:\n\t{0}\n\tWaiting {1} before retrying.", e.Message, _rateLimit[requestType] * 2);
Thread.Sleep(_rateLimit[requestType] * 2);
}
}
if (!response.IsSuccessStatusCode)
{
Log($"Request-Error {response.StatusCode}: {response.ReasonPhrase}");
return new RequestResult(response.StatusCode, Stream.Null);
}
// 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)
{
return new RequestResult(response.StatusCode, response.Content.ReadAsStream(), true, response.RequestMessage.RequestUri.AbsoluteUri);
}
return new RequestResult(response.StatusCode, response.Content.ReadAsStream());
Log($"Waiting {rateLimitTimeout.TotalSeconds} seconds");
Thread.Sleep(rateLimitTimeout);
}
public struct RequestResult
{
public HttpStatusCode statusCode { get; }
public Stream result { get; }
public bool hasBeenRedirected { get; }
public string? redirectedToUrl { get; }
RequestResult result = MakeRequestInternal(url, referrer, clickButton);
_lastExecutedRateLimit[requestType] = DateTime.Now;
return result;
}
public RequestResult(HttpStatusCode statusCode, Stream result)
{
this.statusCode = statusCode;
this.result = result;
}
public RequestResult(HttpStatusCode statusCode, Stream result, bool hasBeenRedirected, string redirectedTo)
: this(statusCode, result)
{
this.hasBeenRedirected = hasBeenRedirected;
redirectedToUrl = redirectedTo;
}
}
}
internal abstract RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null);
}

View File

@ -0,0 +1,75 @@
using System.Net;
using System.Net.Http.Headers;
using HtmlAgilityPack;
namespace Tranga.MangaConnectors;
internal class HttpDownloadClient : DownloadClient
{
private static readonly HttpClient Client = new()
{
Timeout = TimeSpan.FromSeconds(10)
};
public HttpDownloadClient(GlobalBase clone) : base(clone)
{
Client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", TrangaSettings.userAgent);
}
internal override RequestResult MakeRequestInternal(string url, string? referrer = null, string? clickButton = null)
{
if(clickButton is not null)
Log("Can not click button on static site.");
HttpResponseMessage? response = null;
while (response is null)
{
HttpRequestMessage requestMessage = new(HttpMethod.Get, url);
if (referrer is not null)
requestMessage.Headers.Referrer = new Uri(referrer);
//Log($"Requesting {requestType} {url}");
try
{
response = Client.Send(requestMessage);
}
catch (Exception e)
{
switch (e)
{
case TaskCanceledException:
Log($"Request timed out {url}.\n\r{e}");
return new RequestResult(HttpStatusCode.RequestTimeout, null, Stream.Null);
case HttpRequestException:
Log($"Request failed {url}\n\r{e}");
return new RequestResult(HttpStatusCode.BadRequest, null, Stream.Null);
}
}
}
if (!response.IsSuccessStatusCode)
{
Log($"Request-Error {response.StatusCode}: {url}");
return new RequestResult(response.StatusCode, null, Stream.Null);
}
Stream stream = response.Content.ReadAsStream();
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)
{
return new RequestResult(response.StatusCode, document, stream, true,
response.RequestMessage.RequestUri.AbsoluteUri);
}
return new RequestResult(response.StatusCode, document, stream);
}
}

View File

@ -1,5 +1,4 @@
using System.Globalization;
using System.IO.Compression;
using System.IO.Compression;
using System.Net;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
@ -15,14 +14,16 @@ namespace Tranga.MangaConnectors;
public abstract class MangaConnector : GlobalBase
{
internal DownloadClient downloadClient { get; init; } = null!;
public string[] SupportedLanguages;
protected MangaConnector(GlobalBase clone) : base(clone)
protected MangaConnector(GlobalBase clone, string name, string[] supportedLanguages) : base(clone)
{
if (!Directory.Exists(settings.coverImageCache))
Directory.CreateDirectory(settings.coverImageCache);
this.name = name;
this.SupportedLanguages = supportedLanguages;
Directory.CreateDirectory(TrangaSettings.coverImageCache);
}
public abstract string name { get; } //Name of the Connector (e.g. Website)
public string name { get; } //Name of the Connector (e.g. Website)
/// <summary>
/// Returns all Publications with the given string.
@ -30,134 +31,91 @@ public abstract class MangaConnector : GlobalBase
/// </summary>
/// <param name="publicationTitle">Search-Query</param>
/// <returns>Publications matching the query</returns>
public abstract Publication[] GetPublications(string publicationTitle = "");
public abstract Manga[] GetManga(string publicationTitle = "");
public abstract Manga? GetMangaFromUrl(string url);
public abstract Manga? GetMangaFromId(string publicationId);
/// <summary>
/// Returns all Chapters of the publication in the provided language.
/// If the language is empty or null, returns all Chapters in all Languages.
/// </summary>
/// <param name="publication">Publication to get Chapters for</param>
/// <param name="manga">Publication to get Chapters for</param>
/// <param name="language">Language of the Chapters</param>
/// <returns>Array of Chapters matching Publication and Language</returns>
public abstract Chapter[] GetChapters(Publication publication, string language="en");
public abstract Chapter[] GetChapters(Manga manga, string language="en");
/// <summary>
/// Updates the available Chapters of a Publication
/// </summary>
/// <param name="publication">Publication to check</param>
/// <param name="manga">Publication to check</param>
/// <param name="language">Language to receive chapters for</param>
/// <returns>List of Chapters that were previously not in collection</returns>
public Chapter[] GetNewChapters(Publication publication, string language = "en")
public Chapter[] GetNewChapters(Manga manga, string language = "en")
{
Log($"Getting new Chapters for {publication}");
Chapter[] newChapters = this.GetChapters(publication, language);
NumberFormatInfo decimalPoint = new (){ NumberDecimalSeparator = "." };
Log($"Checking for duplicates {publication}");
List<Chapter> newChaptersList = newChapters.Where(nChapter =>
float.Parse(nChapter.chapterNumber, decimalPoint) > publication.ignoreChaptersBelow &&
!nChapter.CheckChapterIsDownloaded(settings.downloadLocation)).ToList();
Log($"{newChaptersList.Count} new chapters. {publication}");
Log($"Getting new Chapters for {manga}");
Chapter[] allChapters = this.GetChapters(manga, language);
if (allChapters.Length < 1)
return Array.Empty<Chapter>();
Log($"Checking for duplicates {manga}");
List<Chapter> newChaptersList = allChapters.Where(nChapter => nChapter.chapterNumber >= manga.ignoreChaptersBelow
&& !nChapter.CheckChapterIsDownloaded()).ToList();
Log($"{newChaptersList.Count} new chapters. {manga}");
try
{
Chapter latestChapterAvailable =
allChapters.Max();
manga.latestChapterAvailable =
Convert.ToSingle(latestChapterAvailable.chapterNumber, numberFormatDecimalPoint);
}
catch (Exception e)
{
Log(e.ToString());
Log($"Failed getting new Chapters for {manga}");
}
return newChaptersList.ToArray();
}
public Chapter[] SelectChapters(Publication publication, string searchTerm, string? language = null)
{
Chapter[] availableChapters = this.GetChapters(publication, language??"en");
Regex volumeRegex = new ("((v(ol)*(olume)*){1} *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
Regex chapterRegex = new ("((c(h)*(hapter)*){1} *([0-9]+(-[0-9]+)?){1})", RegexOptions.IgnoreCase);
Regex singleResultRegex = new("([0-9]+)", RegexOptions.IgnoreCase);
Regex rangeResultRegex = new("([0-9]+(-[0-9]+))", RegexOptions.IgnoreCase);
Regex allRegex = new("a(ll)?", RegexOptions.IgnoreCase);
if (volumeRegex.IsMatch(searchTerm) && chapterRegex.IsMatch(searchTerm))
{
string volume = singleResultRegex.Match(volumeRegex.Match(searchTerm).Value).Value;
string chapter = singleResultRegex.Match(chapterRegex.Match(searchTerm).Value).Value;
return availableChapters.Where(aCh => aCh.volumeNumber is not null &&
aCh.volumeNumber.Equals(volume, StringComparison.InvariantCultureIgnoreCase) &&
aCh.chapterNumber.Equals(chapter, StringComparison.InvariantCultureIgnoreCase))
.ToArray();
}
else if (volumeRegex.IsMatch(searchTerm))
{
string volume = volumeRegex.Match(searchTerm).Value;
if (rangeResultRegex.IsMatch(volume))
{
string range = rangeResultRegex.Match(volume).Value;
int start = Convert.ToInt32(range.Split('-')[0]);
int end = Convert.ToInt32(range.Split('-')[1]);
return availableChapters.Where(aCh => aCh.volumeNumber is not null &&
Convert.ToInt32(aCh.volumeNumber) >= start &&
Convert.ToInt32(aCh.volumeNumber) <= end).ToArray();
}
else if (singleResultRegex.IsMatch(volume))
{
string volumeNumber = singleResultRegex.Match(volume).Value;
return availableChapters.Where(aCh =>
aCh.volumeNumber is not null &&
aCh.volumeNumber.Equals(volumeNumber, StringComparison.InvariantCultureIgnoreCase)).ToArray();
}
}
else if (chapterRegex.IsMatch(searchTerm))
{
string chapter = chapterRegex.Match(searchTerm).Value;
if (rangeResultRegex.IsMatch(chapter))
{
string range = rangeResultRegex.Match(chapter).Value;
int start = Convert.ToInt32(range.Split('-')[0]);
int end = Convert.ToInt32(range.Split('-')[1]);
return availableChapters.Where(aCh => Convert.ToInt32(aCh.chapterNumber) >= start &&
Convert.ToInt32(aCh.chapterNumber) <= end).ToArray();
}
else if (singleResultRegex.IsMatch(chapter))
{
string chapterNumber = singleResultRegex.Match(chapter).Value;
return availableChapters.Where(aCh =>
aCh.chapterNumber.Equals(chapterNumber, StringComparison.InvariantCultureIgnoreCase)).ToArray();
}
}
else
{
if (rangeResultRegex.IsMatch(searchTerm))
{
int start = Convert.ToInt32(searchTerm.Split('-')[0]);
int end = Convert.ToInt32(searchTerm.Split('-')[1]);
return availableChapters[start..(end + 1)];
}
else if(singleResultRegex.IsMatch(searchTerm))
return new [] { availableChapters[Convert.ToInt32(searchTerm)] };
else if (allRegex.IsMatch(searchTerm))
return availableChapters;
}
return Array.Empty<Chapter>();
}
public abstract HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null);
/// <summary>
/// Copies the already downloaded cover from cache to downloadLocation
/// </summary>
/// <param name="publication">Publication to retrieve Cover for</param>
public void CopyCoverFromCacheToDownloadLocation(Publication publication)
/// <param name="manga">Publication to retrieve Cover for</param>
/// <param name="retries">Number of times to retry to copy the cover (or download it first)</param>
public void CopyCoverFromCacheToDownloadLocation(Manga manga, int? retries = 1)
{
Log($"Copy cover {publication}");
Log($"Copy cover {manga}");
//Check if Publication already has a Folder and cover
string publicationFolder = publication.CreatePublicationFolder(settings.downloadLocation);
string publicationFolder = manga.CreatePublicationFolder(TrangaSettings.downloadLocation);
DirectoryInfo dirInfo = new (publicationFolder);
if (dirInfo.EnumerateFiles().Any(info => info.Name.Contains("cover", StringComparison.InvariantCultureIgnoreCase)))
{
Log($"Cover exists {publication}");
Log($"Cover exists {manga}");
return;
}
string fileInCache = Path.Join(settings.coverImageCache, publication.coverFileNameInCache);
string? fileInCache = manga.coverFileNameInCache;
if (fileInCache is null || !File.Exists(fileInCache))
{
Log($"Cloning cover failed: File missing {fileInCache}.");
if (retries > 0 && manga.coverUrl is not null)
{
Log($"Trying {retries} more times");
SaveCoverImageToCache(manga.coverUrl, manga.internalId, 0);
CopyCoverFromCacheToDownloadLocation(manga, --retries);
}
return;
}
string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
Log($"Cloning cover {fileInCache} -> {newFilePath}");
File.Copy(fileInCache, newFilePath, true);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite);
}
/// <summary>
@ -167,43 +125,65 @@ public abstract class MangaConnector : GlobalBase
/// <param name="fullPath"></param>
/// <param name="requestType">RequestType for Rate-Limit</param>
/// <param name="referrer">referrer used in html request header</param>
private HttpStatusCode DownloadImage(string imageUrl, string fullPath, byte requestType, string? referrer = null)
private HttpStatusCode DownloadImage(string imageUrl, string fullPath, RequestType requestType, string? referrer = null)
{
DownloadClient.RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType, referrer);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.result == Stream.Null)
RequestResult requestResult = downloadClient.MakeRequest(imageUrl, requestType, referrer);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return requestResult.statusCode;
byte[] buffer = new byte[requestResult.result.Length];
requestResult.result.ReadExactly(buffer, 0, buffer.Length);
File.WriteAllBytes(fullPath, buffer);
if (requestResult.result == Stream.Null)
return HttpStatusCode.NotFound;
FileStream fs = new (fullPath, FileMode.Create);
requestResult.result.CopyTo(fs);
fs.Close();
return requestResult.statusCode;
}
protected HttpStatusCode DownloadChapterImages(string[] imageUrls, string saveArchiveFilePath, byte requestType, string? comicInfoPath = null, string? referrer = null, ProgressToken? progressToken = null)
protected HttpStatusCode DownloadChapterImages(string[] imageUrls, Chapter chapter, RequestType requestType, string? referrer = null, ProgressToken? progressToken = null)
{
string saveArchiveFilePath = chapter.GetArchiveFilePath();
if (progressToken?.cancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
Log($"Downloading Images for {saveArchiveFilePath}");
if(progressToken is not null)
progressToken.increments = imageUrls.Length;
if (progressToken is not null)
progressToken.increments += imageUrls.Length;
//Check if Publication Directory already exists
string directoryPath = Path.GetDirectoryName(saveArchiveFilePath)!;
if (!Directory.Exists(directoryPath))
Directory.CreateDirectory(directoryPath);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
Directory.CreateDirectory(directoryPath,
UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute );
else
Directory.CreateDirectory(directoryPath);
if (File.Exists(saveArchiveFilePath)) //Don't download twice.
return HttpStatusCode.OK;
{
progressToken?.Complete();
return HttpStatusCode.Created;
}
//Create a temporary folder to store images
string tempFolder = Directory.CreateTempSubdirectory().FullName;
string tempFolder = Directory.CreateTempSubdirectory("trangatemp").FullName;
int chapter = 0;
int chapterNum = 0;
//Download all Images to temporary Folder
if (imageUrls.Length == 0)
{
Log("No images found");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute);
Directory.Delete(tempFolder, true);
progressToken?.Complete();
return HttpStatusCode.NoContent;
}
foreach (string imageUrl in imageUrls)
{
string[] split = imageUrl.Split('.');
string extension = split[^1];
Log($"Downloading image {chapter + 1:000}/{imageUrls.Length:000}"); //TODO
HttpStatusCode status = DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapter++}.{extension}"), requestType, referrer);
string extension = imageUrl.Split('.')[^1].Split('?')[0];
Log($"Downloading image {chapterNum + 1:000}/{imageUrls.Length:000}"); //TODO
HttpStatusCode status = DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapterNum++}.{extension}"), requestType, referrer);
Log($"{saveArchiveFilePath} {chapterNum + 1:000}/{imageUrls.Length:000} {status}");
if ((int)status < 200 || (int)status >= 300)
{
progressToken?.Complete();
@ -211,40 +191,45 @@ public abstract class MangaConnector : GlobalBase
}
if (progressToken?.cancellationRequested ?? false)
{
progressToken?.Complete();
progressToken.Complete();
return HttpStatusCode.RequestTimeout;
}
progressToken?.Increment();
}
if(comicInfoPath is not null)
File.Copy(comicInfoPath, Path.Join(tempFolder, "ComicInfo.xml"));
File.WriteAllText(Path.Join(tempFolder, "ComicInfo.xml"), chapter.GetComicInfoXmlString());
Log($"Creating archive {saveArchiveFilePath}");
//ZIP-it and ship-it
ZipFile.CreateFromDirectory(tempFolder, saveArchiveFilePath);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(saveArchiveFilePath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
chapter.CreateChapterMarker();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(saveArchiveFilePath, UserRead | UserWrite | UserExecute | GroupRead | GroupWrite | GroupExecute | OtherRead | OtherExecute);
Directory.Delete(tempFolder, true); //Cleanup
Log("Created archive.");
progressToken?.Complete();
Log("Download complete.");
return HttpStatusCode.OK;
}
protected string SaveCoverImageToCache(string url, byte requestType)
protected string SaveCoverImageToCache(string url, string mangaInternalId, RequestType requestType, string? referrer = null)
{
string[] split = url.Split('/');
string filename = split[^1];
string saveImagePath = Path.Join(settings.coverImageCache, filename);
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(url);
string filename = $"{match.Groups[1].Value}-{mangaInternalId}.{match.Groups[3].Value}";
string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
if (File.Exists(saveImagePath))
return filename;
return saveImagePath;
DownloadClient.RequestResult coverResult = downloadClient.MakeRequest(url, requestType);
RequestResult coverResult = downloadClient.MakeRequest(url, requestType, referrer);
using MemoryStream ms = new();
coverResult.result.CopyTo(ms);
Directory.CreateDirectory(TrangaSettings.coverImageCache);
File.WriteAllBytes(saveImagePath, ms.ToArray());
Log($"Saving cover to {saveImagePath}");
return filename;
return saveImagePath;
}
}

View File

@ -0,0 +1,55 @@
using System.Data;
using System.Diagnostics;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Tranga.MangaConnectors;
public class MangaConnectorJsonConverter : JsonConverter
{
private GlobalBase _clone;
private readonly HashSet<MangaConnector> _connectors;
internal MangaConnectorJsonConverter(GlobalBase clone, HashSet<MangaConnector> connectors)
{
this._clone = clone;
this._connectors = connectors;
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(MangaConnector));
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
string? connectorName = jo.Value<string>("name");
if (connectorName is null)
throw new ConstraintException("Name can not be null.");
return connectorName switch
{
"MangaDex" => this._connectors.First(c => c is MangaDex),
"Manganato" => this._connectors.First(c => c is Manganato),
"MangaKatana" => this._connectors.First(c => c is MangaKatana),
"Mangaworld" => this._connectors.First(c => c is Mangaworld),
"Bato" => this._connectors.First(c => c is Bato),
"ManhuaPlus" => this._connectors.First(c => c is ManhuaPlus),
"MangaHere" => this._connectors.First(c => c is MangaHere),
"AsuraToon" => this._connectors.First(c => c is AsuraToon),
"Weebcentral" => this._connectors.First(c => c is Weebcentral),
"Webtoons" => this._connectors.First(c => c is Webtoons),
_ => throw new UnreachableException($"Could not find Connector with name {connectorName}")
};
}
public override bool CanWrite => false;
/// <summary>
/// Don't call this
/// </summary>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new Exception("Dont call this");
}
}

View File

@ -1,50 +1,40 @@
using System.Globalization;
using System.Net;
using System.Text.Json;
using System.Net;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Tranga.Jobs;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Tranga.MangaConnectors;
public class MangaDex : MangaConnector
{
public override string name { get; }
private enum RequestType : byte
//https://api.mangadex.org/docs/3-enumerations/#language-codes--localization
//https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
//https://gist.github.com/Josantonius/b455e315bc7f790d14b136d61d9ae469
public MangaDex(GlobalBase clone) : base(clone, "MangaDex", ["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"])
{
Manga,
Feed,
AtHomeServer,
CoverUrl,
Author,
this.downloadClient = new HttpDownloadClient(clone);
}
public MangaDex(GlobalBase clone) : base(clone)
public override Manga[] GetManga(string publicationTitle = "")
{
name = "MangaDex";
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
{
{(byte)RequestType.Manga, 250},
{(byte)RequestType.Feed, 250},
{(byte)RequestType.AtHomeServer, 40},
{(byte)RequestType.CoverUrl, 250},
{(byte)RequestType.Author, 250}
}, clone);
}
public override Publication[] GetPublications(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{publicationTitle}\"");
Log($"Searching Publications. Term={publicationTitle}");
const int limit = 100; //How many values we want returned at once
int offset = 0; //"Page"
int total = int.MaxValue; //How many total results are there, is updated on first request
HashSet<Publication> publications = new();
HashSet<Manga> retManga = new();
int loadedPublicationData = 0;
List<JsonNode> results = new();
//Request all search-results
while (offset < total) //As long as we haven't requested all "Pages"
{
//Request next Page
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}", (byte)RequestType.Manga);
RequestResult requestResult = downloadClient.MakeRequest(
$"https://api.mangadex.org/manga?limit={limit}&title={publicationTitle}&offset={offset}" +
$"&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica" +
$"&contentRating%5B%5D=pornographic" +
$"&includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author" +
$"&includes%5B%5D=artist&includes%5B%5D=tag", RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
break;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
@ -53,107 +43,160 @@ public class MangaDex : MangaConnector
if (result is null)
break;
total = result["total"]!.GetValue<int>(); //Update the total number of Publications
if(result.ContainsKey("total"))
total = result["total"]!.GetValue<int>(); //Update the total number of Publications
else continue;
if (result.ContainsKey("data"))
results.AddRange(result["data"]!.AsArray()!);//Manga-data-Array
}
foreach (JsonNode mangaNode in results)
{
Log($"Getting publication data. {++loadedPublicationData}/{total}");
if(MangaFromJsonObject(mangaNode.AsObject()) is { } manga)
retManga.Add(manga); //Add Publication (Manga) to result
}
Log($"Retrieved {retManga.Count} publications. Term={publicationTitle}");
return retManga.ToArray();
}
public override Manga? GetMangaFromId(string publicationId)
{
RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/manga/{publicationId}?includes%5B%5D=manga&includes%5B%5D=cover_art&includes%5B%5D=author&includes%5B%5D=artist&includes%5B%5D=tag", RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if(result is not null)
return MangaFromJsonObject(result["data"]!.AsObject());
return null;
}
public override Manga? GetMangaFromUrl(string url)
{
Regex idRex = new (@"https:\/\/mangadex.org\/title\/([A-z0-9-]*)\/.*");
string id = idRex.Match(url).Groups[1].Value;
Log($"Got id {id} from {url}");
return GetMangaFromId(id);
}
private Manga? MangaFromJsonObject(JsonObject manga)
{
if (!manga.TryGetPropertyValue("id", out JsonNode? idNode))
return null;
string publicationId = idNode!.GetValue<string>();
JsonArray mangaInResult = result["data"]!.AsArray(); //Manga-data-Array
//Loop each Manga and extract information from JSON
foreach (JsonNode? mangeNode in mangaInResult)
if (!manga.TryGetPropertyValue("attributes", out JsonNode? attributesNode))
return null;
JsonObject attributes = attributesNode!.AsObject();
if (!attributes.TryGetPropertyValue("title", out JsonNode? titleNode))
return null;
string title = titleNode!.AsObject().ContainsKey("en") switch
{
true => titleNode.AsObject()["en"]!.GetValue<string>(),
false => titleNode.AsObject().First().Value!.GetValue<string>()
};
Dictionary<string, string> altTitlesDict = new();
if (attributes.TryGetPropertyValue("altTitles", out JsonNode? altTitlesNode))
{
foreach (JsonNode? altTitleNode in altTitlesNode!.AsArray())
{
Log($"Getting publication data. {++loadedPublicationData}/{total}");
JsonObject manga = (JsonObject)mangeNode!;
JsonObject attributes = manga["attributes"]!.AsObject();
string publicationId = manga["id"]!.GetValue<string>();
string title = attributes["title"]!.AsObject().ContainsKey("en") && attributes["title"]!["en"] is not null
? attributes["title"]!["en"]!.GetValue<string>()
: attributes["title"]![((IDictionary<string, JsonNode?>)attributes["title"]!.AsObject()).Keys.First()]!.GetValue<string>();
string? description = attributes["description"]!.AsObject().ContainsKey("en") && attributes["description"]!["en"] is not null
? attributes["description"]!["en"]!.GetValue<string?>()
: null;
JsonArray altTitlesObject = attributes["altTitles"]!.AsArray();
Dictionary<string, string> altTitlesDict = new();
foreach (JsonNode? altTitleNode in altTitlesObject)
{
JsonObject altTitleObject = (JsonObject)altTitleNode!;
string key = ((IDictionary<string, JsonNode?>)altTitleObject).Keys.ToArray()[0];
altTitlesDict.TryAdd(key, altTitleObject[key]!.GetValue<string>());
}
JsonArray tagsObject = attributes["tags"]!.AsArray();
HashSet<string> tags = new();
foreach (JsonNode? tagNode in tagsObject)
{
JsonObject tagObject = (JsonObject)tagNode!;
if(tagObject["attributes"]!["name"]!.AsObject().ContainsKey("en"))
tags.Add(tagObject["attributes"]!["name"]!["en"]!.GetValue<string>());
}
string? posterId = null;
HashSet<string> authorIds = new();
if (manga.ContainsKey("relationships") && manga["relationships"] is not null)
{
JsonArray relationships = manga["relationships"]!.AsArray();
posterId = relationships.FirstOrDefault(relationship => relationship!["type"]!.GetValue<string>() == "cover_art")!["id"]!.GetValue<string>();
foreach (JsonNode? node in relationships.Where(relationship =>
relationship!["type"]!.GetValue<string>() == "author"))
authorIds.Add(node!["id"]!.GetValue<string>());
}
string? coverUrl = GetCoverUrl(publicationId, posterId);
string? coverCacheName = null;
if (coverUrl is not null)
coverCacheName = SaveCoverImageToCache(coverUrl, (byte)RequestType.AtHomeServer);
List<string> authors = GetAuthors(authorIds);
Dictionary<string, string> linksDict = new();
if (attributes.ContainsKey("links") && attributes["links"] is not null)
{
JsonObject linksObject = attributes["links"]!.AsObject();
foreach (string key in ((IDictionary<string, JsonNode?>)linksObject).Keys)
{
linksDict.Add(key, linksObject[key]!.GetValue<string>());
}
}
int? year = attributes.ContainsKey("year") && attributes["year"] is not null
? attributes["year"]!.GetValue<int?>()
: null;
string? originalLanguage = attributes.ContainsKey("originalLanguage") && attributes["originalLanguage"] is not null
? attributes["originalLanguage"]!.GetValue<string?>()
: null;
string status = attributes["status"]!.GetValue<string>();
Publication pub = new (
title,
authors,
description,
altTitlesDict,
tags.ToArray(),
coverUrl,
coverCacheName,
linksDict,
year,
originalLanguage,
status,
publicationId
);
publications.Add(pub); //Add Publication (Manga) to result
JsonObject altTitleNodeObject = altTitleNode!.AsObject();
altTitlesDict.TryAdd(altTitleNodeObject.First().Key, altTitleNodeObject.First().Value!.GetValue<string>());
}
}
cachedPublications.AddRange(publications);
Log($"Retrieved {publications.Count} publications. Term=\"{publicationTitle}\"");
return publications.ToArray();
if (!attributes.TryGetPropertyValue("description", out JsonNode? descriptionNode))
return null;
string description = descriptionNode!.AsObject().ContainsKey("en") switch
{
true => descriptionNode.AsObject()["en"]!.GetValue<string>(),
false => descriptionNode.AsObject().FirstOrDefault().Value?.GetValue<string>() ?? ""
};
Dictionary<string, string> linksDict = new();
if (attributes.TryGetPropertyValue("links", out JsonNode? linksNode) && linksNode is not null)
foreach (KeyValuePair<string, JsonNode?> linkKv in linksNode!.AsObject())
linksDict.TryAdd(linkKv.Key, linkKv.Value.GetValue<string>());
string? originalLanguage =
attributes.TryGetPropertyValue("originalLanguage", out JsonNode? originalLanguageNode) switch
{
true => originalLanguageNode?.GetValue<string>(),
false => null
};
Manga.ReleaseStatusByte status = Manga.ReleaseStatusByte.Unreleased;
if (attributes.TryGetPropertyValue("status", out JsonNode? statusNode))
{
status = statusNode?.GetValue<string>().ToLower() switch
{
"ongoing" => Manga.ReleaseStatusByte.Continuing,
"completed" => Manga.ReleaseStatusByte.Completed,
"hiatus" => Manga.ReleaseStatusByte.OnHiatus,
"cancelled" => Manga.ReleaseStatusByte.Cancelled,
_ => Manga.ReleaseStatusByte.Unreleased
};
}
int? year = attributes.TryGetPropertyValue("year", out JsonNode? yearNode) switch
{
true => yearNode?.GetValue<int>(),
false => null
};
HashSet<string> tags = new(128);
if (attributes.TryGetPropertyValue("tags", out JsonNode? tagsNode))
foreach (JsonNode? tagNode in tagsNode!.AsArray())
tags.Add(tagNode!["attributes"]!["name"]!["en"]!.GetValue<string>());
if (!manga.TryGetPropertyValue("relationships", out JsonNode? relationshipsNode))
return null;
JsonNode? coverNode = relationshipsNode!.AsArray()
.FirstOrDefault(rel => rel!["type"]!.GetValue<string>().Equals("cover_art"));
if (coverNode is null)
return null;
string fileName = coverNode["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
string coverCacheName = SaveCoverImageToCache(coverUrl, publicationId, RequestType.MangaCover);
List<string> authors = new();
JsonNode?[] authorNodes = relationshipsNode.AsArray()
.Where(rel => rel!["type"]!.GetValue<string>().Equals("author") || rel!["type"]!.GetValue<string>().Equals("artist")).ToArray();
foreach (JsonNode? authorNode in authorNodes)
{
string authorName = authorNode!["attributes"]!["name"]!.GetValue<string>();
if(!authors.Contains(authorName))
authors.Add(authorName);
}
Manga pub = new(
title,
authors,
description,
altTitlesDict,
tags.ToArray(),
coverUrl,
coverCacheName,
linksDict,
year,
originalLanguage,
publicationId,
status,
websiteUrl: $"https://mangadex.org/title/{publicationId}"
);
AddMangaToCache(pub);
return pub;
}
public override Chapter[] GetChapters(Publication publication, string language="en")
public override Chapter[] GetChapters(Manga manga, string language="en")
{
Log($"Getting chapters {publication}");
Log($"Getting chapters {manga}");
const int limit = 100; //How many values we want returned at once
int offset = 0; //"Page"
int total = int.MaxValue; //How many total results are there, is updated on first request
@ -162,9 +205,9 @@ public class MangaDex : MangaConnector
while (offset < total)
{
//Request next "Page"
DownloadClient.RequestResult requestResult =
RequestResult requestResult =
downloadClient.MakeRequest(
$"https://api.mangadex.org/manga/{publication.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}", (byte)RequestType.Feed);
$"https://api.mangadex.org/manga/{manga.publicationId}/feed?limit={limit}&offset={offset}&translatedLanguage%5B%5D={language}&contentRating%5B%5D=safe&contentRating%5B%5D=suggestive&contentRating%5B%5D=erotica&contentRating%5B%5D=pornographic", RequestType.MangaDexFeed);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
break;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
@ -180,6 +223,7 @@ public class MangaDex : MangaConnector
{
JsonObject chapter = (JsonObject)jsonNode!;
JsonObject attributes = chapter["attributes"]!.AsObject();
string chapterId = chapter["id"]!.GetValue<string>();
string? title = attributes.ContainsKey("title") && attributes["title"] is not null
@ -194,30 +238,57 @@ public class MangaDex : MangaConnector
? attributes["chapter"]!.GetValue<string>()
: "null";
if(chapterNum is not "null")
chapters.Add(new Chapter(publication, title, volume, chapterNum, chapterId));
if (attributes.ContainsKey("pages") && attributes["pages"] is not null &&
attributes["pages"]!.GetValue<int>() < 1)
{
Log($"Skipping {chapterId} Vol.{volume} Ch.{chapterNum} {title} because it has no pages or is externally linked.");
continue;
}
try
{
if(!chapters.Any(chp =>
chp.volumeNumber.Equals(float.Parse(volume??"0", numberFormatDecimalPoint)) &&
chp.chapterNumber.Equals(float.Parse(chapterNum, numberFormatDecimalPoint))))
chapters.Add(new Chapter(manga, title, volume, chapterNum, chapterId, chapterId));
}
catch (Exception e)
{
Log($"Failed to load chapter {chapterNum}: {e.Message}");
}
}
}
//Return Chapters ordered by Chapter-Number
NumberFormatInfo chapterNumberFormatInfo = new() { NumberDecimalSeparator = "." };
Log($"Got {chapters.Count} chapters. {publication}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
Log($"Got {chapters.Count} chapters. {manga}");
return chapters.Order().ToArray();
}
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
Log($"Retrieving chapter-info {chapter} {chapter.parentPublication}");
}
Manga chapterParentManga = chapter.parentManga;
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
//Request URLs for Chapter-Images
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false'", (byte)RequestType.AtHomeServer);
RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/at-home/server/{chapter.url}?forcePort443=false", RequestType.MangaDexImage);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
progressToken?.Cancel();
return requestResult.statusCode;
}
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null)
{
progressToken?.Cancel();
return HttpStatusCode.NoContent;
}
string baseUrl = result["baseUrl"]!.GetValue<string>();
string hash = result["chapter"]!["hash"]!.GetValue<string>();
@ -226,57 +297,8 @@ public class MangaDex : MangaConnector
HashSet<string> imageUrls = new();
foreach (JsonNode? image in imageFileNames)
imageUrls.Add($"{baseUrl}/data/{hash}/{image!.GetValue<string>()}");
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
//Download Chapter-Images
return DownloadChapterImages(imageUrls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), (byte)RequestType.AtHomeServer, comicInfoPath, progressToken:progressToken);
}
private string? GetCoverUrl(string publicationId, string? posterId)
{
Log($"Getting CoverUrl for Publication {publicationId}");
if (posterId is null)
{
Log("No cover.");
return null;
}
//Request information where to download Cover
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/cover/{posterId}", (byte)RequestType.CoverUrl);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null)
return null;
string fileName = result["data"]!["attributes"]!["fileName"]!.GetValue<string>();
string coverUrl = $"https://uploads.mangadex.org/covers/{publicationId}/{fileName}";
Log($"Cover-Url {publicationId} -> {coverUrl}");
return coverUrl;
}
private List<string> GetAuthors(IEnumerable<string> authorIds)
{
Log("Retrieving authors.");
List<string> ret = new();
foreach (string authorId in authorIds)
{
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://api.mangadex.org/author/{authorId}", (byte)RequestType.Author);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return ret;
JsonObject? result = JsonSerializer.Deserialize<JsonObject>(requestResult.result);
if (result is null)
return ret;
string authorName = result["data"]!["attributes"]!["name"]!.GetValue<string>();
ret.Add(authorName);
Log($"Got author {authorId} -> {authorName}");
}
return ret;
return DownloadChapterImages(imageUrls.ToArray(), chapter, RequestType.MangaImage, progressToken:progressToken);
}
}

View File

@ -0,0 +1,208 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.Jobs;
namespace Tranga.MangaConnectors;
public class MangaHere : MangaConnector
{
public MangaHere(GlobalBase clone) : base(clone, "MangaHere", ["en"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
public override Manga[] GetManga(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join('+', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
string requestUrl = $"https://www.mangahere.cc/search?title={sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
return Array.Empty<Manga>();
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
{
if (document.DocumentNode.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),' container ')]").Any(node => node.ChildNodes.Any(cNode => cNode.HasClass("search-keywords"))))
return Array.Empty<Manga>();
List<string> urls = document.DocumentNode
.SelectNodes("//a[contains(@href, '/manga/') and not(contains(@href, '.html'))]")
.Select(thumb => $"https://www.mangahere.cc{thumb.GetAttributeValue("href", "")}").Distinct().ToList();
HashSet<Manga> ret = new();
foreach (string url in urls)
{
Manga? manga = GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
}
return ret.ToArray();
}
public override Manga? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://www.mangahere.cc/manga/{publicationId}");
}
public override Manga? GetMangaFromUrl(string url)
{
RequestResult requestResult =
downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
return null;
Regex idRex = new (@"https:\/\/www\.mangahere\.[a-z]{0,63}\/manga\/([0-9A-z\-]+).*");
string id = idRex.Match(url).Groups[1].Value;
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, id, url);
}
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
string originalLanguage = "", status = "";
Dictionary<string, string> altTitles = new(), links = new();
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
//We dont get posters, because same origin bs HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//img[contains(concat(' ',normalize-space(@class),' '),' detail-info-cover-img ')]");
string posterUrl = "http://static.mangahere.cc/v20230914/mangahere/images/nopicture.jpg";
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover);
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//span[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-title-font ')]");
string sortName = titleNode.InnerText;
List<string> authors = document.DocumentNode
.SelectNodes("//p[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-say ')]/a")
.Select(node => node.InnerText)
.ToList();
HashSet<string> tags = document.DocumentNode
.SelectNodes("//p[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-tag-list ')]/a")
.Select(node => node.InnerText)
.ToHashSet();
status = document.DocumentNode.SelectSingleNode("//span[contains(concat(' ',normalize-space(@class),' '),' detail-info-right-title-tip ')]").InnerText;
switch (status.ToLower())
{
case "cancelled": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
case "hiatus": releaseStatus = Manga.ReleaseStatusByte.OnHiatus; break;
case "discontinued": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
case "complete": releaseStatus = Manga.ReleaseStatusByte.Completed; break;
case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break;
}
HtmlNode descriptionNode = document.DocumentNode
.SelectSingleNode("//p[contains(concat(' ',normalize-space(@class),' '),' fullcontent ')]");
string description = descriptionNode.InnerText;
Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links,
null, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
return manga;
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
Log($"Getting chapters {manga}");
string requestUrl = $"https://www.mangahere.cc/manga/{manga.publicationId}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 || requestResult.htmlDocument is null)
return Array.Empty<Chapter>();
List<string> urls = requestResult.htmlDocument.DocumentNode.SelectNodes("//div[@id='list-1']/ul//li//a[contains(@href, '/manga/')]")
.Select(node => node.GetAttributeValue("href", "")).ToList();
Regex chapterRex = new(@".*\/manga\/[a-zA-Z0-9\-\._\~\!\$\&\'\(\)\*\+\,\;\=\:\@]+\/v([0-9(TBD)]+)\/c([0-9\.]+)\/.*");
List<Chapter> chapters = new();
foreach (string url in urls)
{
Match rexMatch = chapterRex.Match(url);
string volumeNumber = rexMatch.Groups[1].Value == "TBD" ? "0" : rexMatch.Groups[1].Value;
string chapterNumber = rexMatch.Groups[2].Value;
string fullUrl = $"https://www.mangahere.cc{url}";
try
{
chapters.Add(new Chapter(manga, "", volumeNumber, chapterNumber, fullUrl));
}
catch (Exception e)
{
Log($"Failed to load chapter {chapterNumber}: {e.Message}");
}
}
//Return Chapters ordered by Chapter-Number
Log($"Got {chapters.Count} chapters. {manga}");
return chapters.Order().ToArray();
}
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Manga chapterParentManga = chapter.parentManga;
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
List<string> imageUrls = new();
int downloaded = 1;
int images = 1;
string url = string.Join('/', chapter.url.Split('/')[..^1]);
do
{
RequestResult requestResult =
downloadClient.MakeRequest($"{url}/{downloaded}.html", RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
progressToken?.Cancel();
return requestResult.statusCode;
}
if (requestResult.htmlDocument is null)
{
progressToken?.Cancel();
return HttpStatusCode.InternalServerError;
}
imageUrls.AddRange(ParseImageUrlsFromHtml(requestResult.htmlDocument));
images = requestResult.htmlDocument.DocumentNode
.SelectNodes("//a[contains(@href, '/manga/')]")
.MaxBy(node => node.GetAttributeValue("data-page", 0))!.GetAttributeValue("data-page", 0);
logger?.WriteLine($"MangaHere speciality: Get Image-url {downloaded}/{images}");
if (progressToken is not null)
{
progressToken.increments = images * 2;//we also have to download the images later
progressToken.Increment();
}
} while (downloaded++ <= images);
if (progressToken is not null)
progressToken.increments = images;//we blip to normal length, in downloadchapterimages it is increasaed by the amount of urls again
return DownloadChapterImages(imageUrls.ToArray(), chapter, RequestType.MangaImage, progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
{
return document.DocumentNode
.SelectNodes("//img[contains(concat(' ',normalize-space(@class),' '),' reader-main-img ')]")
.Select(node =>
{
string url = node.GetAttributeValue("src", "");
return url.StartsWith("//") ? $"https:{url}" : url;
})
.ToArray();
}
}

View File

@ -1,5 +1,4 @@
using System.Globalization;
using System.Net;
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.Jobs;
@ -8,26 +7,20 @@ namespace Tranga.MangaConnectors;
public class MangaKatana : MangaConnector
{
public override string name { get; }
public MangaKatana(GlobalBase clone) : base(clone)
public MangaKatana(GlobalBase clone) : base(clone, "MangaKatana", ["en"])
{
this.name = "MangaKatana";
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
{
{1, 60}
}, clone);
this.downloadClient = new HttpDownloadClient(clone);
}
public override Publication[] GetPublications(string publicationTitle = "")
public override Manga[] GetManga(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
string sanitizedTitle = string.Join("%20", Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
string requestUrl = $"https://mangakatana.com/?search={sanitizedTitle}&search_by=book_name";
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Publication>();
return Array.Empty<Manga>();
// ReSharper disable once MergeIntoPattern
// If a single result is found, the user will be redirected to the results directly instead of a result page
@ -35,16 +28,29 @@ public class MangaKatana : MangaConnector
&& requestResult.redirectedToUrl is not null
&& requestResult.redirectedToUrl.Contains("mangakatana.com/manga"))
{
return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1]) };
return new [] { ParseSinglePublicationFromHtml(requestResult.result, requestResult.redirectedToUrl.Split('/')[^1], requestResult.redirectedToUrl) };
}
Publication[] publications = ParsePublicationsFromHtml(requestResult.result);
cachedPublications.AddRange(publications);
Manga[] publications = ParsePublicationsFromHtml(requestResult.result);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
private Publication[] ParsePublicationsFromHtml(Stream html)
public override Manga? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://mangakatana.com/manga/{publicationId}");
}
public override Manga? GetMangaFromUrl(string url)
{
RequestResult requestResult =
downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
return ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1], url);
}
private Manga[] ParsePublicationsFromHtml(Stream html)
{
StreamReader reader = new(html);
string htmlString = reader.ReadToEnd();
@ -52,7 +58,7 @@ public class MangaKatana : MangaConnector
document.LoadHtml(htmlString);
IEnumerable<HtmlNode> searchResults = document.DocumentNode.SelectNodes("//*[@id='book_list']/div");
if (searchResults is null || !searchResults.Any())
return Array.Empty<Publication>();
return Array.Empty<Manga>();
List<string> urls = new();
foreach (HtmlNode mangaResult in searchResults)
{
@ -60,32 +66,29 @@ public class MangaKatana : MangaConnector
.First(a => a.Name == "href").Value);
}
HashSet<Publication> ret = new();
HashSet<Manga> ret = new();
foreach (string url in urls)
{
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(url, 1);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Publication>();
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]));
Manga? manga = GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
}
return ret.ToArray();
}
private Publication ParseSinglePublicationFromHtml(Stream html, string publicationId)
private Manga ParseSinglePublicationFromHtml(Stream html, string publicationId, string websiteUrl)
{
StreamReader reader = new(html);
string htmlString = reader.ReadToEnd();
HtmlDocument document = new();
document.LoadHtml(htmlString);
string status = "";
Dictionary<string, string> altTitles = new();
Dictionary<string, string>? links = null;
HashSet<string> tags = new();
string[] authors = Array.Empty<string>();
string originalLanguage = "";
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
HtmlNode infoNode = document.DocumentNode.SelectSingleNode("//*[@id='single_book']");
string sortName = infoNode.Descendants("h1").First(n => n.HasClass("heading")).InnerText;
@ -108,7 +111,11 @@ public class MangaKatana : MangaConnector
authors = value.Split(',');
break;
case "status":
status = value;
switch (value.ToLower())
{
case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break;
case "completed": releaseStatus = Manga.ReleaseStatusByte.Completed; break;
}
break;
case "genres":
tags = row.SelectNodes("div").Last().Descendants("a").Select(a => a.InnerText).ToHashSet();
@ -119,7 +126,7 @@ public class MangaKatana : MangaConnector
string posterUrl = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[1]/div").Descendants("img").First()
.GetAttributes().First(a => a.Name == "src").Value;
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover);
string description = document.DocumentNode.SelectSingleNode("//*[@id='single_book']/div[3]/p").InnerText;
while (description.StartsWith('\n'))
@ -134,31 +141,29 @@ public class MangaKatana : MangaConnector
year = Convert.ToInt32(yearString);
}
return new Publication(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, status, publicationId);
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
return manga;
}
public override Chapter[] GetChapters(Publication publication, string language="en")
public override Chapter[] GetChapters(Manga manga, string language="en")
{
Log($"Getting chapters {publication}");
string requestUrl = $"https://mangakatana.com/manga/{publication.publicationId}";
Log($"Getting chapters {manga}");
string requestUrl = $"https://mangakatana.com/manga/{manga.publicationId}";
// Leaving this in for verification if the page exists
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>();
//Return Chapters ordered by Chapter-Number
NumberFormatInfo chapterNumberFormatInfo = new()
{
NumberDecimalSeparator = "."
};
List<Chapter> chapters = ParseChaptersFromHtml(publication, requestUrl);
Log($"Got {chapters.Count} chapters. {publication}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestUrl);
Log($"Got {chapters.Count} chapters. {manga}");
return chapters.Order().ToArray();
}
private List<Chapter> ParseChaptersFromHtml(Publication publication, string mangaUrl)
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
{
// Using HtmlWeb will include the chapters since they are loaded with js
HtmlWeb web = new();
@ -168,16 +173,27 @@ public class MangaKatana : MangaConnector
HtmlNode chapterList = document.DocumentNode.SelectSingleNode("//div[contains(@class, 'chapters')]/table/tbody");
Regex volumeRex = new(@"[0-9a-z\-\.]+\/[0-9a-z\-]*v([0-9\.]+)");
Regex chapterNumRex = new(@"[0-9a-z\-\.]+\/[0-9a-z\-]*c([0-9\.]+)");
Regex chapterNameRex = new(@"Chapter [0-9\.]+:? (.*)");
foreach (HtmlNode chapterInfo in chapterList.Descendants("tr"))
{
string fullString = chapterInfo.Descendants("a").First().InnerText;
string? volumeNumber = fullString.Contains("Vol.") ? fullString.Replace("Vol.", "").Split(' ')[0] : null;
string chapterNumber = fullString.Split(':')[0].Split("Chapter ")[1].Split(" ")[0].Replace('-', '.');
string chapterName = string.Concat(fullString.Split(':')[1..]);
string url = chapterInfo.Descendants("a").First()
.GetAttributeValue("href", "");
ret.Add(new Chapter(publication, chapterName, volumeNumber, chapterNumber, url));
string? volumeNumber = volumeRex.IsMatch(url) ? volumeRex.Match(url).Groups[1].Value : null;
string chapterNumber = chapterNumRex.Match(url).Groups[1].Value;
string chapterName = chapterNameRex.Match(fullString).Groups[1].Value;
try
{
ret.Add(new Chapter(manga, chapterName, volumeNumber, chapterNumber, url));
}
catch (Exception e)
{
Log($"Failed to load chapter {chapterNumber}: {e.Message}");
}
}
return ret;
@ -186,21 +202,26 @@ public class MangaKatana : MangaConnector
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
Log($"Retrieving chapter-info {chapter} {chapter.parentPublication}");
}
Manga chapterParentManga = chapter.parentManga;
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
string requestUrl = chapter.url;
// Leaving this in to check if the page exists
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
progressToken?.Cancel();
return requestResult.statusCode;
}
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, "https://mangakatana.com/", progressToken:progressToken);
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(string mangaUrl)

View File

@ -8,163 +8,181 @@ namespace Tranga.MangaConnectors;
public class Manganato : MangaConnector
{
public override string name { get; }
public Manganato(GlobalBase clone) : base(clone)
public Manganato(GlobalBase clone) : base(clone, "Manganato", ["en"])
{
this.name = "Manganato";
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
{
{1, 60}
}, clone);
this.downloadClient = new HttpDownloadClient(clone);
}
public override Publication[] GetPublications(string publicationTitle = "")
public override Manga[] GetManga(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*")).ToLower();
string requestUrl = $"https://manganato.com/search/story/{sanitizedTitle}";
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
string requestUrl = $"https://manganato.gg/search/story/{sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Publication>();
return Array.Empty<Manga>();
Publication[] publications = ParsePublicationsFromHtml(requestResult.result);
cachedPublications.AddRange(publications);
if (requestResult.htmlDocument is null)
return Array.Empty<Manga>();
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
private Publication[] ParsePublicationsFromHtml(Stream html)
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
{
StreamReader reader = new (html);
string htmlString = reader.ReadToEnd();
HtmlDocument document = new ();
document.LoadHtml(htmlString);
IEnumerable<HtmlNode> searchResults = document.DocumentNode.Descendants("div").Where(n => n.HasClass("search-story-item"));
List<HtmlNode> searchResults = document.DocumentNode.Descendants("div").Where(n => n.HasClass("story_item")).ToList();
Log($"{searchResults.Count} items.");
List<string> urls = new();
foreach (HtmlNode mangaResult in searchResults)
{
urls.Add(mangaResult.Descendants("a").First(n => n.HasClass("item-title")).GetAttributes()
.First(a => a.Name == "href").Value);
try
{
urls.Add(mangaResult.Descendants("h3").First(n => n.HasClass("story_name"))
.Descendants("a").First().GetAttributeValue("href", ""));
} catch
{
//failed to get a url, send it to the void
}
}
HashSet<Publication> ret = new();
HashSet<Manga> ret = new();
foreach (string url in urls)
{
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(url, 1);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Publication>();
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, url.Split('/')[^1]));
Manga? manga = GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
}
return ret.ToArray();
}
private Publication ParseSinglePublicationFromHtml(Stream html, string publicationId)
public override Manga? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://chapmanganato.com/{publicationId}");
}
public override Manga? GetMangaFromUrl(string url)
{
RequestResult requestResult =
downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
if (requestResult.htmlDocument is null)
return null;
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, url.Split('/')[^1], url);
}
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
StreamReader reader = new (html);
string htmlString = reader.ReadToEnd();
HtmlDocument document = new ();
document.LoadHtml(htmlString);
string status = "";
Dictionary<string, string> altTitles = new();
Dictionary<string, string>? links = null;
HashSet<string> tags = new();
string[] authors = Array.Empty<string>();
string originalLanguage = "";
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("story-info-right"));
HtmlNode infoNode = document.DocumentNode.Descendants("ul").First(d => d.HasClass("manga-info-text"));
string sortName = infoNode.Descendants("h1").First().InnerText;
HtmlNode infoTable = infoNode.Descendants().First(d => d.Name == "table");
foreach (HtmlNode row in infoTable.Descendants("tr"))
foreach (HtmlNode li in infoNode.Descendants("li"))
{
string key = row.SelectNodes("td").First().InnerText.ToLower();
string value = row.SelectNodes("td").Last().InnerText;
string keySanitized = string.Concat(Regex.Matches(key, "[a-z]"));
switch (keySanitized)
string text = li.InnerText.Trim().ToLower();
if (text.StartsWith("author(s) :"))
{
case "alternative":
string[] alts = value.Split(" ; ");
for(int i = 0; i < alts.Length; i++)
altTitles.Add(i.ToString(), alts[i]);
break;
case "authors":
authors = value.Split('-');
break;
case "status":
status = value;
break;
case "genres":
string[] genres = value.Split(" - ");
tags = genres.ToHashSet();
break;
authors = li.Descendants("a").Select(a => a.InnerText.Trim()).ToArray();
}
else if (text.StartsWith("status :"))
{
string status = text.Replace("status :", "").Trim().ToLower();
if (string.IsNullOrWhiteSpace(status))
releaseStatus = Manga.ReleaseStatusByte.Continuing;
else if (status == "ongoing")
releaseStatus = Manga.ReleaseStatusByte.Continuing;
else
releaseStatus = Enum.Parse<Manga.ReleaseStatusByte>(status, true);
}
else if (li.HasClass("genres"))
{
tags = li.Descendants("a").Select(a => a.InnerText.Trim()).ToHashSet();
}
}
string posterUrl = document.DocumentNode.Descendants("span").First(s => s.HasClass("info-image")).Descendants("img").First()
string posterUrl = document.DocumentNode.Descendants("div").First(s => s.HasClass("manga-info-pic")).Descendants("img").First()
.GetAttributes().First(a => a.Name == "src").Value;
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover, "https://www.manganato.gg/");
string description = document.DocumentNode.Descendants("div").First(d => d.HasClass("panel-story-info-description"))
string description = document.DocumentNode.SelectSingleNode("//div[@id='contentBox']")
.InnerText.Replace("Description :", "");
while (description.StartsWith('\n'))
description = description.Substring(1);
string yearString = document.DocumentNode.Descendants("li").Last(li => li.HasClass("a-h")).Descendants("span")
.First(s => s.HasClass("chapter-time")).InnerText;
int year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000;
return new Publication(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, status, publicationId);
string pattern = "MMM-dd-yyyy HH:mm";
HtmlNode? oldestChapter = document.DocumentNode
.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),' row ')]/span[@title]").MaxBy(
node => DateTime.ParseExact(node.GetAttributeValue("title", "Dec-31-2400 23:59"), pattern,
CultureInfo.InvariantCulture).Millisecond);
int year = DateTime.ParseExact(oldestChapter?.GetAttributeValue("title", "Dec 31 2400, 23:59")??"Dec 31 2400, 23:59", pattern,
CultureInfo.InvariantCulture).Year;
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
return manga;
}
public override Chapter[] GetChapters(Publication publication, string language="en")
public override Chapter[] GetChapters(Manga manga, string language="en")
{
Log($"Getting chapters {publication}");
string requestUrl = $"https://chapmanganato.com/{publication.publicationId}";
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
Log($"Getting chapters {manga}");
string requestUrl = manga.websiteUrl;
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>();
//Return Chapters ordered by Chapter-Number
NumberFormatInfo chapterNumberFormatInfo = new()
{
NumberDecimalSeparator = "."
};
List<Chapter> chapters = ParseChaptersFromHtml(publication, requestResult.result);
Log($"Got {chapters.Count} chapters. {publication}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
if (requestResult.htmlDocument is null)
return Array.Empty<Chapter>();
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
Log($"Got {chapters.Count} chapters. {manga}");
return chapters.Order().ToArray();
}
private List<Chapter> ParseChaptersFromHtml(Publication publication, Stream html)
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
{
StreamReader reader = new (html);
string htmlString = reader.ReadToEnd();
HtmlDocument document = new ();
document.LoadHtml(htmlString);
List<Chapter> ret = new();
HtmlNode chapterList = document.DocumentNode.Descendants("ul").First(l => l.HasClass("row-content-chapter"));
HtmlNode chapterList = document.DocumentNode.Descendants("div").First(l => l.HasClass("chapter-list"));
foreach (HtmlNode chapterInfo in chapterList.Descendants("li"))
Regex volRex = new(@"Vol\.([0-9]+).*");
Regex chapterRex = new(@"https:\/\/chapmanganato.[A-z]+\/manga-[A-z0-9]+\/chapter-([0-9\.]+)");
Regex nameRex = new(@"Chapter ([0-9]+(\.[0-9]+)*){1}:? (.*)");
foreach (HtmlNode chapterInfo in chapterList.Descendants("div").Where(x => x.HasClass("row")))
{
string fullString = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name")).InnerText;
string? volumeNumber = fullString.Contains("Vol.") ? fullString.Replace("Vol.", "").Split(' ')[0] : null;
string chapterNumber = fullString.Split(':')[0].Split("Chapter ")[1].Replace('-','.');
string chapterName = string.Concat(fullString.Split(':')[1..]);
string url = chapterInfo.Descendants("a").First(d => d.HasClass("chapter-name"))
.GetAttributeValue("href", "");
ret.Add(new Chapter(publication, chapterName, volumeNumber, chapterNumber, url));
string url = chapterInfo.Descendants("a").First().GetAttributeValue("href", "");
var name = chapterInfo.Descendants("a").First().InnerText.Trim();
string chapterName = nameRex.Match(name).Groups[3].Value;
string chapterNumber = Regex.Match(name, @"Chapter ([0-9]+(\.[0-9]+)*)").Groups[1].Value;
string? volumeNumber = Regex.Match(chapterName, @"Vol\.([0-9]+)").Groups[1].Value;
if (string.IsNullOrWhiteSpace(volumeNumber))
volumeNumber = "0";
try
{
ret.Add(new Chapter(manga, chapterName, volumeNumber, chapterNumber, url));
}
catch (Exception e)
{
Log($"Failed to load chapter {chapterNumber}: {e.Message}");
}
}
ret.Reverse();
return ret;
@ -173,28 +191,35 @@ public class Manganato : MangaConnector
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
Log($"Retrieving chapter-info {chapter} {chapter.parentPublication}");
string requestUrl = chapter.url;
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return requestResult.statusCode;
}
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.result);
Manga chapterParentManga = chapter.parentManga;
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
string requestUrl = chapter.url;
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
progressToken?.Cancel();
return requestResult.statusCode;
}
if (requestResult.htmlDocument is null)
{
progressToken?.Cancel();
return HttpStatusCode.InternalServerError;
}
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(imageUrls, chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, "https://chapmanganato.com/", progressToken:progressToken);
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, "https://www.manganato.gg", progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(Stream html)
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
{
StreamReader reader = new (html);
string htmlString = reader.ReadToEnd();
HtmlDocument document = new ();
document.LoadHtml(htmlString);
List<string> ret = new();
HtmlNode imageContainer =

View File

@ -1,277 +0,0 @@
using System.Globalization;
using System.Net;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using HtmlAgilityPack;
using Newtonsoft.Json;
using PuppeteerSharp;
using Tranga.Jobs;
namespace Tranga.MangaConnectors;
public class Mangasee : MangaConnector
{
public override string name { get; }
private IBrowser? _browser;
private const string ChromiumVersion = "1154303";
public Mangasee(GlobalBase clone) : base(clone)
{
this.name = "Mangasee";
this.downloadClient = new DownloadClient(new Dictionary<byte, int>()
{
{ 1, 60 }
}, clone);
Task d = new Task(DownloadBrowser);
d.Start();
}
private async void DownloadBrowser()
{
BrowserFetcher browserFetcher = new BrowserFetcher();
foreach(string rev in browserFetcher.LocalRevisions().Where(rev => rev != ChromiumVersion))
browserFetcher.Remove(rev);
if (!browserFetcher.LocalRevisions().Contains(ChromiumVersion))
{
Log("Downloading headless browser");
DateTime last = DateTime.Now.Subtract(TimeSpan.FromSeconds(5));
browserFetcher.DownloadProgressChanged += (_, args) =>
{
double currentBytes = Convert.ToDouble(args.BytesReceived) / Convert.ToDouble(args.TotalBytesToReceive);
if (args.TotalBytesToReceive == args.BytesReceived)
Log("Browser downloaded.");
else if (DateTime.Now > last.AddSeconds(1))
{
Log($"Browser download progress: {currentBytes:P2}");
last = DateTime.Now;
}
};
if (!browserFetcher.CanDownloadAsync(ChromiumVersion).Result)
{
Log($"Can't download browser version {ChromiumVersion}");
throw new Exception();
}
await browserFetcher.DownloadAsync(ChromiumVersion);
}
Log("Starting Browser.");
this._browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true,
ExecutablePath = browserFetcher.GetExecutablePath(ChromiumVersion),
Args = new [] {
"--disable-gpu",
"--disable-dev-shm-usage",
"--disable-setuid-sandbox",
"--no-sandbox"}
});
}
public override Publication[] GetPublications(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string requestUrl = $"https://mangasee123.com/_search.php";
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, 1);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Publication>();
Publication[] publications = ParsePublicationsFromHtml(requestResult.result, publicationTitle);
cachedPublications.AddRange(publications);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
private Publication[] ParsePublicationsFromHtml(Stream html, string publicationTitle)
{
string jsonString = new StreamReader(html).ReadToEnd();
List<SearchResultItem> result = JsonConvert.DeserializeObject<List<SearchResultItem>>(jsonString)!;
Dictionary<SearchResultItem, int> queryFiltered = new();
foreach (SearchResultItem resultItem in result)
{
int matches = resultItem.GetMatches(publicationTitle);
if (matches > 0)
queryFiltered.TryAdd(resultItem, matches);
}
queryFiltered = queryFiltered.Where(item => item.Value >= publicationTitle.Split(' ').Length - 1)
.ToDictionary(item => item.Key, item => item.Value);
Log($"Retrieved {queryFiltered.Count} publications.");
HashSet<Publication> ret = new();
List<SearchResultItem> orderedFiltered =
queryFiltered.OrderBy(item => item.Value).ToDictionary(item => item.Key, item => item.Value).Keys.ToList();
uint index = 1;
foreach (SearchResultItem orderedItem in orderedFiltered)
{
DownloadClient.RequestResult requestResult =
downloadClient.MakeRequest($"https://mangasee123.com/manga/{orderedItem.i}", 1);
if ((int)requestResult.statusCode >= 200 || (int)requestResult.statusCode < 300)
{
Log($"Retrieving Publication info: {orderedItem.s} {index++}/{orderedFiltered.Count}");
ret.Add(ParseSinglePublicationFromHtml(requestResult.result, orderedItem.s, orderedItem.i, orderedItem.a));
}
}
return ret.ToArray();
}
private Publication ParseSinglePublicationFromHtml(Stream html, string sortName, string publicationId, string[] a)
{
StreamReader reader = new (html);
HtmlDocument document = new ();
document.LoadHtml(reader.ReadToEnd());
string originalLanguage = "", status = "";
Dictionary<string, string> altTitles = new(), links = new();
HashSet<string> tags = new();
HtmlNode posterNode =
document.DocumentNode.Descendants("img").First(img => img.HasClass("img-fluid") && img.HasClass("bottom-5"));
string posterUrl = posterNode.GetAttributeValue("src", "");
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, 1);
HtmlNode attributes = document.DocumentNode.Descendants("div")
.First(div => div.HasClass("col-md-9") && div.HasClass("col-sm-8") && div.HasClass("top-5"))
.Descendants("ul").First();
HtmlNode[] authorsNodes = attributes.Descendants("li")
.First(node => node.InnerText.Contains("author(s):", StringComparison.CurrentCultureIgnoreCase))
.Descendants("a").ToArray();
List<string> authors = new();
foreach(HtmlNode authorNode in authorsNodes)
authors.Add(authorNode.InnerText);
HtmlNode[] genreNodes = attributes.Descendants("li")
.First(node => node.InnerText.Contains("genre(s):", StringComparison.CurrentCultureIgnoreCase))
.Descendants("a").ToArray();
foreach (HtmlNode genreNode in genreNodes)
tags.Add(genreNode.InnerText);
HtmlNode yearNode = attributes.Descendants("li")
.First(node => node.InnerText.Contains("released:", StringComparison.CurrentCultureIgnoreCase))
.Descendants("a").First();
int year = Convert.ToInt32(yearNode.InnerText);
HtmlNode[] statusNodes = attributes.Descendants("li")
.First(node => node.InnerText.Contains("status:", StringComparison.CurrentCultureIgnoreCase))
.Descendants("a").ToArray();
foreach(HtmlNode statusNode in statusNodes)
if (statusNode.InnerText.Contains("publish", StringComparison.CurrentCultureIgnoreCase))
status = statusNode.InnerText.Split(' ')[0];
HtmlNode descriptionNode = attributes.Descendants("li").First(node => node.InnerText.Contains("description:", StringComparison.CurrentCultureIgnoreCase)).Descendants("div").First();
string description = descriptionNode.InnerText;
int i = 0;
foreach(string at in a)
altTitles.Add((i++).ToString(), at);
return new Publication(sortName, authors, description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, status, publicationId);
}
// ReSharper disable once ClassNeverInstantiated.Local Will be instantiated during deserialization
private class SearchResultItem
{
public string i { get; init; }
public string s { get; init; }
public string[] a { get; init; }
[JsonConstructor]
public SearchResultItem(string i, string s, string[] a)
{
this.i = i;
this.s = s;
this.a = a;
}
public int GetMatches(string title)
{
int ret = 0;
Regex cleanRex = new("[A-z0-9]*");
string[] badWords = { "a", "an", "no", "ni", "so", "as", "and", "the", "of", "that", "in", "is", "for" };
string[] titleTerms = title.Split(new[] { ' ', '-' }).Where(str => !badWords.Contains(str)).ToArray();
foreach (Match matchTerm in cleanRex.Matches(this.i))
ret += titleTerms.Count(titleTerm =>
titleTerm.Equals(matchTerm.Value, StringComparison.OrdinalIgnoreCase));
foreach (Match matchTerm in cleanRex.Matches(this.s))
ret += titleTerms.Count(titleTerm =>
titleTerm.Equals(matchTerm.Value, StringComparison.OrdinalIgnoreCase));
foreach(string alt in this.a)
foreach (Match matchTerm in cleanRex.Matches(alt))
ret += titleTerms.Count(titleTerm =>
titleTerm.Equals(matchTerm.Value, StringComparison.OrdinalIgnoreCase));
return ret;
}
}
public override Chapter[] GetChapters(Publication publication, string language="en")
{
Log($"Getting chapters {publication}");
XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{publication.publicationId}.xml");
XElement[] chapterItems = doc.Descendants("item").ToArray();
List<Chapter> chapters = new();
foreach (XElement chapter in chapterItems)
{
string volumeNumber = "1";
string chapterName = chapter.Descendants("title").First().Value;
string chapterNumber = Regex.Matches(chapterName, "[0-9]+")[^1].ToString();
string url = chapter.Descendants("link").First().Value;
url = url.Replace(Regex.Matches(url,"(-page-[0-9])")[0].ToString(),"");
chapters.Add(new Chapter(publication, "", volumeNumber, chapterNumber, url));
}
//Return Chapters ordered by Chapter-Number
NumberFormatInfo chapterNumberFormatInfo = new()
{
NumberDecimalSeparator = "."
};
Log($"Got {chapters.Count} chapters. {publication}");
return chapters.OrderBy(chapter => Convert.ToSingle(chapter.chapterNumber, chapterNumberFormatInfo)).ToArray();
}
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
return HttpStatusCode.RequestTimeout;
while (this._browser is null && !(progressToken?.cancellationRequested??false))
{
Log("Waiting for headless browser to download...");
Thread.Sleep(1000);
}
if (progressToken?.cancellationRequested??false)
return HttpStatusCode.RequestTimeout;
Log($"Retrieving chapter-info {chapter} {chapter.parentPublication}");
IPage page = _browser!.NewPageAsync().Result;
IResponse response = page.GoToAsync(chapter.url).Result;
if (response.Ok)
{
HtmlDocument document = new ();
document.LoadHtml(page.GetContentAsync().Result);
HtmlNode gallery = document.DocumentNode.Descendants("div").First(div => div.HasClass("ImageGallery"));
HtmlNode[] images = gallery.Descendants("img").Where(img => img.HasClass("img-fluid")).ToArray();
List<string> urls = new();
foreach(HtmlNode galleryImage in images)
urls.Add(galleryImage.GetAttributeValue("src", ""));
string comicInfoPath = Path.GetTempFileName();
File.WriteAllText(comicInfoPath, chapter.GetComicInfoXmlString());
return DownloadChapterImages(urls.ToArray(), chapter.GetArchiveFilePath(settings.downloadLocation), 1, comicInfoPath, progressToken:progressToken);
}
return response.Status;
}
}

View File

@ -0,0 +1,240 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.Jobs;
namespace Tranga.MangaConnectors;
public class Mangaworld: MangaConnector
{
public Mangaworld(GlobalBase clone) : base(clone, "Mangaworld", ["it"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
public override Manga[] GetManga(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
string requestUrl = $"https://www.mangaworld.ac/archive?keyword={sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Manga>();
if (requestResult.htmlDocument is null)
return Array.Empty<Manga>();
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
{
if (!document.DocumentNode.SelectSingleNode("//div[@class='comics-grid']").ChildNodes
.Any(node => node.HasClass("entry")))
return Array.Empty<Manga>();
List<string> urls = document.DocumentNode
.SelectNodes(
"//div[@class='comics-grid']//div[@class='entry']//a[contains(concat(' ',normalize-space(@class),' '),'thumb')]")
.Select(thumb => thumb.GetAttributeValue("href", "")).ToList();
HashSet<Manga> ret = new();
foreach (string url in urls)
{
Manga? manga = GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
}
return ret.ToArray();
}
public override Manga? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://www.mangaworld.ac/manga/{publicationId}");
}
public override Manga? GetMangaFromUrl(string url)
{
RequestResult requestResult =
downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return null;
if (requestResult.htmlDocument is null)
return null;
Regex idRex = new (@"https:\/\/www\.mangaworld\.[a-z]{0,63}\/manga\/([0-9]+\/[0-9A-z\-]+).*");
string id = idRex.Match(url).Groups[1].Value;
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, id, url);
}
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
Dictionary<string, string> altTitles = new();
Dictionary<string, string>? links = null;
string originalLanguage = "";
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
HtmlNode infoNode = document.DocumentNode.Descendants("div").First(d => d.HasClass("info"));
string sortName = infoNode.Descendants("h1").First().InnerText;
HtmlNode metadata = infoNode.Descendants().First(d => d.HasClass("meta-data"));
HtmlNode altTitlesNode = metadata.SelectSingleNode("//span[text()='Titoli alternativi: ' or text()='Titolo alternativo: ']/..").ChildNodes[1];
string[] alts = altTitlesNode.InnerText.Split(", ");
for(int i = 0; i < alts.Length; i++)
altTitles.Add(i.ToString(), alts[i]);
HtmlNode genresNode =
metadata.SelectSingleNode("//span[text()='Generi: ' or text()='Genero: ']/..");
HashSet<string> tags = genresNode.SelectNodes("a").Select(node => node.InnerText).ToHashSet();
HtmlNode authorsNode =
metadata.SelectSingleNode("//span[text()='Autore: ' or text()='Autori: ']/..");
string[] authors = authorsNode.SelectNodes("a").Select(node => node.InnerText).ToArray();
string status = metadata.SelectSingleNode("//span[text()='Stato: ']/..").SelectNodes("a").First().InnerText;
// ReSharper disable 5 times StringLiteralTypo
switch (status.ToLower())
{
case "cancellato": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
case "in pausa": releaseStatus = Manga.ReleaseStatusByte.OnHiatus; break;
case "droppato": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
case "finito": releaseStatus = Manga.ReleaseStatusByte.Completed; break;
case "in corso": releaseStatus = Manga.ReleaseStatusByte.Continuing; break;
}
string posterUrl = document.DocumentNode.SelectSingleNode("//img[@class='rounded']").GetAttributeValue("src", "");
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId.Replace('/', '-'), RequestType.MangaCover);
string description = document.DocumentNode.SelectSingleNode("//div[@id='noidungm']").InnerText;
string yearString = metadata.SelectSingleNode("//span[text()='Anno di uscita: ']/..").SelectNodes("a").First().InnerText;
int year = Convert.ToInt32(yearString);
Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
return manga;
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
Log($"Getting chapters {manga}");
string requestUrl = $"https://www.mangaworld.ac/manga/{manga.publicationId}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>();
//Return Chapters ordered by Chapter-Number
if (requestResult.htmlDocument is null)
return Array.Empty<Chapter>();
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
Log($"Got {chapters.Count} chapters. {manga}");
return chapters.Order().ToArray();
}
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
{
List<Chapter> ret = new();
HtmlNode chaptersWrapper =
document.DocumentNode.SelectSingleNode(
"//div[contains(concat(' ',normalize-space(@class),' '),'chapters-wrapper')]");
Regex volumeRex = new(@"[Vv]olume ([0-9]+).*");
Regex chapterRex = new(@"[Cc]apitolo ([0-9]+(?:\.[0-9]+)?).*");
Regex idRex = new(@".*\/read\/([a-z0-9]+)(?:[?\/].*)?");
if (chaptersWrapper.Descendants("div").Any(descendant => descendant.HasClass("volume-element")))
{
foreach (HtmlNode volNode in document.DocumentNode.SelectNodes("//div[contains(concat(' ',normalize-space(@class),' '),'volume-element')]"))
{
string volume = volumeRex.Match(volNode.SelectNodes("div").First(node => node.HasClass("volume")).SelectSingleNode("p").InnerText).Groups[1].Value;
foreach (HtmlNode chNode in volNode.SelectNodes("div").First(node => node.HasClass("volume-chapters")).SelectNodes("div"))
{
string number = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
try
{
ret.Add(new Chapter(manga, null, volume, number, url, id));
}
catch (Exception e)
{
Log($"Failed to load chapter {number}: {e.Message}");
}
}
}
}
else
{
foreach (HtmlNode chNode in chaptersWrapper.SelectNodes("div").Where(node => node.HasClass("chapter")))
{
string number = chapterRex.Match(chNode.SelectSingleNode("a").SelectSingleNode("span").InnerText).Groups[1].Value;
string url = chNode.SelectSingleNode("a").GetAttributeValue("href", "");
string id = idRex.Match(chNode.SelectSingleNode("a").GetAttributeValue("href", "")).Groups[1].Value;
try
{
ret.Add(new Chapter(manga, null, null, number, url, id));
}
catch (Exception e)
{
Log($"Failed to load chapter {number}: {e.Message}");
}
}
}
ret.Reverse();
return ret;
}
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Manga chapterParentManga = chapter.parentManga;
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
string requestUrl = $"{chapter.url}?style=list";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
progressToken?.Cancel();
return requestResult.statusCode;
}
if (requestResult.htmlDocument is null)
{
progressToken?.Cancel();
return HttpStatusCode.InternalServerError;
}
string[] imageUrls = ParseImageUrlsFromHtml(requestResult.htmlDocument);
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage,"https://www.mangaworld.bz/", progressToken:progressToken);
}
private string[] ParseImageUrlsFromHtml(HtmlDocument document)
{
List<string> ret = new();
HtmlNode imageContainer =
document.DocumentNode.SelectSingleNode("//div[@id='page']");
foreach(HtmlNode imageNode in imageContainer.Descendants("img"))
ret.Add(imageNode.GetAttributeValue("src", ""));
return ret.ToArray();
}
}

View File

@ -0,0 +1,203 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.Jobs;
namespace Tranga.MangaConnectors;
public class ManhuaPlus : MangaConnector
{
public ManhuaPlus(GlobalBase clone) : base(clone, "ManhuaPlus", ["en"])
{
this.downloadClient = new ChromiumDownloadClient(clone);
}
public override Manga[] GetManga(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(str => str.Length > 0)).ToLower();
string requestUrl = $"https://manhuaplus.org/search?keyword={sanitizedTitle}";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Manga>();
if (requestResult.htmlDocument is null)
return Array.Empty<Manga>();
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
{
if (document.DocumentNode.SelectSingleNode("//h1/../..").ChildNodes//I already want to not.
.Any(node => node.InnerText.Contains("No manga found")))
return Array.Empty<Manga>();
List<string> urls = document.DocumentNode
.SelectNodes("//h1/../..//a[contains(@href, 'https://manhuaplus.org/manga/') and contains(concat(' ',normalize-space(@class),' '),' clamp ') and not(contains(@href, '/chapter'))]")
.Select(mangaNode => mangaNode.GetAttributeValue("href", "")).ToList();
logger?.WriteLine($"Got {urls.Count} urls.");
HashSet<Manga> ret = new();
foreach (string url in urls)
{
Manga? manga = GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
}
return ret.ToArray();
}
public override Manga? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://manhuaplus.org/manga/{publicationId}");
}
public override Manga? GetMangaFromUrl(string url)
{
Regex publicationIdRex = new(@"https:\/\/manhuaplus.org\/manga\/(.*)(\/.*)*");
string publicationId = publicationIdRex.Match(url).Groups[1].Value;
RequestResult requestResult = this.downloadClient.MakeRequest(url, RequestType.MangaInfo);
if((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 && requestResult.htmlDocument is not null && requestResult.redirectedToUrl != "https://manhuaplus.org/home") //When manga doesnt exists it redirects to home
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
return null;
}
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
string originalLanguage = "", status = "";
Dictionary<string, string> altTitles = new(), links = new();
HashSet<string> tags = new();
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("/html/body/main/div/div/div[2]/div[1]/figure/a/img");//BRUH
Regex posterRex = new(@".*(\/uploads/covers/[a-zA-Z0-9\-\._\~\!\$\&\'\(\)\*\+\,\;\=\:\@]+).*");
string posterUrl = $"https://manhuaplus.org/{posterRex.Match(posterNode.GetAttributeValue("src", "")).Groups[1].Value}";
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover);
HtmlNode titleNode = document.DocumentNode.SelectSingleNode("//h1");
string sortName = titleNode.InnerText.Replace("\n", "");
List<string> authors = new();
try
{
HtmlNode[] authorsNodes = document.DocumentNode
.SelectNodes("//a[contains(@href, 'https://manhuaplus.org/authors/')]")
.ToArray();
foreach (HtmlNode authorNode in authorsNodes)
authors.Add(authorNode.InnerText);
}
catch (ArgumentNullException e)
{
Log("No authors found.");
}
try
{
HtmlNode[] genreNodes = document.DocumentNode
.SelectNodes("//a[contains(@href, 'https://manhuaplus.org/genres/')]").ToArray();
foreach (HtmlNode genreNode in genreNodes)
tags.Add(genreNode.InnerText.Replace("\n", ""));
}
catch (ArgumentNullException e)
{
Log("No genres found");
}
Regex yearRex = new(@"(?:[0-9]{1,2}\/){2}([0-9]{2,4}) [0-9]{1,2}:[0-9]{1,2}");
HtmlNode yearNode = document.DocumentNode.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-clock ')]/../span");
Match match = yearRex.Match(yearNode.InnerText);
int year = match.Success && match.Groups[1].Success ? int.Parse(match.Groups[1].Value) : 1960;
status = document.DocumentNode.SelectSingleNode("//aside//i[contains(concat(' ',normalize-space(@class),' '),' fa-rss ')]/../span").InnerText.Replace("\n", "");
switch (status.ToLower())
{
case "cancelled": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
case "hiatus": releaseStatus = Manga.ReleaseStatusByte.OnHiatus; break;
case "discontinued": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
case "complete": releaseStatus = Manga.ReleaseStatusByte.Completed; break;
case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break;
}
HtmlNode descriptionNode = document.DocumentNode
.SelectSingleNode("//div[@id='syn-target']");
string description = descriptionNode.InnerText;
Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
return manga;
}
public override Chapter[] GetChapters(Manga manga, string language="en")
{
Log($"Getting chapters {manga}");
RequestResult result = downloadClient.MakeRequest($"https://manhuaplus.org/manga/{manga.publicationId}", RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
{
return Array.Empty<Chapter>();
}
HtmlNodeCollection chapterNodes = result.htmlDocument.DocumentNode.SelectNodes("//li[contains(concat(' ',normalize-space(@class),' '),' chapter ')]//a");
string[] urls = chapterNodes.Select(node => node.GetAttributeValue("href", "")).ToArray();
Regex urlRex = new (@".*\/chapter-([0-9\-]+).*");
List<Chapter> chapters = new();
foreach (string url in urls)
{
Match rexMatch = urlRex.Match(url);
string volumeNumber = "1";
string chapterNumber = rexMatch.Groups[1].Value;
string fullUrl = url;
try
{
chapters.Add(new Chapter(manga, "", volumeNumber, chapterNumber, fullUrl));
}
catch (Exception e)
{
Log($"Failed to load chapter {chapterNumber}: {e.Message}");
}
}
//Return Chapters ordered by Chapter-Number
Log($"Got {chapters.Count} chapters. {manga}");
return chapters.Order().ToArray();
}
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Manga chapterParentManga = chapter.parentManga;
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
RequestResult requestResult = this.downloadClient.MakeRequest(chapter.url, RequestType.Default);
if (requestResult.htmlDocument is null)
{
progressToken?.Cancel();
return HttpStatusCode.RequestTimeout;
}
HtmlDocument document = requestResult.htmlDocument;
HtmlNode[] images = document.DocumentNode.SelectNodes("//a[contains(concat(' ',normalize-space(@class),' '),' readImg ')]/img").ToArray();
List<string> urls = images.Select(node => node.GetAttributeValue("src", "")).ToList();
return DownloadChapterImages(urls.ToArray(), chapter, RequestType.MangaImage, progressToken:progressToken);
}
}

View File

@ -0,0 +1,27 @@
using System.Net;
using HtmlAgilityPack;
namespace Tranga.MangaConnectors;
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;
}
}

View File

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

View File

@ -0,0 +1,273 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.Jobs;
namespace Tranga.MangaConnectors;
public class Webtoons : MangaConnector
{
public Webtoons(GlobalBase clone) : base(clone, "Webtoons", ["en"])
{
this.downloadClient = new HttpDownloadClient(clone);
}
// Done
public override Manga[] GetManga(string publicationTitle = "")
{
string sanitizedTitle = string.Join(' ', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
Log($"Searching Publications. Term=\"{publicationTitle}\"");
string requestUrl = $"https://www.webtoons.com/en/search?keyword={sanitizedTitle}&searchType=WEBTOON";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) {
Log($"Failed to retrieve site");
return Array.Empty<Manga>();
}
if (requestResult.htmlDocument is null)
{
Log($"Failed to retrieve site");
return Array.Empty<Manga>();
}
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
// Done
public override Manga? GetMangaFromId(string publicationId)
{
PublicationManager pb = new PublicationManager(publicationId);
return GetMangaFromUrl($"https://www.webtoons.com/en/{pb.Category}/{pb.Title}/list?title_no={pb.Id}");
}
// Done
public override Manga? GetMangaFromUrl(string url)
{
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) {
return null;
}
if (requestResult.htmlDocument is null)
{
Log($"Failed to retrieve site");
return null;
}
Regex regex = new Regex(@".*webtoons\.com\/en\/(?<category>[^\/]+)\/(?<title>[^\/]+)\/list\?title_no=(?<id>\d+).*");
Match match = regex.Match(url);
if(match.Success) {
PublicationManager pm = new PublicationManager(match.Groups["title"].Value, match.Groups["category"].Value, match.Groups["id"].Value);
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, pm.getPublicationId(), url);
}
Log($"Failed match Regex ID");
return null;
}
// Done
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
{
HtmlNode mangaList = document.DocumentNode.SelectSingleNode("//ul[contains(@class, 'card_lst')]");
if (!mangaList.ChildNodes.Any(node => node.Name == "li")) {
Log($"Failed to parse publication");
return Array.Empty<Manga>();
}
List<string> urls = document.DocumentNode
.SelectNodes("//ul[contains(@class, 'card_lst')]/li/a")
.Select(node => node.GetAttributeValue("href", "https://www.webtoons.com"))
.ToList();
HashSet<Manga> ret = new();
foreach (string url in urls)
{
Manga? manga = GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
}
return ret.ToArray();
}
private string capitalizeString(string str = "") {
if(str.Length == 0) return "";
if(str.Length == 1) return str.ToUpper();
return char.ToUpper(str[0]) + str.Substring(1).ToLower();
}
// Done
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
HtmlNode infoNode1 = document.DocumentNode.SelectSingleNode("//*[@id='content']/div[2]/div[1]/div[1]");
HtmlNode infoNode2 = document.DocumentNode.SelectSingleNode("//*[@id='content']/div[2]/div[2]/div[2]");
string sortName = infoNode1.SelectSingleNode(".//h1[contains(@class, 'subj')]").InnerText;
string description = infoNode2.SelectSingleNode(".//p[contains(@class, 'summary')]")
.InnerText.Trim();
HtmlNode posterNode = document.DocumentNode.SelectSingleNode("//div[contains(@class, 'detail_body') and contains(@class, 'banner')]");
Regex regex = new Regex(@"url\('(?<url>.*?)'\)");
Match match = regex.Match(posterNode.GetAttributeValue("style", ""));
string posterUrl = match.Groups["url"].Value;
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover, websiteUrl);
string genre = infoNode1.SelectSingleNode(".//h2[contains(@class, 'genre')]")
.InnerText.Trim();
string[] tags = [ genre ];
List<HtmlNode> authorsNodes = infoNode1.SelectSingleNode(".//div[contains(@class, 'author_area')]").Descendants("a").ToList();
List<string> authors = authorsNodes.Select(node => node.InnerText.Trim()).ToList();
string originalLanguage = "";
int year = DateTime.Now.Year;
string status1 = infoNode2.SelectSingleNode(".//p").InnerText;
string status2 = infoNode2.SelectSingleNode(".//p/span").InnerText;
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
if(status2.Length == 0 || status1.ToLower() == "completed") {
releaseStatus = Manga.ReleaseStatusByte.Completed;
} else if(status2.ToLower() == "up") {
releaseStatus = Manga.ReleaseStatusByte.Continuing;
}
Manga manga = new(sortName, authors, description, new Dictionary<string, string>(), tags, posterUrl, coverFileNameInCache, new Dictionary<string, string>(),
year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl);
AddMangaToCache(manga);
return manga;
}
// Done
public override Chapter[] GetChapters(Manga manga, string language = "en")
{
PublicationManager pm = new PublicationManager(manga.publicationId);
string requestUrl = $"https://www.webtoons.com/en/{pm.Category}/{pm.Title}/list?title_no={pm.Id}";
// Leaving this in for verification if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Chapter>();
// Get number of pages
int pages = requestResult.htmlDocument.DocumentNode
.SelectNodes("//div[contains(@class, 'paginate')]/a")
.ToList()
.Count;
List<Chapter> chapters = new List<Chapter>();
for(int page = 1; page <= pages; page++) {
string pageRequestUrl = $"{requestUrl}&page={page}";
chapters.AddRange(ParseChaptersFromHtml(manga, pageRequestUrl));
}
Log($"Got {chapters.Count} chapters. {manga}");
return chapters.Order().ToArray();
}
// Done
private List<Chapter> ParseChaptersFromHtml(Manga manga, string mangaUrl)
{
RequestResult result = downloadClient.MakeRequest(mangaUrl, RequestType.Default);
if ((int)result.statusCode < 200 || (int)result.statusCode >= 300 || result.htmlDocument is null)
{
Log("Failed to load site");
return new List<Chapter>();
}
List<Chapter> ret = new();
foreach (HtmlNode chapterInfo in result.htmlDocument.DocumentNode.SelectNodes("//ul/li[contains(@class, '_episodeItem')]"))
{
HtmlNode infoNode = chapterInfo.SelectSingleNode(".//a");
string url = infoNode.GetAttributeValue("href", "");
string id = chapterInfo.GetAttributeValue("id", "");
if(id == "") continue;
string? volumeNumber = null;
string chapterNumber = chapterInfo.GetAttributeValue("data-episode-no", "");
if(chapterNumber == "") continue;
string chapterName = infoNode.SelectSingleNode(".//span[contains(@class, 'subj')]/span").InnerText.Trim();
ret.Add(new Chapter(manga, chapterName, volumeNumber, chapterNumber, url));
}
return ret;
}
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Manga chapterParentManga = chapter.parentManga;
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
string requestUrl = chapter.url;
// Leaving this in to check if the page exists
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
progressToken?.Cancel();
return requestResult.statusCode;
}
string[] imageUrls = ParseImageUrlsFromHtml(requestUrl);
return DownloadChapterImages(imageUrls, chapter, RequestType.MangaImage, progressToken:progressToken, referrer: requestUrl);
}
private string[] ParseImageUrlsFromHtml(string mangaUrl)
{
RequestResult requestResult =
downloadClient.MakeRequest(mangaUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
{
return Array.Empty<string>();
}
if (requestResult.htmlDocument is null)
{
Log($"Failed to retrieve site");
return Array.Empty<string>();
}
return requestResult.htmlDocument.DocumentNode
.SelectNodes("//*[@id='_imageList']/img")
.Select(node =>
node.GetAttributeValue("data-url", ""))
.ToArray();
}
}
internal class PublicationManager {
public PublicationManager(string title = "", string category = "", string id = "") {
this.Title = title;
this.Category = category;
this.Id = id;
}
public PublicationManager(string publicationId) {
string[] parts = publicationId.Split("|");
if(parts.Length == 3) {
this.Title = parts[0];
this.Category = parts[1];
this.Id = parts[2];
} else {
this.Title = "";
this.Category = "";
this.Id = "";
}
}
public string getPublicationId() {
return $"{this.Title}|{this.Category}|{this.Id}";
}
public string Title { get; set; }
public string Category { get; set; }
public string Id { get; set; }
}

View File

@ -0,0 +1,215 @@
using System.Net;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using Tranga.Jobs;
namespace Tranga.MangaConnectors;
public class Weebcentral : MangaConnector
{
private readonly string _baseUrl = "https://weebcentral.com";
private readonly string[] _filterWords =
{ "a", "the", "of", "as", "to", "no", "for", "on", "with", "be", "and", "in", "wa", "at", "be", "ni" };
public Weebcentral(GlobalBase clone) : base(clone, "Weebcentral", ["en"])
{
downloadClient = new ChromiumDownloadClient(clone);
}
public override Manga[] GetManga(string publicationTitle = "")
{
Log($"Searching Publications. Term=\"{publicationTitle}\"");
const int limit = 32; //How many values we want returned at once
int offset = 0; //"Page"
string requestUrl =
$"{_baseUrl}/search/data?limit={limit}&offset={offset}&text={publicationTitle}&sort=Best+Match&order=Ascending&official=Any&display_mode=Minimal%20Display";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300 ||
requestResult.htmlDocument == null)
{
Log($"Failed to retrieve search: {requestResult.statusCode}");
return [];
}
Manga[] publications = ParsePublicationsFromHtml(requestResult.htmlDocument);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
}
private Manga[] ParsePublicationsFromHtml(HtmlDocument document)
{
if (document.DocumentNode.SelectNodes("//article") == null)
return [];
List<string> urls = document.DocumentNode.SelectNodes("/html/body/article/a[@class='link link-hover tooltip tooltip-bottom']")
.Select(elem => elem.GetAttributeValue("href", "")).ToList();
HashSet<Manga> ret = new();
foreach (string url in urls)
{
Manga? manga = GetMangaFromUrl(url);
if (manga is not null)
ret.Add((Manga)manga);
}
return ret.ToArray();
}
public override Manga? GetMangaFromUrl(string url)
{
Regex publicationIdRex = new(@"https:\/\/weebcentral\.com\/series\/(\w*)\/(.*)");
string publicationId = publicationIdRex.Match(url).Groups[1].Value;
RequestResult requestResult = downloadClient.MakeRequest(url, RequestType.MangaInfo);
if ((int)requestResult.statusCode < 300 && (int)requestResult.statusCode >= 200 &&
requestResult.htmlDocument is not null)
return ParseSinglePublicationFromHtml(requestResult.htmlDocument, publicationId, url);
return null;
}
private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publicationId, string websiteUrl)
{
HtmlNode? posterNode =
document.DocumentNode.SelectSingleNode("//section[@class='flex items-center justify-center']/picture/img");
string posterUrl = posterNode?.GetAttributeValue("src", "") ?? "";
string coverFileNameInCache = SaveCoverImageToCache(posterUrl, publicationId, RequestType.MangaCover);
HtmlNode? titleNode = document.DocumentNode.SelectSingleNode("//section/h1");
string sortName = titleNode?.InnerText ?? "Undefined";
HtmlNode[] authorsNodes =
document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Author(s): ']/span")?.ToArray() ?? [];
List<string> authors = authorsNodes.Select(n => n.InnerText).ToList();
HtmlNode[] genreNodes =
document.DocumentNode.SelectNodes("//ul/li[strong/text() = 'Tags(s): ']/span")?.ToArray() ?? [];
HashSet<string> tags = genreNodes.Select(n => n.InnerText).ToHashSet();
HtmlNode? statusNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Status: ']/a");
string status = statusNode?.InnerText ?? "";
Log("unable to parse status");
Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased;
switch (status.ToLower())
{
case "cancelled": releaseStatus = Manga.ReleaseStatusByte.Cancelled; break;
case "hiatus": releaseStatus = Manga.ReleaseStatusByte.OnHiatus; break;
case "complete": releaseStatus = Manga.ReleaseStatusByte.Completed; break;
case "ongoing": releaseStatus = Manga.ReleaseStatusByte.Continuing; break;
}
HtmlNode? yearNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Released: ']/span");
int year = Convert.ToInt32(yearNode?.InnerText ?? "0");
HtmlNode? descriptionNode = document.DocumentNode.SelectSingleNode("//ul/li[strong/text() = 'Description']/p");
string description = descriptionNode?.InnerText ?? "Undefined";
HtmlNode[] altTitleNodes = document.DocumentNode
.SelectNodes("//ul/li[strong/text() = 'Associated Name(s)']/ul/li")?.ToArray() ?? [];
Dictionary<string, string> altTitles = new(), links = new();
for (int i = 0; i < altTitleNodes.Length; i++)
altTitles.Add(i.ToString(), altTitleNodes[i].InnerText);
string originalLanguage = "";
Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl,
coverFileNameInCache, links,
year, originalLanguage, publicationId, releaseStatus, websiteUrl);
AddMangaToCache(manga);
return manga;
}
public override Manga? GetMangaFromId(string publicationId)
{
return GetMangaFromUrl($"https://weebcentral.com/series/{publicationId}");
}
public override Chapter[] GetChapters(Manga manga, string language = "en")
{
Log($"Getting chapters {manga}");
string requestUrl = $"{_baseUrl}/series/{manga.publicationId}/full-chapter-list";
RequestResult requestResult =
downloadClient.MakeRequest(requestUrl, RequestType.Default);
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return [];
//Return Chapters ordered by Chapter-Number
if (requestResult.htmlDocument is null)
return [];
List<Chapter> chapters = ParseChaptersFromHtml(manga, requestResult.htmlDocument);
Log($"Got {chapters.Count} chapters. {manga}");
return chapters.OrderByDescending(c => c.name).ThenBy(c => c.volumeNumber).ThenBy(c => c.chapterNumber).ToArray();
}
private List<Chapter> ParseChaptersFromHtml(Manga manga, HtmlDocument document)
{
HtmlNode? chaptersWrapper = document.DocumentNode.SelectSingleNode("/html/body");
Regex chapterRex = new(@"(\d+(?:\.\d+)*)");
Regex chapterNameRex = new(@"(\w* )+");
Regex idRex = new(@"https:\/\/weebcentral\.com\/chapters\/(\w*)");
List<Chapter> ret = chaptersWrapper.Descendants("a").Select(elem =>
{
string url = elem.GetAttributeValue("href", "") ?? "Undefined";
if (!url.StartsWith("https://") && !url.StartsWith("http://"))
return new Chapter(manga, null, null, "-1", "undefined");
Match idMatch = idRex.Match(url);
string? id = idMatch.Success ? idMatch.Groups[1].Value : null;
string chapterNode = elem.SelectSingleNode("span[@class='grow flex items-center gap-2']/span")?.InnerText ??
"Undefined";
MatchCollection chapterNumberMatch = chapterRex.Matches(chapterNode);
string chapterNumber = chapterNumberMatch.Count > 0 ? chapterNumberMatch[^1].Groups[1].Value : "-1";
MatchCollection chapterNameMatch = chapterNameRex.Matches(chapterNode);
string chapterName = chapterNameMatch.Count > 0
? string.Join(" - ",
chapterNameMatch.Select(m => m.Groups[1].Value.Trim())
.Where(name => name.Length > 0 && !name.Equals("Chapter", StringComparison.OrdinalIgnoreCase)).ToArray()).Trim()
: "";
return new Chapter(manga, chapterName != "" ? chapterName : null, null, chapterNumber, url, id);
}).Where(elem => elem.chapterNumber != -1 && elem.url != "undefined").ToList();
ret.Reverse();
return ret;
}
public override HttpStatusCode DownloadChapter(Chapter chapter, ProgressToken? progressToken = null)
{
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Manga chapterParentManga = chapter.parentManga;
if (progressToken?.cancellationRequested ?? false)
{
progressToken.Cancel();
return HttpStatusCode.RequestTimeout;
}
Log($"Retrieving chapter-info {chapter} {chapterParentManga}");
RequestResult requestResult = downloadClient.MakeRequest(chapter.url, RequestType.Default);
if (requestResult.htmlDocument is null)
{
progressToken?.Cancel();
return HttpStatusCode.RequestTimeout;
}
HtmlDocument? document = requestResult.htmlDocument;
HtmlNode[] imageNodes =
document.DocumentNode.SelectNodes($"//section[@hx-get='{chapter.url}/images']/img")?.ToArray() ?? [];
string[] urls = imageNodes.Select(imgNode => imgNode.GetAttributeValue("src", "")).ToArray();
return DownloadChapterImages(urls, chapter, RequestType.MangaImage, progressToken: progressToken, referrer: "https://weebcentral.com/");
}
}

View File

@ -11,13 +11,20 @@ public class Gotify : NotificationConnector
private readonly HttpClient _client = new();
[JsonConstructor]
public Gotify(string endpoint, string appToken, GlobalBase clone) : base(NotificationManagerType.Gotify, clone)
public Gotify(GlobalBase clone, string endpoint, string appToken) : base(clone, NotificationConnectorType.Gotify)
{
this.endpoint = endpoint;
if (!baseUrlRex.IsMatch(endpoint))
throw new ArgumentException("endpoint does not match pattern");
this.endpoint = baseUrlRex.Match(endpoint).Value;;
this.appToken = appToken;
}
public override void SendNotification(string title, string notificationText)
public override string ToString()
{
return $"Gotify {endpoint}";
}
protected override void SendNotificationInternal(string title, string notificationText)
{
Log($"Sending notification: {title} - {notificationText}");
MessageData message = new(title, notificationText);
@ -28,7 +35,7 @@ public class Gotify : NotificationConnector
if (!response.IsSuccessStatusCode)
{
StreamReader sr = new (response.Content.ReadAsStream());
logger?.WriteLine(this.GetType().ToString(), $"{response.StatusCode}: {sr.ReadToEnd()}");
Log($"{response.StatusCode}: {sr.ReadToEnd()}");
}
}

View File

@ -10,12 +10,17 @@ public class LunaSea : NotificationConnector
private readonly HttpClient _client = new();
[JsonConstructor]
public LunaSea(string id, GlobalBase clone) : base(NotificationManagerType.LunaSea, clone)
public LunaSea(GlobalBase clone, string id) : base(clone, NotificationConnectorType.LunaSea)
{
this.id = id;
}
public override void SendNotification(string title, string notificationText)
public override string ToString()
{
return $"LunaSea {id}";
}
protected override void SendNotificationInternal(string title, string notificationText)
{
Log($"Sending notification: {title} - {notificationText}");
MessageData message = new(title, notificationText);

View File

@ -2,14 +2,73 @@
public abstract class NotificationConnector : GlobalBase
{
public NotificationManagerType notificationManagerType;
public readonly NotificationConnectorType notificationConnectorType;
private DateTime? _notificationRequested = null;
private readonly Thread? _notificationBufferThread = null;
private const int NoChangeTimeout = 3, BiggestInterval = 30;
private List<KeyValuePair<string, string>> _notifications = new();
protected NotificationConnector(NotificationManagerType notificationManagerType, GlobalBase clone) : base(clone)
protected NotificationConnector(GlobalBase clone, NotificationConnectorType notificationConnectorType) : base(clone)
{
this.notificationManagerType = notificationManagerType;
Log($"Creating notificationConnector {Enum.GetName(notificationConnectorType)}");
this.notificationConnectorType = notificationConnectorType;
if (TrangaSettings.bufferLibraryUpdates)
{
_notificationBufferThread = new(CheckNotificationBuffer);
_notificationBufferThread.Start();
}
}
public enum NotificationManagerType : byte { Gotify = 0, LunaSea = 1 }
private void CheckNotificationBuffer()
{
while (true)
{
if (_notificationRequested is not null && DateTime.Now.Subtract((DateTime)_notificationRequested) > TimeSpan.FromMinutes(NoChangeTimeout)) //If no updates have been requested for NoChangeTimeout minutes, update library
{
string[] uniqueTitles = _notifications.DistinctBy(n => n.Key).Select(n => n.Key).ToArray();
Log($"Notification Buffer sending! Notifications: {string.Join(", ", uniqueTitles)}");
foreach (string ut in uniqueTitles)
{
string[] texts = _notifications.Where(n => n.Key == ut).Select(n => n.Value).ToArray();
SendNotificationInternal($"{ut} ({texts.Length})", string.Join('\n', texts));
}
_notificationRequested = null;
_notifications.Clear();
}
Thread.Sleep(100);
}
}
public abstract void SendNotification(string title, string notificationText);
public enum NotificationConnectorType : byte { Gotify = 0, LunaSea = 1, Ntfy = 2 }
public void SendNotification(string title, string notificationText, bool buffer = false)
{
_notificationRequested ??= DateTime.Now;
if (!TrangaSettings.bufferNotifications || !buffer)
{
SendNotificationInternal(title, notificationText);
return;
}
_notifications.Add(new(title, notificationText));
if (_notificationRequested is not null &&
DateTime.Now.Subtract((DateTime)_notificationRequested) > TimeSpan.FromMinutes(BiggestInterval)) //If the last update has been more than BiggestInterval minutes ago, update library
{
string[] uniqueTitles = _notifications.DistinctBy(n => n.Key).Select(n => n.Key).ToArray();
foreach (string ut in uniqueTitles)
{
string[] texts = _notifications.Where(n => n.Key == ut).Select(n => n.Value).ToArray();
SendNotificationInternal(ut, string.Join('\n', texts));
}
_notificationRequested = null;
_notifications.Clear();
}
else if(_notificationRequested is not null)
{
Log($"Buffering Notifications (Updates in latest {((DateTime)_notificationRequested).Add(TimeSpan.FromMinutes(BiggestInterval)).Subtract(DateTime.Now)} or {((DateTime)_notificationRequested).Add(TimeSpan.FromMinutes(NoChangeTimeout)).Subtract(DateTime.Now)})");
}
}
protected abstract void SendNotificationInternal(string title, string notificationText);
}

View File

@ -5,6 +5,13 @@ namespace Tranga.NotificationConnectors;
public class NotificationManagerJsonConverter : JsonConverter
{
private GlobalBase _clone;
public NotificationManagerJsonConverter(GlobalBase clone)
{
this._clone = clone;
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(NotificationConnector));
@ -14,10 +21,15 @@ public class NotificationManagerJsonConverter : JsonConverter
JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
if (jo["notificationManagerType"]!.Value<byte>() == (byte)NotificationConnector.NotificationManagerType.Gotify)
return jo.ToObject<Gotify>(serializer)!;
else if (jo["notificationManagerType"]!.Value<byte>() == (byte)NotificationConnector.NotificationManagerType.LunaSea)
return jo.ToObject<LunaSea>(serializer)!;
switch (jo["notificationConnectorType"]!.Value<byte>())
{
case (byte)NotificationConnector.NotificationConnectorType.Gotify:
return new Gotify(this._clone, jo.GetValue("endpoint")!.Value<string>()!, jo.GetValue("appToken")!.Value<string>()!);
case (byte)NotificationConnector.NotificationConnectorType.LunaSea:
return new LunaSea(this._clone, jo.GetValue("id")!.Value<string>()!);
case (byte)NotificationConnector.NotificationConnectorType.Ntfy:
return new Ntfy(this._clone, jo.GetValue("endpoint")!.Value<string>()!, jo.GetValue("topic")!.Value<string>()!, jo.GetValue("auth")!.Value<string>()!);
}
throw new Exception();
}

View File

@ -0,0 +1,87 @@
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
namespace Tranga.NotificationConnectors;
public class Ntfy : NotificationConnector
{
// ReSharper disable twice MemberCanBePrivate.Global
public string endpoint { get; init; }
public string auth { get; init; }
public string topic { get; init; }
private readonly HttpClient _client = new();
[JsonConstructor]
public Ntfy(GlobalBase clone, string endpoint, string topic, string auth) : base(clone, NotificationConnectorType.Ntfy)
{
this.endpoint = endpoint;
this.topic = topic;
this.auth = auth;
}
public Ntfy(GlobalBase clone, string endpoint, string username, string password, string? topic = null) :
this(clone, EndpointAndTopicFromUrl(endpoint)[0], topic??EndpointAndTopicFromUrl(endpoint)[1], AuthFromUsernamePassword(username, password))
{
}
private static string AuthFromUsernamePassword(string username, string password)
{
string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
string authParam = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=","");
return authParam;
}
private static string[] EndpointAndTopicFromUrl(string url)
{
string[] ret = new string[2];
if (!baseUrlRex.IsMatch(url))
throw new ArgumentException("url does not match pattern");
Regex rootUriRex = new(@"(https?:\/\/[a-zA-Z0-9-\.]+\.[a-zA-Z0-9]+)(?:\/([a-zA-Z0-9-\.]+))?.*");
Match match = rootUriRex.Match(url);
if(!match.Success)
throw new ArgumentException($"Error getting URI from provided endpoint-URI: {url}");
ret[0] = match.Groups[1].Value;
ret[1] = match.Groups[2].Success && match.Groups[2].Value.Length > 0 ? match.Groups[2].Value : "tranga";
return ret;
}
public override string ToString()
{
return $"Ntfy {endpoint} {topic}";
}
protected override void SendNotificationInternal(string title, string notificationText)
{
Log($"Sending notification: {title} - {notificationText}");
MessageData message = new(title, topic, notificationText);
HttpRequestMessage request = new(HttpMethod.Post, $"{this.endpoint}?auth={this.auth}");
request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json");
HttpResponseMessage response = _client.Send(request);
if (!response.IsSuccessStatusCode)
{
StreamReader sr = new (response.Content.ReadAsStream());
Log($"{response.StatusCode}: {sr.ReadToEnd()}");
}
}
private class MessageData
{
// ReSharper disable UnusedAutoPropertyAccessor.Local
public string topic { get; }
public string title { get; }
public string message { get; }
public int priority { get; }
public MessageData(string title, string topic, string message)
{
this.topic = topic;
this.title = title;
this.message = message;
this.priority = 3;
}
}
}

View File

@ -1,136 +0,0 @@
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using static System.IO.UnixFileMode;
namespace Tranga;
/// <summary>
/// Contains information on a Publication (Manga)
/// </summary>
public struct Publication
{
public string sortName { get; }
public List<string> authors { get; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public Dictionary<string,string> altTitles { get; }
// ReSharper disable once MemberCanBePrivate.Global
public string? description { get; }
public string[] tags { get; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public string? posterUrl { get; }
public string? coverFileNameInCache { get; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public Dictionary<string,string> links { get; }
// ReSharper disable once MemberCanBePrivate.Global
public int? year { get; }
public string? originalLanguage { get; }
// ReSharper disable once MemberCanBePrivate.Global
public string status { get; }
public string folderName { get; }
public string publicationId { get; }
public string internalId { get; }
public float ignoreChaptersBelow { get; set; }
private static readonly Regex LegalCharacters = new (@"[A-Z]*[a-z]*[0-9]* *\.*-*,*'*\'*\)*\(*~*!*");
[JsonConstructor]
public Publication(string sortName, List<string> authors, string? description, Dictionary<string,string> altTitles, string[] tags, string? posterUrl, string? coverFileNameInCache, Dictionary<string,string>? links, int? year, string? originalLanguage, string status, string publicationId, string? folderName = null, float? ignoreChaptersBelow = 0)
{
this.sortName = sortName;
this.authors = authors;
this.description = description;
this.altTitles = altTitles;
this.tags = tags;
this.coverFileNameInCache = coverFileNameInCache;
this.posterUrl = posterUrl;
this.links = links ?? new Dictionary<string, string>();
this.year = year;
this.originalLanguage = originalLanguage;
this.status = status;
this.publicationId = publicationId;
this.folderName = folderName ?? string.Concat(LegalCharacters.Matches(sortName));
while (this.folderName.EndsWith('.'))
this.folderName = this.folderName.Substring(0, this.folderName.Length - 1);
string onlyLowerLetters = string.Concat(this.sortName.ToLower().Where(Char.IsLetter));
this.internalId = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{onlyLowerLetters}{this.year}"));
this.ignoreChaptersBelow = ignoreChaptersBelow ?? 0f;
}
public string CreatePublicationFolder(string downloadDirectory)
{
string publicationFolder = Path.Join(downloadDirectory, this.folderName);
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;
}
public void SaveSeriesInfoJson(string downloadDirectory)
{
string publicationFolder = CreatePublicationFolder(downloadDirectory);
string seriesInfoPath = Path.Join(publicationFolder, "series.json");
if(!File.Exists(seriesInfoPath))
File.WriteAllText(seriesInfoPath,this.GetSeriesInfoJson());
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(seriesInfoPath, GroupRead | GroupWrite | OtherRead | OtherWrite | UserRead | UserWrite);
}
/// <returns>Serialized JSON String for series.json</returns>
private string GetSeriesInfoJson()
{
SeriesInfo si = new (new Metadata(this.sortName, this.year.ToString() ?? string.Empty, this.status, this.description ?? ""));
return System.Text.Json.JsonSerializer.Serialize(si);
}
//Only for series.json
private struct SeriesInfo
{
// ReSharper disable once UnusedAutoPropertyAccessor.Local we need it, trust
[JsonRequired]public Metadata metadata { get; }
public SeriesInfo(Metadata metadata) => this.metadata = metadata;
}
//Only for series.json what an abomination, why are all the fields not-null????
private struct Metadata
{
// ReSharper disable UnusedAutoPropertyAccessor.Local we need them all, trust me
[JsonRequired] public string type { get; }
[JsonRequired] public string publisher { get; }
// ReSharper disable twice IdentifierTypo
[JsonRequired] public int comicid { get; }
[JsonRequired] public string booktype { get; }
// ReSharper disable InconsistentNaming This one property is capitalized. Why?
[JsonRequired] public string ComicImage { get; }
[JsonRequired] public int total_issues { get; }
[JsonRequired] public string publication_run { get; }
[JsonRequired]public string name { get; }
[JsonRequired]public string year { get; }
[JsonRequired]public string status { get; }
[JsonRequired]public string description_text { get; }
public Metadata(string name, string year, string status, string description_text)
{
this.name = name;
this.year = year;
if(status.ToLower() == "ongoing" || status.ToLower() == "hiatus")
this.status = "Continuing";
else if (status.ToLower() == "completed" || status.ToLower() == "cancelled" || status.ToLower() == "discontinued")
this.status = "Ended";
else
this.status = status;
this.description_text = description_text;
//kill it with fire, but otherwise Komga will not parse
type = "Manga";
publisher = "";
comicid = 0;
booktype = "";
ComicImage = "";
total_issues = 0;
publication_run = "";
}
}
}

View File

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

View File

@ -7,43 +7,62 @@ namespace Tranga;
public partial class Tranga : GlobalBase
{
public bool keepRunning;
public JobBoss _jobBoss;
private Server server;
private HashSet<MangaConnector> connectors;
public JobBoss jobBoss;
private Server _server;
private HashSet<MangaConnector> _connectors;
public Tranga(Logger? logger, TrangaSettings settings) : base(logger, settings)
public Tranga(Logger? logger) : base(logger)
{
Log("\n\n _______ \n|_ _|.----..---.-..-----..-----..---.-.\n | | | _|| _ || || _ || _ |\n |___| |__| |___._||__|__||___ ||___._|\n |_____| \n\n");
keepRunning = true;
_jobBoss = new(this);
connectors = new HashSet<MangaConnector>()
_connectors = new HashSet<MangaConnector>()
{
new Manganato(this),
new Mangasee(this),
new MangaDex(this),
new MangaKatana(this)
new MangaKatana(this),
new Mangaworld(this),
new Bato(this),
new ManhuaPlus(this),
new MangaHere(this),
new AsuraToon(this),
new Weebcentral(this),
new Webtoons(this),
};
foreach(DirectoryInfo dir in new DirectoryInfo(Path.GetTempPath()).GetDirectories("trangatemp"))//Cleanup old temp folders
dir.Delete();
jobBoss = new(this, this._connectors);
StartJobBoss();
this.server = new Server(this);
this._server = new Server(this);
string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"};
SendNotifications("Tranga Started", emojis[Random.Shared.Next(0,emojis.Length-1)]);
Log(TrangaSettings.AsJObject().ToString());
}
public MangaConnector? GetConnector(string name)
{
foreach(MangaConnector mc in connectors)
foreach(MangaConnector mc in _connectors)
if (mc.name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
return mc;
return null;
}
public IEnumerable<MangaConnector> GetConnectors()
public bool TryGetConnector(string name, out MangaConnector? connector)
{
return connectors;
connector = GetConnector(name);
return connector is not null;
}
public Publication? GetPublicationById(string internalId)
public IEnumerable<MangaConnector> GetConnectors()
{
if (cachedPublications.Exists(publication => publication.internalId == internalId))
return cachedPublications.First(publication => publication.internalId == internalId);
return null;
return _connectors;
}
public Manga? GetPublicationById(string internalId) => GetCachedManga(internalId);
public bool TryGetPublicationById(string internalId, out Manga? manga)
{
manga = GetPublicationById(internalId);
return manga is not null;
}
private void StartJobBoss()
@ -52,10 +71,23 @@ public partial class Tranga : GlobalBase
{
while (keepRunning)
{
_jobBoss.CheckJobs();
Thread.Sleep(1000);
if(!TrangaSettings.aprilFoolsMode || !IsAprilFirst())
jobBoss.CheckJobs();
else
Log("April Fools Mode in Effect");
Thread.Sleep(100);
}
});
t.Start();
}
private bool IsAprilFirst()
{
//UTC 01 Apr +-12hrs
DateTime start = new DateTime(DateTime.Now.Year, 03, 31, 12, 0, 0, DateTimeKind.Utc);
DateTime end = new DateTime(DateTime.Now.Year, 04, 02, 12, 0, 0, DateTimeKind.Utc);
if (DateTime.UtcNow > start && DateTime.UtcNow < end)
return true;
return false;
}
}

View File

@ -1,16 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<LangVersion>12</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
<PackageReference Include="GlaxArguments" Version="1.1.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="PuppeteerSharp" Version="10.0.0" />
<PackageReference Include="PuppeteerSharp" Version="20.1.0" />
<PackageReference Include="Soenneker.Utils.String.NeedlemanWunsch" Version="2.1.301" />
</ItemGroup>
<ItemGroup>

View File

@ -1,4 +1,5 @@
using Logging;
using GlaxArguments;
namespace Tranga;
@ -7,130 +8,44 @@ public partial class Tranga : GlobalBase
public static void Main(string[] args)
{
string[]? help = GetArg(args, ArgEnum.Help);
if (help is not null)
{
PrintHelp();
return;
}
Argument downloadLocation = new (new[] { "-d", "--downloadLocation" }, 1, "Directory to which downloaded Manga are saved");
Argument workingDirectory = new (new[] { "-w", "--workingDirectory" }, 1, "Directory in which application-data is saved");
Argument consoleLogger = new (new []{"-c", "--consoleLogger"}, 0, "Enables the consoleLogger");
Argument fileLogger = new (new []{"-f", "--fileLogger"}, 0, "Enables the fileLogger");
Argument fPath = new (new []{"-l", "--fPath"}, 1, "Log Folder Path");
string[]? consoleLogger = GetArg(args, ArgEnum.ConsoleLogger);
string[]? fileLogger = GetArg(args, ArgEnum.FileLogger);
string? filePath = fileLogger?[0];//TODO validate path
Argument[] arguments = new[]
{
downloadLocation,
workingDirectory,
consoleLogger,
fileLogger,
fPath
};
ArgumentFetcher fetcher = new (arguments);
Dictionary<Argument, string[]> fetched = fetcher.Fetch(args);
string? directoryPath = fetched.TryGetValue(fPath, out string[]? path) ? path[0] : null;
if (directoryPath is not null && !Directory.Exists(directoryPath))
Directory.CreateDirectory(directoryPath);
List<Logger.LoggerType> enabledLoggers = new();
if(consoleLogger is not null)
if(fetched.ContainsKey(consoleLogger))
enabledLoggers.Add(Logger.LoggerType.ConsoleLogger);
if (fileLogger is not null)
if (fetched.ContainsKey(fileLogger))
enabledLoggers.Add(Logger.LoggerType.FileLogger);
Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, filePath);
Logger logger = new(enabledLoggers.ToArray(), Console.Out, Console.OutputEncoding, directoryPath);
TrangaSettings? settings = null;
string[]? downloadLocationPath = GetArg(args, ArgEnum.DownloadLocation);
string[]? workingDirectory = GetArg(args, ArgEnum.WorkingDirectory);
bool dlp = fetched.TryGetValue(downloadLocation, out string[]? downloadLocationPath);
bool wdp = fetched.TryGetValue(workingDirectory, out string[]? workingDirectoryPath);
if (downloadLocationPath is not null && workingDirectory is not null)
{
settings = new TrangaSettings(downloadLocationPath[0], workingDirectory[0]);
}else if (downloadLocationPath is not null)
{
if (settings is null)
settings = new TrangaSettings(downloadLocation: downloadLocationPath[0]);
else
settings = new TrangaSettings(downloadLocation: downloadLocationPath[0], settings.workingDirectory);
}else if (workingDirectory is not null)
{
if (settings is null)
settings = new TrangaSettings(downloadLocation: workingDirectory[0]);
else
settings = new TrangaSettings(settings.downloadLocation, workingDirectory[0]);
}
if (wdp)
TrangaSettings.LoadFromWorkingDirectory(workingDirectoryPath![0]);
else
{
settings = new TrangaSettings();
}
Directory.CreateDirectory(settings.downloadLocation);//TODO validate path
Directory.CreateDirectory(settings.workingDirectory);//TODO validate path
TrangaSettings.CreateOrUpdate();
if(dlp)
TrangaSettings.CreateOrUpdate(downloadDirectory: downloadLocationPath![0]);
Tranga _ = new (logger, settings);
}
private static void PrintHelp()
{
Console.WriteLine("Tranga-Help:");
foreach (Argument argument in arguments.Values)
{
foreach(string name in argument.names)
Console.Write("{0} ", name);
if(argument.parameterCount > 0)
Console.Write($"<{argument.parameterCount}>");
Console.Write("\r\n {0}\r\n", argument.helpText);
}
}
/// <summary>
/// Returns an array containing the parameters for the argument.
/// </summary>
/// <param name="args">List of argument-strings</param>
/// <param name="arg">Requested parameter</param>
/// <returns>
/// If there are no parameters for an argument, returns an empty array.
/// If the argument is not found returns null.
/// </returns>
private static string[]? GetArg(string[] args, ArgEnum arg)
{
List<string> argsList = args.ToList();
List<string> ret = new();
foreach (string name in arguments[arg].names)
{
int argIndex = argsList.IndexOf(name);
if (argIndex != -1)
{
if (arguments[arg].parameterCount == 0)
return ret.ToArray();
for (int parameterIndex = 1; parameterIndex <= arguments[arg].parameterCount; parameterIndex++)
{
if(argIndex + parameterIndex >= argsList.Count || args[argIndex + parameterIndex].Contains('-'))//End of arguments, or no parameter provided, when one is required
Console.WriteLine($"No parameter provided for argument {name}. -h for help.");
ret.Add(args[argIndex + parameterIndex]);
}
}
}
return ret.Any() ? ret.ToArray() : null;
}
private static Dictionary<ArgEnum, Argument> arguments = new()
{
{ ArgEnum.DownloadLocation, new(new []{"-d", "--downloadLocation"}, 1, "Directory to which downloaded Manga are saved") },
{ ArgEnum.WorkingDirectory, new(new []{"-w", "--workingDirectory"}, 1, "Directory in which application-data is saved") },
{ ArgEnum.ConsoleLogger, new(new []{"-c", "--consoleLogger"}, 0, "Enables the consoleLogger") },
{ ArgEnum.FileLogger, new(new []{"-f", "--fileLogger"}, 1, "Enables the fileLogger, Directory where logfiles are saved") },
{ ArgEnum.Help, new(new []{"-h", "--help"}, 0, "Print this") }
//{ ArgEnum., new(new []{""}, 1, "") }
};
internal enum ArgEnum
{
TrangaSettings,
DownloadLocation,
WorkingDirectory,
ConsoleLogger,
FileLogger,
Help
}
private struct Argument
{
public string[] names { get; }
public byte parameterCount { get; }
public string helpText { get; }
public Argument(string[] names, byte parameterCount, string helpText)
{
this.names = names;
this.parameterCount = parameterCount;
this.helpText = helpText;
}
Tranga _ = new (logger);
}
}

View File

@ -1,42 +1,72 @@
using Logging;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Tranga.LibraryConnectors;
using Tranga.MangaConnectors;
using Tranga.NotificationConnectors;
using static System.IO.UnixFileMode;
namespace Tranga;
public class TrangaSettings
public static class TrangaSettings
{
public string downloadLocation { get; private set; }
public string workingDirectory { get; init; }
public int apiPortNumber { get; init; }
[JsonIgnore] public string settingsFilePath => Path.Join(workingDirectory, "settings.json");
[JsonIgnore] public string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json");
[JsonIgnore] public string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json");
[JsonIgnore] public string tasksFilePath => Path.Join(workingDirectory, "tasks.json");
[JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache");
public ushort? version { get; set; }
public TrangaSettings(string? downloadLocation = null, string? workingDirectory = null, int apiPortNumber = 6531)
[JsonIgnore] internal static readonly string DefaultUserAgent = $"Tranga ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")}) / 1.0";
public static string downloadLocation { get; private set; } = (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Downloads"));
public static string workingDirectory { get; private set; } = Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
public static int apiPortNumber { get; private set; } = 6531;
public static string userAgent { get; private set; } = DefaultUserAgent;
public static bool bufferLibraryUpdates { get; private set; } = false;
public static bool bufferNotifications { get; private set; } = false;
[JsonIgnore] public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
[JsonIgnore] public static string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json");
[JsonIgnore] public static string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json");
[JsonIgnore] public static string jobsFolderPath => Path.Join(workingDirectory, "jobs");
[JsonIgnore] public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
public static ushort? version { get; } = 2;
public static bool aprilFoolsMode { get; private set; } = true;
[JsonIgnore]internal static readonly Dictionary<RequestType, int> DefaultRequestLimits = new ()
{
this.apiPortNumber = apiPortNumber;
downloadLocation ??= Path.Join(Directory.GetCurrentDirectory(), "Downloads");
workingDirectory ??= Directory.GetCurrentDirectory();
if (downloadLocation.Length < 1 || workingDirectory.Length < 1)
throw new ArgumentException("Download-location and working-directory paths can not be empty!");
this.workingDirectory = workingDirectory;
this.downloadLocation = downloadLocation;
if (File.Exists(settingsFilePath))
{
TrangaSettings settings = JsonConvert.DeserializeObject<TrangaSettings>(File.ReadAllText(settingsFilePath))!;
this.downloadLocation = settings.downloadLocation;
this.workingDirectory = settings.workingDirectory;
}
{RequestType.MangaInfo, 250},
{RequestType.MangaDexFeed, 250},
{RequestType.MangaDexImage, 40},
{RequestType.MangaImage, 60},
{RequestType.MangaCover, 250},
{RequestType.Default, 60}
};
public static Dictionary<RequestType, int> requestLimits { get; set; } = DefaultRequestLimits;
public static int ChromiumStartupTimeoutMs { get; set; } = 30000;
public static int ChromiumPageTimeoutMs { get; set; } = 30000;
public static void LoadFromWorkingDirectory(string directory)
{
TrangaSettings.workingDirectory = directory;
if(File.Exists(settingsFilePath))
Deserialize(File.ReadAllText(settingsFilePath));
else return;
Directory.CreateDirectory(downloadLocation);
Directory.CreateDirectory(workingDirectory);
ExportSettings();
}
public HashSet<LibraryConnector> LoadLibraryConnectors()
public static void CreateOrUpdate(string? downloadDirectory = null, string? pWorkingDirectory = null, int? pApiPortNumber = null, string? pUserAgent = null, bool? pAprilFoolsMode = null, bool? pBufferLibraryUpdates = null, bool? pBufferNotifications = null)
{
if(pWorkingDirectory is null && File.Exists(settingsFilePath))
LoadFromWorkingDirectory(workingDirectory);
downloadLocation = downloadDirectory ?? downloadLocation;
workingDirectory = pWorkingDirectory ?? workingDirectory;
apiPortNumber = pApiPortNumber ?? apiPortNumber;
userAgent = pUserAgent ?? userAgent;
aprilFoolsMode = pAprilFoolsMode ?? aprilFoolsMode;
bufferLibraryUpdates = pBufferLibraryUpdates ?? bufferLibraryUpdates;
bufferNotifications = pBufferNotifications ?? bufferNotifications;
Directory.CreateDirectory(downloadLocation);
Directory.CreateDirectory(workingDirectory);
ExportSettings();
}
public static HashSet<LibraryConnector> LoadLibraryConnectors(GlobalBase clone)
{
if (!File.Exists(libraryConnectorsFilePath))
return new HashSet<LibraryConnector>();
@ -45,44 +75,129 @@ public class TrangaSettings
{
Converters =
{
new LibraryManagerJsonConverter()
new LibraryManagerJsonConverter(clone)
}
})!;
}
public HashSet<NotificationConnector> LoadNotificationConnectors()
public static HashSet<NotificationConnector> LoadNotificationConnectors(GlobalBase clone)
{
if (!File.Exists(notificationConnectorsFilePath))
return new HashSet<NotificationConnector>();
return JsonConvert.DeserializeObject<HashSet<NotificationConnector>>(File.ReadAllText(libraryConnectorsFilePath),
return JsonConvert.DeserializeObject<HashSet<NotificationConnector>>(File.ReadAllText(notificationConnectorsFilePath),
new JsonSerializerSettings()
{
Converters =
{
new NotificationManagerJsonConverter()
new NotificationManagerJsonConverter(clone)
}
})!;
}
public void ExportSettings()
public static void UpdateAprilFoolsMode(bool enabled)
{
while (File.Exists(settingsFilePath))
aprilFoolsMode = enabled;
ExportSettings();
}
public static void UpdateDownloadLocation(string newPath, bool moveFiles = true)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
Directory.CreateDirectory(newPath,
GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite);
else
Directory.CreateDirectory(newPath);
if (moveFiles && Directory.Exists(downloadLocation))
Directory.Move(downloadLocation, newPath);
downloadLocation = newPath;
ExportSettings();
}
public static void UpdateWorkingDirectory(string newPath)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
Directory.CreateDirectory(newPath,
GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite);
else
Directory.CreateDirectory(newPath);
Directory.Move(workingDirectory, newPath);
workingDirectory = newPath;
ExportSettings();
}
public static void UpdateUserAgent(string? customUserAgent)
{
userAgent = customUserAgent ?? DefaultUserAgent;
ExportSettings();
}
public static void UpdateRateLimit(RequestType requestType, int newLimit)
{
requestLimits[requestType] = newLimit;
ExportSettings();
}
public static void ResetRateLimits()
{
requestLimits = DefaultRequestLimits;
ExportSettings();
}
public static void ExportSettings()
{
if (File.Exists(settingsFilePath))
{
bool inUse = true;
while (inUse)
{
try
{
using FileStream stream = new (settingsFilePath, FileMode.Open, FileAccess.Read, FileShare.None);
stream.Close();
inUse = false;
}
catch (IOException)
{
Thread.Sleep(100);
}
}
while(GlobalBase.IsFileInUse(settingsFilePath, null))
Thread.Sleep(100);
}
File.WriteAllText(settingsFilePath, JsonConvert.SerializeObject(this));
else
Directory.CreateDirectory(new FileInfo(settingsFilePath).DirectoryName!);
File.WriteAllText(settingsFilePath, Serialize());
}
public static JObject AsJObject()
{
JObject jobj = new JObject();
jobj.Add("downloadLocation", JToken.FromObject(downloadLocation));
jobj.Add("workingDirectory", JToken.FromObject(workingDirectory));
jobj.Add("apiPortNumber", JToken.FromObject(apiPortNumber));
jobj.Add("userAgent", JToken.FromObject(userAgent));
jobj.Add("aprilFoolsMode", JToken.FromObject(aprilFoolsMode));
jobj.Add("version", JToken.FromObject(version));
jobj.Add("requestLimits", JToken.FromObject(requestLimits));
jobj.Add("bufferLibraryUpdates", JToken.FromObject(bufferLibraryUpdates));
jobj.Add("bufferNotifications", JToken.FromObject(bufferNotifications));
jobj.Add("chromiumStartTimeout", JToken.FromObject(ChromiumStartupTimeoutMs));
jobj.Add("chromiumPageTimeout", JToken.FromObject(ChromiumPageTimeoutMs));
return jobj;
}
public static string Serialize() => AsJObject().ToString();
public static void Deserialize(string serialized)
{
JObject jobj = JObject.Parse(serialized);
if (jobj.TryGetValue("downloadLocation", out JToken? dl))
downloadLocation = dl.Value<string>()!;
if (jobj.TryGetValue("workingDirectory", out JToken? wd))
workingDirectory = wd.Value<string>()!;
if (jobj.TryGetValue("apiPortNumber", out JToken? apn))
apiPortNumber = apn.Value<int>();
if (jobj.TryGetValue("userAgent", out JToken? ua))
userAgent = ua.Value<string>()!;
if (jobj.TryGetValue("aprilFoolsMode", out JToken? afm))
aprilFoolsMode = afm.Value<bool>()!;
if (jobj.TryGetValue("requestLimits", out JToken? rl))
requestLimits = rl.ToObject<Dictionary<RequestType, int>>()!;
if (jobj.TryGetValue("bufferLibraryUpdates", out JToken? blu))
bufferLibraryUpdates = blu.Value<bool>()!;
if (jobj.TryGetValue("bufferNotifications", out JToken? bn))
bufferNotifications = bn.Value<bool>()!;
if (jobj.TryGetValue("chromiumStartTimeout", out JToken? cst))
ChromiumStartupTimeoutMs = cst.Value<int>();
if (jobj.TryGetValue("chromiumPageTimeout", out JToken? cpt))
ChromiumPageTimeoutMs = cpt.Value<int>();
}
}

21
docker-compose.local.yaml Normal file
View File

@ -0,0 +1,21 @@
version: '3'
services:
tranga-api:
build:
dockerfile: Dockerfile
context: .
container_name: tranga-api
volumes:
- ./Manga:/Manga
- ./settings:/usr/share/tranga-api
ports:
- "6531:6531"
restart: unless-stopped
tranga-website:
image: glax/tranga-website:latest
container_name: tranga-website
ports:
- "9555:80"
depends_on:
- tranga-api
restart: unless-stopped

View File

@ -4,16 +4,14 @@ services:
image: glax/tranga-api:latest
container_name: tranga-api
volumes:
- ./tranga:/usr/share/Tranga-API #1 when replacing ./tranga replace #2 with same value
- ./Manga:/Manga
- ./settings:/usr/share/tranga-api
ports:
- "6531:6531"
restart: unless-stopped
tranga-website:
image: glax/tranga-website:latest
container_name: tranga-website
volumes:
- ./tranga/imageCache:/usr/share/nginx/html/imageCache:ro #2 when replacing Point to same value as #1/imageCache
ports:
- "9555:80"
depends_on: