Compare commits

...

193 Commits

Author SHA1 Message Date
Chopper
c66abf25b9
SirenKomik: Fix loading pages (#8893)
All checks were successful
CI / Prepare job (push) Successful in 5s
CI / Build individual modules (push) Successful in 5m40s
CI / Publish repo (push) Successful in 47s
Fix loading pages
2025-05-20 21:28:58 +01:00
Chopper
87f31ed04b
KomikStation: Fix manga details (#8892)
Fix manga details
2025-05-20 21:28:58 +01:00
Chopper
ebaccb53c1
Kaguya(Yubikiri): Rebranding and fixes (#8891)
Rebranding
2025-05-20 21:28:58 +01:00
Chopper
39919d1e0a
ManhwaList: Update domain (#8889)
Update domain
2025-05-20 21:28:58 +01:00
Chopper
2a0022b3c7
GenzToons: Fix popularManga (#8888)
Fix popularManga
2025-05-20 21:28:58 +01:00
Chopper
b9282c11ec
Noromax: Update mangaUrlDirectory (#8887)
Update mangaUrlDirectory
2025-05-20 21:28:58 +01:00
Chopper
94ec34b7ab
IgnisComic: Force status code OK (#8886)
Force status code OK
2025-05-20 21:28:58 +01:00
Wyatt Ross
3297e41d08
ReadAllComics: re-add homepage support (#8883) 2025-05-20 21:28:58 +01:00
Chopper
488ec7a2cd
Remove dead sources (#8875)
* Remove Mangaku

* Remove MangaRuhu

* Remove PussyToons
2025-05-20 21:28:52 +01:00
Zero
36f9e925f2
updated birdtoon domain (#8862)
* Update BirdToon.kt

* Update build.gradle

* Update build.gradle
2025-05-20 21:27:51 +01:00
Chopper
86ea37c3a5
AnisaManga: Migrate source (#8857)
Migrate from Madara to Etoshore
2025-05-20 21:27:51 +01:00
Chopper
69d400b0ef
Update domains (#8856)
Update domain
2025-05-20 21:27:51 +01:00
Chopper
067d4a6bec
Add Inkapk (#8855) 2025-05-20 21:27:51 +01:00
Chopper
adb389334b
HuntersScans: Update domain (#8854)
Update domain
2025-05-20 21:27:51 +01:00
Creepler13
6a6ea56420
Change domain: Milasub (#8832)
* chagne domain

* add subdomain
2025-05-20 21:27:51 +01:00
Romain
16bda48c56
Add ComicTop (#8849)
* Add ComicTop
Closes #1264

* Wtff

* Watch out for a second train

* Shloud be good

* Add isNsfw
2025-05-20 21:27:51 +01:00
peakedshout
8f429616b4
Jinman Tiantang: Fix SChapter name error (#8841)
Fix: SChapter name error
2025-05-20 21:27:51 +01:00
happywillow0
4fbe372043
Myreadingmanga Fix Chapter Titles (#8838)
* Bump Extension Version Code

* Bump User Agent Version

* Remove Chapter Name Logic

Deprecated: Source no longer provides table of contents with chapter names.

* Update Chapter Name

Chapter list based on page list number and not true chapter number.
2025-05-20 21:27:51 +01:00
Dimitri Lerévérend
377e2124f1
Night SCANS: Remove duplicate page (#8836)
* Remove duplicate page

* Call parent parser instead
2025-05-20 21:27:51 +01:00
bapeey
8fb7d54fee
TempleScan(es): Add preference for domain change (#8828)
* update domain pref

* update domain
2025-05-20 21:27:51 +01:00
Chopper
a8e083aa40
GreenShit: Fix source data issue (#8827)
* Fix source data issue

* Rename method
2025-05-20 21:27:51 +01:00
marioplus
c44aba3f1a
feat(misskon): split chapters into individual pages (#8800)
* feat(misskon): split chapters into individual pages

- Migrate from single-chapter to per-page architecture
- Fix long loading time issues

* fix(misskon): Remove chapter_number configuration
- Drop deprecated chapter_number field setup
2025-05-20 21:27:51 +01:00
sovereign-beagle
314c5e0ed3
Update Multporn search query parameters (#8769)
* Update Multporn search query parameters
Closes #8072

* Update Multporn search query parameters
Closes #8072
2025-05-20 21:27:51 +01:00
Jake
61f37300ed
ReadComicOnline - Update Parsing to Use QuickJS and Make it Configurable (#8672)
* unscuff code, update regexes as configurable

* Update default values, improved pref

* Update Readcomiconline.kt

Add placeholders for future implementation

* Updated page list parsing to use quickjs

* add json config, remove decryption class

* review changes, updated default config path

* review changes, lint

* lint

* lint...
2025-05-20 21:27:51 +01:00
marioplus
d3fa36c82d
feat(buondua): split chapters into individual pages (#8803)
* feat(buondua): split chapters into individual pages

- Migrate from single-chapter to per-page architecture
- Fix long loading time issues

* fix(buondua): Remove chapter_number configuration

- Drop deprecated chapter_number field setup
2025-05-20 21:27:51 +01:00
lamaxama
d0357da16a
MangaBox: Fix no pages found error (#8797)
* MangaBox: Fix no pages found error

* bump

* bruh

* add fallback

* fix fallback

* fix fallback

* fix bump
2025-05-20 21:27:51 +01:00
Chopper
526b8ec979
Manhastro: Update domain and add custom settings (#8763)
* Update domain and add custom settings

* Ue network.clouflareClient
2025-05-20 21:27:51 +01:00
Chopper
062e3f84bd
Remangas: Fix timout (#8808)
Fix timout
2025-05-20 21:27:51 +01:00
Chopper
508f71b204
Remove LunarScan (#8809) 2025-05-20 21:27:51 +01:00
Chopper
b44f7c144d
EgoToons: Fix loading content (#8801)
Fix loading content
2025-05-20 21:27:51 +01:00
Chopper
913eddbbfc
SussyToons: Fix manga details (#8807)
Fix manga details
2025-05-20 21:27:51 +01:00
Chopper
2905e17a9a
SussyToons: Fix chapters and pages (#8804)
* Fix chapters and pages

* Bump version

* Fix chapter in webview
2025-05-20 21:27:51 +01:00
Chopper
db840dd353
Add GreenShit (#8745)
* Add GreenShit

* Replace icons

* Add icons

* Use latestUpdate as popularManga

* Remove dependencies unused

* Use lib-utils

* Change apiUrl visibilty
2025-05-20 21:27:51 +01:00
morallkat
3e1688e565
zh/BoyLove: Fix chapter list parsing and enable upload dates display in chapters (#8754)
* zh/BoyLove: Fix chapter list parsing and Enable upload dates

* import and use tryParse
2025-05-20 21:27:51 +01:00
Prem Kumar
0b3628f24e
Remove Reaper, Astra, Vortexfree (#8795)
* Delete reaperscans

* Delete astrascans

* Delete vortexscansfree
2025-05-20 21:27:51 +01:00
bapeey
3c91f2a834
EternalMangas: Fix browse (#8780)
* fix

* oops

* use destructor
2025-05-20 21:27:51 +01:00
zhongfly
d50732286a
dmzj: add apiv1 search (#8749)
* dmzj: add apiv1 search

* dmzj: remove old text search
2025-05-20 21:27:49 +01:00
Vetle Ledaal
adbfe86669
Manhwa18.cc: encode search, add headers, remove redundant override (#8767) 2025-05-20 21:27:49 +01:00
are-are-are
ea75b2c202
Update some domain (vi) (#8758)
* Vlogtruyen update domain

* LXManga update domain

* HentaiVN.plus update domain

* HentaiCB update domain

* TruyenVN update domain

* DocTruyen3Q update domain

* TopTruyen update domain

* CoManhua update domain
2025-05-20 21:27:49 +01:00
Chopper
0d16a6ca77
Add LittleTyrant (#8746) 2025-05-20 21:27:49 +01:00
bapeey
7d55595507
CatharsisWorld: Fix page decrypt (#8744)
* fix decrypt

* add open vars

* bruh
2025-05-20 21:27:49 +01:00
bapeey
d4e22509bc
TempleScan(es): Update domain (#8743)
update url
2025-05-20 21:27:49 +01:00
Chopper
c407c67d0f
YugenMangas: Update domain (#8742)
Update domain
2025-05-20 21:27:49 +01:00
Chopper
7503c2d20e
ApeComics, Atemporal, Crystal Comics: Migrate to Mangathemesia (#8741) 2025-05-20 21:27:49 +01:00
Chopper
8955f2b7e4
Bakai: Fix popular selector (#8740)
Fix popular selector
2025-05-20 21:27:49 +01:00
minhngoc25a
2b26d7b3a6
NettruyenCO (unoriginal): update domain and use ajax to fetch chapters (#8709)
* Changes domain of NettruyenCO and update chapter parsing logic (using AJAX)

* GET with headers

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* * Used `kotlinx.serialization` instead of `org.json.JSONObject`
* Used HttpUrl.Builder to encode
* Used `keiyoushi.utils.tryParse`

* * Replaced jsonPrimitive with better logic
* Remove data keyword
* Passed chapter date into constructor

* Update NetTruyenCO.kt

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-05-20 21:27:48 +01:00
Vetle Ledaal
c06e206cda
AComics: replicate filters 1:1 from site (#8730) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
d3d573fe77
Kiutaku: encode search (#8729) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
9f6f2d8b12
Madokami: encode search (#8727) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
8e13bcd5b6
Erofus: encode search (#8726) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
e5f3f65c25
NineAnime: encode search (#8728) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
9d01606868
Add Dark Nebulus Manga (#8725)
* Add Dark Nebulus Manga

* Dark Nebulus: update icon. Fix cover, author, artist, description
2025-05-20 21:27:48 +01:00
Vetle Ledaal
f3e9430b50
Add Borat Scans (#8724) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
2466c0c159
Add Rüya Manga.net (#8723) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
9e3028508e
Add Miae Translations (#8722) 2025-05-20 21:27:48 +01:00
Smol Ame
698a02de9e
Reorganize Usage section in README (#8717) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
93348a57a3
Add Ghost Hentai (#8715) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
a79efc512c
Add Pluto Scans (#8712) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
aad47bab3c
Komga: encode search, add headers everywhere (#8702) 2025-05-20 21:27:48 +01:00
Creepler13
06c8247f12
change domain hiperdex (#8711)
change domain
2025-05-20 21:27:48 +01:00
Creepler13
ecb1dd6899
change domain Onma (#8710)
change domain
2025-05-20 21:27:48 +01:00
Creepler13
5d8df09535
change domain cat300 (#8707)
* change domain

* version
2025-05-20 21:27:48 +01:00
mohamedotaku
d4e960700e
Update url Manga pro Ar (#8700)
* Update url

* Update url MangaPro arabic
2025-05-20 21:27:48 +01:00
Smol Ame
fd023a8000
Remove Aisha (#8682) 2025-05-20 21:27:48 +01:00
bapeey
302ac4b0cb
EternalMangas: Fetch regex from external repo (#8674)
* get regex from repo

* bump
2025-05-20 21:27:48 +01:00
Wyatt Ross
06aaa51240
re-add ReadAllComics (#8656)
* restored ReadAllComics

* removed logic to do with the popular titles page

* feedback

* deleted unecessary thing

* feedback
2025-05-20 21:27:48 +01:00
Vetle Ledaal
c4ff02c6c4
Oh Joy Sex Toy: encode search (#8662) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
956b7f05e0
Manta Comics: encode search, mark as NSFW (#8661) 2025-05-20 21:27:48 +01:00
Vetle Ledaal
07509542fd
Doujins: add headers, encode search (#8660) 2025-05-20 21:27:48 +01:00
AwkwardPeak7
e3cbc49e38
MangaHub: try to refresh api key for all api requests (#8659)
* MangaHub: try to refresh api key for all api requests

* update in interceptor

* remove logs
2025-05-20 21:27:48 +01:00
AwkwardPeak7
a56eb29dec
RizzComic: handle edge case for slugs (#8658)
* RizzComic: handle edge case for slugs

* use trim
2025-05-20 21:27:48 +01:00
Vetle Ledaal
fe5b10916f
MyHentaiGallery: minor cleanup, fix URLs (#8652)
* MyHentaiGallery: minor cleanup, fix URLs

* use markdown syntax

* update categories
2025-05-20 21:27:48 +01:00
Vetle Ledaal
30e8681278
Mangatown: escape search, add headers (#8650) 2025-05-20 21:27:45 +01:00
Vetle Ledaal
19543c9bba
XOXO Comics: escape search, add headers (#8651) 2025-05-20 21:27:45 +01:00
Vetle Ledaal
0bf5c08f50
Lunar Scans: remove redundant overrides (#8649)
Removes `ts_reader.run(...)` logic, might fit better in parent class.
Search now allows special characters otherwise not encoded.
2025-05-20 21:27:45 +01:00
xMohnad
c9d562c63a
Fix/revert to img (#8648)
* TeamX: revert page image extraction to use <img> instead of <canvas>

* TeamX: support both <img> and <canvas> for page extraction
2025-05-20 21:27:45 +01:00
bapeey
312747fb1a
EternalMangas: Fix no results and NPE (#8647)
fix
2025-05-20 21:27:45 +01:00
Tim Schneeberger
0da807ff70
feat: add GlobalComix (#8637)
* feat: add GlobalComix

Closes #3726

* fix: parse comic URLs correctly

* style: cleanup

* refactor: rename PageListDataDto to PageDataDto

* fix: sort search results by relevancy

* fix: improve premium chapter detection

* refactor: add chapter number to SChapter

* refactor: remove unused fields and allow some fields to be nullable

* refactor: minor cleanup

* Update src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComix.kt

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Update src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComix.kt

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Update src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComix.kt

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Update src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/ChapterDto.kt

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* refactor: remove CacheControl

* refactor: move constants of out object

* refactor: add new imports & remove 204 check

* refactor: remove chapter list 204 check

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-05-20 21:27:45 +01:00
Jake
1a6774af59
MangaHub (multisrc) - Fixes and Improvements (#8586)
* remove rate limit

* Fixes and improvements

* Version bump

* Review changes, more improvements
2025-05-20 21:27:45 +01:00
Creepler13
5818f1dc64
Fix Vortex Scans (#8645) 2025-05-20 21:27:45 +01:00
KirinRaikage
590f013578
Add Royal Manga (#8643)
* Add Royal Manga

* Update src/fr/royalmanga/build.gradle
2025-05-20 21:27:45 +01:00
Vetle Ledaal
38f511b815
RCO: decrypt page links in extension (#8634)
* Reimplemented pageListParse to decrypt the page in-app

* remove unused assets

---------

Co-authored-by: JakeeLuwi <jeloubirad@gmail.com>
2025-05-20 21:27:45 +01:00
AwkwardPeak7
f8a44eb538
Keyoapp: better selector for paid chapters (#8627) 2025-05-20 21:26:58 +01:00
AwkwardPeak7
ee29f284f5
batcave: remove cross origin referer (#8625) 2025-05-20 21:26:58 +01:00
xMohnad
83631d067e
TeamX: extract images from canvas instead of img (#8624)
* feat(ar/teamx): Update image parsing logic to use data-src attribute

* teamx: Update Team X app version code
2025-05-20 21:26:58 +01:00
Creepler13
54d9318a35
add PizzariaScan (#8622) 2025-05-20 21:26:58 +01:00
Smol Ame
67a9995b03
West Manga: Update domain URL (#8619)
* Westmanga: Bump overrideVersionCode

* Westmanga: Update baseURL

* Westmanga: Update baseURL
2025-05-20 21:26:58 +01:00
Smol Ame
5e02defd77
VyvyManga: Fix thumbnails (#8618)
* VyvyManga: Bump extVersionCode

* VyvyManga: Update search thumbnail selector

* VyvyManga: Update entry thumbnail selector
2025-05-20 21:26:58 +01:00
mahranaka
548d76cfc6
Update 1stkissmanga url (#8573)
* Update FirstKissMangaNet.kt

Updated the url of the extension.

* Update build.gradle

* updating of build.gradle

removed trailing slash

* Updating of FirstKissMangaNet.kt

removed trailing slash

* Update src/en/firstkissmanganet/build.gradle

Bumping version code as suggested

Co-authored-by: Prem Kumar <60751338+prem-k-r@users.noreply.github.com>

---------

Co-authored-by: Prem Kumar <60751338+prem-k-r@users.noreply.github.com>
2025-05-20 21:26:58 +01:00
Wyatt Ross
9dfa9a89a0
Add recursive parsing for chapters in ComicHubFree (#8605)
Fixed pagination
2025-05-20 21:26:58 +01:00
Prem Kumar
7c7ce285cd
Rizz Fables: Update status mapping (#8571)
* MangaThemesia: More status mapping

* Include more Status mapping

* fix error

* Typo

* consistent
2025-05-20 21:26:58 +01:00
Prem Kumar
b1ef508489
Add Madara Scans (#8559)
* Added MadaraScans

* Update MadaraScans.kt

* Update MadaraScans.kt

lint

* revert

* Update MadaraScans.kt

simply hide paid chapters by default

* lint
2025-05-20 21:26:58 +01:00
peakedshout
e853527587
Picacomic: Fix Channel logic error (#8555)
* Adding a Referer value to the request header increases the possibility of circumventing Cloudflare.

* Adding a Referer value to the request header increases the possibility of circumventing Cloudflare.

* Adding a Referer value to the request header increases the possibility of circumventing Cloudflare.

* Update src/zh/jinmantiantang/src/eu/kanade/tachiyomi/extension/zh/jinmantiantang/Jinmantiantang.kt

* Fix: Channel logic error

* Fix: Channel logic error

* Fix: Channel logic error

* Fix: Channel logic error

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-05-20 21:26:58 +01:00
Senxian Z
2edb3b6164
Update 6Manhua parser (#8539)
* Update 6Manhua parser

* Code review changes

* Code review changes

* Code review changes
2025-05-20 21:26:58 +01:00
A2va
6433c41cb7
AnimeSama: fix chapter list parsing (#8608)
Closes #5075
2025-05-20 21:26:58 +01:00
mahranaka
9fc03a5357
Update TCBScans url (#8562)
* Update TCBScans.kt

Updated the url of tcb scans to the newest valid one. I hope this is the correct place to fix it so the extension works again?

* Update build.gradle
2025-05-20 21:26:58 +01:00
Stratzcha
bea0c8897d
Webtoons: Add max quality pref (#8532) 2025-05-20 21:26:58 +01:00
Creepler13
cba5267312
Fix MangaDistrict (#8556)
change mangaSubString
2025-05-20 21:26:58 +01:00
AwkwardPeak7
9ad099295c
Toonily: various fixes (#8544)
* Toonily: various fixes

- change `mangaSubString` -> serie
  - prevent 302 redirect
- disable count views
  - 400 bad request
- use LoadMoreRequest
- fetch hd covers if possible

* query cleanup

* review changes
2025-05-20 21:26:58 +01:00
AwkwardPeak7
1c2876f7a5
Kdt Scans: update url (#8543)
update url
2025-05-20 21:26:58 +01:00
kerimmkirac
f3cf3488e8
Updating url of Turkish Extensions (#8542)
* Update build.gradle

* Update MangaTRNet.kt

* Update MangaGezgini.kt

* Update build.gradle

* Update build.gradle

* Update build.gradle

* Update Siyahmelek.kt

* Update build.gradle

* Update TimeNaight.kt
2025-05-20 21:26:58 +01:00
are-are-are
2dcd6d13a0
Update domain TruyenGG & TruyenQQ (#8538)
* TruyenGG update domain

* TruyenQQ update domain & add option domain switch

* fix buid
2025-05-20 21:26:58 +01:00
Dr1ks
b59238f938
Mangabuff: fix (#8537) 2025-05-20 21:26:58 +01:00
peakedshout
37e0aaeb75
Jinman Tiantang: add Referer header (#8527)
* Adding a Referer value to the request header increases the possibility of circumventing Cloudflare.

* Adding a Referer value to the request header increases the possibility of circumventing Cloudflare.

* Adding a Referer value to the request header increases the possibility of circumventing Cloudflare.

* Update src/zh/jinmantiantang/src/eu/kanade/tachiyomi/extension/zh/jinmantiantang/Jinmantiantang.kt

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-05-20 21:26:58 +01:00
Vetle Ledaal
ad829436f1
RCO: expand page search (#8541)
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-05-20 21:26:58 +01:00
lamaxama
b6ab8fb843
RCO: increase timeout & Tachimanga workaround (#8536)
* RCO: Add workaround for Tachimanga.

* increase the timeout to 30 seconds
2025-05-20 21:26:57 +01:00
mr-brune
e6866d43c2
Fix mangaworld (#8501)
* dont intercept request coming from the cdn eg. skip images

* Update build.gradle

* fix comment

* remove comment

* idk

* revert version

* Update build.gradle.kts

* fix without cdn

* check content type
2025-05-20 21:26:57 +01:00
marioplus
f7e3684d45
feat(YellowNote): add source 小黄书 (#8485)
* feat(YellowNote): add source 小黄书

* fix(YellowNote): reverse chapter sorting order

* feat(YellowNote): improve domain configuration and URL handling

- Implement dual domain configuration (default + override)
  - Default settings refresh when code updates occur
  - Override settings maintain custom configurations
- Enhance URL processing using HttpUrl
  - Fix encoding issues in query URLs
  - Improve URL parsing reliability

* feat(YellowNote): adapter /amateurs.html
2025-05-20 21:26:57 +01:00
Secozzi
b709f76b96
Comick: add tag exclude to extension settings (#8504)
* feat(all/comick): Add tag exclude to extension settings

* dont add empty tags
2025-05-20 21:26:57 +01:00
Prem Kumar
c9fc08676f
Remove 3 dead sources (#8502)
Remove AsuraScansFree, Inmoral No FanSub, YuriNeko
2025-05-20 21:26:57 +01:00
Smol Ame
4524da7e08
Replace troubleshooting link in acknowledgements (#8496) 2025-05-20 21:26:57 +01:00
AwkwardPeak7
219ceaac1e
MangaPark: fixes & improvements (#8483)
* cover absolute url & uncensored cover for hentai

* utils

* nsfw pref and thumbnail baseurl

* lint

* try upload status when original status is unknown

* include extra info in description

* off by default

* bump

* clean title

* nullable

* status set using nullability

* review changes

* revert

* actually set to off
2025-05-20 21:26:57 +01:00
AwkwardPeak7
1393a25fbb
RCO: fix page list (#8510)
fix
2025-05-20 21:26:57 +01:00
AwkwardPeak7
22ba4be2b6
Update README.md 2025-05-20 21:26:57 +01:00
lamaxama
962a22aa34
RCO: Fail to get image links. (#8474)
* RCO: Fail to get image links.

* Update regex
2025-05-20 21:26:57 +01:00
AwkwardPeak7
877ebd33c7
fix build 2025-05-20 21:26:57 +01:00
marioplus
ae6d455bb8
fix(pornpics): properly configure language settings (#8466)
* fix(pornpics): properly configure language settings

- Implement language configuration via SourceFactory

* fix(pornpics): properly bind ID and language settings

- Associate language codes with correct source IDs
- Use 'all' ID for English (en) language

* fix: remove all lang
2025-05-20 21:26:57 +01:00
Dr1ks
c6e4780feb
Grouple: update filters (#8463)
* Readmanga: update filters list

* Mintmanga: update filters list

* Seimanga: update filters list

* Selfmanga: update filters list

* Usagi: update filters list

* Seimanga: update filters list 2

* Mintmanga: update filters list 2

* RuMix: update filters list

* AllHentai: update filters list

* Grouple: reduce code duplication

* Grouple: remove unused import

* Grouple: bump

* Grouple: lint
2025-05-20 21:26:57 +01:00
AwkwardPeak7
aa64e66055
Update README (#8462) 2025-05-20 21:26:57 +01:00
bapeey
4c89276d16
CatManhwas: Update domain (#8459)
update domain
2025-05-20 21:26:57 +01:00
bapeey
6c87c1634b
ManhwaWeb: Filter out invalid pages (#8457)
* filter

* simplify condition

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-05-20 21:26:57 +01:00
bapeey
2dee930bbf
OlympusScanlation: Use site bookmarks to update manga url (#8429)
* use bookmarks

* bump

* use slugmap

* change message

* fixes
2025-05-20 21:26:57 +01:00
Jake
7586d7ff61
MangaHub (multisrc) - Rewrite Extension to Use the API Instead of Scraping (#8392)
* updated chapter list parsing

* More robust changes

* Now uses HttpSource, updated logic to use API, and more

* Fixed bugs, review changes, search and filter implementation

* Address some PR comments

* Review changes, improved API refresh logic, added pref for chapter titles

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-05-20 21:26:57 +01:00
DokterKaj
400079e2ae
Flame Comics: Detect author as list + Display artists + Fix HTML tags in description (#8449)
* Detect author as list + Display artists + Fix HTML tags in description

* Correct camel case
2025-05-20 21:26:54 +01:00
Dr1ks
c6a92ce7c4
Grouple: Fix offset (#8444)
* Grouple: Fix

* Grouple: Fix search
2025-05-20 21:26:54 +01:00
spicemace
c8de3f1c9d
[Kemono] 'added' is now nullable field (#8406)
* [Kemono] 'added' is now nullable field

* Update build.gradle.kts

* Update KemonoDto.kt

* Update KemonoDto.kt

* Update KemonoDto.kt

* Update KemonoDto.kt

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-05-20 21:26:53 +01:00
bapeey
a607d7024d
LectorJpg: Change theme (#8441)
* change theme

* bump versionId
2025-05-20 21:26:53 +01:00
AwkwardPeak7
17d6151584
BatCave: add referer (#8440) 2025-05-20 21:26:53 +01:00
lamaxama
1d2fc4493c
RCO: Fix timeout getting image links error (#8433)
* RCO: Fix timeout getting image links error

* Update regex
2025-05-20 21:26:53 +01:00
are-are-are
54055cb43c
VlogTruyen: Update domain & fix pageListParse (#8432)
* VlogTruyen Update domain & fix pageListParse

* Revert

* Use selectFrist safe call
2025-05-20 21:26:53 +01:00
Wyatt Ross
3af754cb20
Add Comichubfree (#8423)
* Added base level functionality

* Added base level functionality

* Added icons

* removed redundant override

* feedback

* feedback, also fixed a bug with the selector to do with searches without results

* feedback

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-05-20 21:26:53 +01:00
bapeey
9b89cc0b99
utils: add toJsonString function (#8439)
add util
2025-05-20 21:26:53 +01:00
Fioren
0ffd571f18
Fix thumbnail blurred image (#8430)
* Fix thumbnail blurred image

Fix thumbnail blurred image in CBHentai

* fix

* Update

* imageFromElement
2025-05-20 21:26:53 +01:00
DarkOps3
b425b94051
komga: sort genres and tags in alphabetical order (#8427) 2025-05-20 21:26:53 +01:00
TheKingTermux
b9a05d4fcc
Doujindesu: Add more filter and fix description (#8405)
* [WIP] DoujinDesu Add Group and Series Filter

https://github.com/keiyoushi/extensions-source/issues/7816

* Remove Unused Feature

* Bump version

* Add Class

* Rephrase

* Progress today

* Fixing errors that were not fixed yesterday

* Separate Author Filter same as Group and Series

* Update again about the desc

* Uhh my left over

* Changes requested

Thanks to @AwkwardPeak7

* I left it

* Don't use Author, Group, and Series filter when query isn't blank
2025-05-20 21:26:53 +01:00
Yush0DAN
fceca33d30
Barmanga: Update domain (#8426)
Update domain

Co-authored-by: ChuyC95 <187808637+ChuyC95@users.noreply.github.com>
2025-05-20 21:26:53 +01:00
Fioren
fe7d5c5019
TopTruyen, DocTruyen3Q: option automatically change domain (#8417)
* add option automatic change domain

add option automatic change domain TopTruyen and DocTruyen3Q

* update
2025-05-20 21:26:53 +01:00
DokterKaj
eb9b9b9aee
DeviantArt: Order chapter list chronologically instead of by source (#8407)
Order chapter list chronologically instead of by source
2025-05-20 21:26:53 +01:00
AwkwardPeak7
ac48b271c1
hitomi: handle gifs (#8398) 2025-05-20 21:26:53 +01:00
Cuong-Tran
364c90339a
Truyenqq: fix download not able to complete (#8373)
* fix non-existed picture references to another site, preventing download completion

* better selection as suggested

* My mind was somewhere else
2025-05-20 21:26:53 +01:00
AwkwardPeak7
d0ea9fadc6
Rework the Dynasty Source (#8326)
* exact match

* single search

* single search fixed

* rm

* thumbnail fast

* details

* chapter details in browse

* chapter details in browse

* man

* chapters and page

* chapters and page

* cleanup

* small cleanup

* enforce type filter manually

* enforce type filter manually

* reversed list or not

* newlines in description adjustment

* pairing filter

* remove single fetching logic

not worth it

* add other dynasty factories for legacy compatibility

* cleanup

* fix status and header being null in some cases

* update covers

* status

* unused

* inline function

* selector, reorder etc

* lint

* no empty types

* author in chapter name

* add InputStream parseAs utils function

* Review Changes

* dont include all authors

prevent https://imgur.com/dJ9LI4z

* Revert "add InputStream parseAs utils function"

This reverts commit 1b6bdc45aa6cfcb1ee046924a8c1ba68ec35789a.

* revert

* use encodedPath for covers

* more constants

* update covers
2025-05-20 21:26:53 +01:00
are-are-are
8dcfac5ba8
Update some domain (#8380)
* TruyenVN update domain

* LXManga update domain

* TopTruyen update domain

* ManhuaRock update domain & add option domain switch
2025-05-20 21:26:53 +01:00
are-are-are
d8e635afac
Update HentaiCB (#8379) 2025-05-20 21:26:53 +01:00
Creepler13
14117d1c5f
Asurascans: Fix title selector (#8371)
* fix titel selector

* merged title selectors
2025-05-20 21:26:53 +01:00
marioplus
334dd69fab
Add Source PornPics (#8364)
* feat(pornpics): add source pornpics

* feat(pornpics): add i18n support

* fix(pornpics): Default category read error

* fix(pornpics): Content-Type is not set correctly

* fix(pornpics): properly handle searches

* fix(pornpics): properly handle searches

* fix(pornpics): correct next page detection logic.

Add +1 to requested image count per page,Compare actual received count with pageSize to determine next page.

* fix(pornpics): set base language to en

* fix(pornpics): safely handle gallery info parsing

* feat(pornpics): add filter

* chore(pornpics): remove unused dependency

* fix(pornpics): correct category urlPart values.

* refactor(pornpics): simplify category browsing logic

- Remove category unselected
- Treat all non-search requests as category browsing

* refactor(pornpics): make a singelton object and remove comanion object

* refactor(pornpics): put in class to reuse preference

* refactor(pornpics): optimize chapter loading with fetchChapterList

- Replace chapterFromElement with fetchChapterList
- Reduce unnecessary network requests

* fix(pornpics): correct CategoryType initialization

* refactor(pornpics): improve method naming

- Rename `addQueryPageParameter` to `addQueryParameterPage` for clarity

* refactor(pornpics): improve API readability with boolean parameters

- Change `buildMangasPageRequest(page: Int, period: Int)` to:
  `buildMangasPageRequest(page: Int, popular: Boolean)`
- Replace numeric period flag with semantic boolean
- Simplify request building logic

* refactor(pornpics): extract category search logic to dedicated method

- Extract `useSearch` as standalone method
- Add enhanced validation logic for category search

* refactor(pornpics): replace manual parsing with HttpUrl

- Replace custom URL parsing logic with HttpUrl utility

* fix(pornpics): remove invalid category options

* refactor(pornpics): improve JSON error handling

- Throw specific exception type when JSON parsing fails
2025-05-20 21:26:53 +01:00
lamaxama
ebd527364e
Jinmantiantang: fixed empty date string issue (#8376)
* Jinmantiantang: fixed empty date string issue

* Use tryParse
2025-05-20 21:26:53 +01:00
AwkwardPeak7
47d14b6f29
KdtScans: update url, filter LNs from search, fix date (#8370)
KdtScans: update url & filter LNs fom search
2025-05-20 21:26:53 +01:00
AwkwardPeak7
e91a361ad8
fix Madara deeplink entry not being marked as in library (#8369) 2025-05-20 21:26:53 +01:00
Jake
68c6a5a6af
Fix MangaHub (multisrc) Chapter List Selector (#8361)
Updated selector
2025-05-20 21:26:53 +01:00
Maxim Molochkov
3fb869e5e2
Return Henchan source (#8280) 2025-05-20 21:26:53 +01:00
Fioren
45581e3697
Remove ads HentaiVNPlus (#8354) 2025-05-20 21:26:53 +01:00
dngonz
85c4793096
Bato.to: Fix manga not recognized from different url entries (#8352)
fix url
2025-05-20 21:26:53 +01:00
dngonz
fcb1ff01ad
Manga livre: Fix domain (#8351)
fix domain
2025-05-20 21:26:53 +01:00
dngonz
f1fd001e3f
Hentaicb: Fix domain (#8350)
fix domain
2025-05-20 21:26:53 +01:00
dngonz
1e3a5d8906
Manga Tilkisi: Fix source and change domain (#8349)
fix source
2025-05-20 21:26:53 +01:00
Creepler13
7cdee6623f
Add Stormx (#8341) 2025-05-20 21:26:53 +01:00
KirinRaikage
1233a3199a
Shadowtrad: Remove extension (#8338) 2025-05-20 21:26:53 +01:00
bapeey
dd95e39ebc
MangaFire: Fix image coudn't be loaded (#8332)
add referer
2025-05-20 21:26:53 +01:00
bapeey
fd0fe685f5
TempleScan(es): Update theme (#8331)
update theme
2025-05-20 21:26:53 +01:00
zhongfly
303b789fff
zaimanhua: fix search and detail (#8323) 2025-05-20 21:26:53 +01:00
dragon-masterk
86369167da
LuaScans: Fix missing chapters due to Timezone issue (#8318)
* Updated Timezone for LuaScans to fix missing chapters

* bump on overrideVersionCode

* reverted versionId
2025-05-20 21:26:53 +01:00
dngonz
cfc8624c40
AnimeSama: Fix source (#8317)
fix source
2025-05-20 21:26:53 +01:00
dngonz
f210f27a7c
Mangakimi: Fix images (#8316)
fix descramble image
2025-05-20 21:26:52 +01:00
dngonz
9639b7c585
Jiangzaitoon : Fix url (#8308)
fix url
2025-05-20 21:26:52 +01:00
Creepler13
7dcde729a6
Add Mangastep (#8307) 2025-05-20 21:26:52 +01:00
Chopper
383d9c5535
MangaHosted: Update domain and api url (#8303)
Update api url
2025-05-20 21:26:52 +01:00
marioplus
fad76bc4b2
feat(BoaBua): add source BaoBua (#8253)
* feat(buondua): add source BaoBua

Refs: #1104

* fix(buondua/search): resolve pagination param forwarding
chore(buondua/search): clean up URL format and flag

* Fix:
  - Add missing pagination parameter propagation
* Maintenance:
  - Remove redundant trailing "/" in pagination URLs
  - Set `supportsLatest` to false (default behavior)

Closes: #1104

* chore(buondua): remove redundant trim() calls

Closes: #1104

* refactor(BaoBua): standardize chapter names and URL handling

- Replace date-based chapter names with static "Gallery" value
- Remove baseUrl from category URLs (construct dynamically when used)

Closes: #1104

* chore(BaoBua): remove unused

Closes: #1104

* chore(BaoBua): revert settings.gradle.kts

Closes: #1104

* chore(BaoBua): remove unused import

Closes: #1104

* chore(BaoBua): remove needless blank line

Closes: #1104

* fix(BaoBua): add unselected Category

Closes: #1104

* refactor(BaoBua): optimize manga details parsing

- Set update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
- Remove unused randomua dependency

Closes: #1104
2025-05-20 21:26:37 +01:00
marioplus
40a9d2ec6a
feat(misskon): add source misskon. (#8225)
* feat(misskon): add source misskon.

refs: #1855

* feat(misskon): Improve network handling and code structure

• Network enhancements:
  - Add headers parameter for `GET` methods
  - Implement standardized query with `HttpUrl`
  - Replace `attr()` with `asbUrl()` for safe path composition

• Structural improvements:
  - Move date regex to companion object
  - Extract SimpleDateFormat as global constant
  - Refactor Filter logic into standalone class

• Robustness upgrades:
  - Remove unsafe non-null assertion(!!) on date_upload
  - Adopt SManga.create().apply  { } chaining pattern

refs: #1855

* feat(misskon): improve code structure and null safety

- Replace `attr()` with `absUrl()` for more reliable URL extraction
- Remove non-null assertion (`!!`) from `thumbnail_url` as it's not critical
- Fix usage of `select()` and `selectFirst()` to properly handle nullable cases

refs: #1855

* chore(MissKon): remove needless blank line

- Fix lint violation (no-consecutive-blank-lines)

Closes: #1855

* refactor(MissKon): optimize URL handling and reuse utils

* Remove baseUrl from category URLs (build dynamically when used)
* Reuse existing utils for:
  - Date parsing
  - Filter queries
* Standardize pagination URL construction

Closes: #1855

* fix(MissKon): Correct URL template, set default gallery name, and unify selectors

Closes: #1855

* refactor(MissKon): fix URL construction and client configuration

- Fix string interpolation in search URL ("$it.url"  → "${it.url}")
- Remove MOBILE user agent restriction
- Remove unused dependency
- Add ONLY_FETCH_ONCE update strategy
- Clean up selector syntax
2025-05-20 21:26:37 +01:00
Chopper
6341d5ff73
Update domains (#8297)
Update domain
2025-05-20 21:26:37 +01:00
Vetle Ledaal
485c7f078e
Yaoi Flix: update domain (#8291)
* Revert "Remove broken extensions/sites (#8167)"

Partial.
This reverts commit 5e1a1cd5ef34852fe524287a81d9769d34661713.

* Yaoi Flix: update domain
2025-05-20 21:26:37 +01:00
Vetle Ledaal
153934bed7
Revert removal of some MadTheme sites (#8290)
* Revert "MadTheme: Fix MangaForest, remove redirect domains (#14661)"

This reverts commit 42d9c0b1184ea23679bb7471696c3d7b1bebd36e.

* Switch to new multisrc structure
2025-05-20 21:26:37 +01:00
Creepler13
b7e40990d8
Add Alucard Scans (#8289)
* Add Alucard Scans

* Update src/tr/alucardscans/src/eu/kanade/tachiyomi/extension/tr/alucardscans/Alucardscans.kt

Ah True, it still worked so i didnt see it

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-05-20 21:26:37 +01:00
bapeey
10cea1e57d
ManhwaWeb: Filter out unreleased chapters (#8282)
filter out not released chapters
2025-05-20 21:26:37 +01:00
Creepler13
7457106c23
Add APComics (#8279)
add APComics
2025-05-20 21:26:37 +01:00
lamaxama
752920a0d4
Add migration reminders for Manganato, Mangakakalot and Mangabat. (#8278) 2025-05-20 21:26:37 +01:00
Creepler13
c124f76d64
Add Manhuarm (#8277)
add Manhuarm
2025-05-20 21:26:37 +01:00
Smol Ame
34bd753d27
Remove AQUA Scans (ManhwaWorld) (#8276) 2025-05-20 21:26:37 +01:00
Chopper
d0d9558eb2
GekkouScans: Migrate theme (#8275)
Migrate theme
2025-05-20 21:26:37 +01:00
Chopper
7fcbbec4c7
ReadMangas: Add compatibility with id and slug (#8274)
Add compatibility with id and slug
2025-05-20 21:26:37 +01:00
Shikonin
cea3c8a60b
DMZJ: Update API (#8264)
Update API
2025-05-20 21:26:37 +01:00
Fioren
26eb2757b4
Fix images not load DocTruyen3Q (#8272)
* Fix images not load DocTruyen3Q

Fix images not load, update domain DocTruyen3Q

* update
2025-05-20 21:26:37 +01:00
Chopper
e64df9ebc4
TeamLanhLung: Merge A3Manga with TeamLanhLung and fix search manga (#8271)
* Merge A3Manga and TeamLanhLung and fix search manga

* Use parseAs from utils
2025-05-20 21:26:37 +01:00
Prem Kumar
e5a63cc2e6
Arven Scans: Update domain (#8267)
update domain
2025-05-20 21:26:37 +01:00
Vetle Ledaal
98bd2586fb
Koharu: do not use network on main thread (#8255) 2025-05-20 21:26:37 +01:00
Vetle Ledaal
49c03653f8
Fix CI, Looper.getMainLooper() (#8257) 2025-05-20 21:26:37 +01:00
marioplus
2723d5d0ca
fix(buondua): enhance Cloudflare challenge bypass (#8249)
* fix(buondua): enhance Cloudflare challenge bypass

- Add rate limiting (max 10 requests/second)
- Implement random User-Agent rotation
- Inject Referer header
- Version bump 2 → 3

refs: #8079

* chore(buondua): replace interceptor with headersBuilder for request headers

- Replace the interceptor with  headersBuilder()
- Move the SimpleDateFormat to the companion object or class variable

Refs: #8079
2025-05-20 21:26:37 +01:00
Romain
f4a08ea908
Add .git-blame-ignore-revs (#8232) 2025-05-20 21:26:37 +01:00
Romain
9efc599e9c
Migration of PhoenixScans (#8191)
* PhenixScans: Add support for new site

* Search, Filter, Genres

* Cleaning

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: Luqman <16263232+Riztard@users.noreply.github.com>
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>

* Status

* Variable formatting

* Move Filters to a separate file

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: Luqman <16263232+Riztard@users.noreply.github.com>
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2025-05-20 21:26:37 +01:00
Chopper
09d9b33080
SussyToons: Fix memory leak (#8240)
* Fix memory leak

* Remove variable
2025-05-20 21:26:37 +01:00
Chopper
fa09f8122d
ReadMangas: Fix loading content (#8239)
* Fix details, chapter and page

* Fix popular and latest

* Fix search

* Add change suggestion
2025-05-20 21:26:37 +01:00
mrtear
5de9ae2485
HiveScans: Update Domain (#8235)
Hive: Update Domain
2025-05-20 21:26:37 +01:00
Chopper
ae982b97ca
Keyoapp: Fix duplicate entries (#8226)
* Fix duplicate entries

* Remove delimiter

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>

---------

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
2025-05-20 21:26:37 +01:00
mrtear
8ee6d0184e
Arvenscans & Aeinscans: Update URL & Fix Details (#8216)
* Arven: Domain Update

* Aein: Fix null tag & details
2025-05-20 21:26:37 +01:00
702 changed files with 25919 additions and 5204 deletions

2
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,2 @@
# Force \n line ending, trailing newline
b72776e1f528c56ae6a9ef3a6f8d72533b00d39f

View File

@ -99,7 +99,7 @@ body:
required: true
- label: I have updated all installed extensions.
required: true
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
- label: I have tried the [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
required: true

View File

@ -1,14 +1,18 @@
# Keiyoushi Extensions
### Please give the repo a :star:
| Build | Support Server |
| Build | Need Help? |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [![CI](https://github.com/keiyoushi/extensions-source/actions/workflows/build_push.yml/badge.svg)](https://github.com/keiyoushi/extensions-source/actions/workflows/build_push.yml) | [![Discord](https://img.shields.io/discord/1193460528052453448.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/3FbCpdKbdY) |
# Usage
## Usage
**If you are new to repository/extensions, please read the [Keiyoushi Getting Started guide](https://keiyoushi.github.io/docs/guides/getting-started#adding-the-extension-repo) first.**
[Getting started](https://keiyoushi.github.io/docs/guides/getting-started#adding-the-extension-repo)
* You can add our repo by visiting the [Keiyoushi Website](https://keiyoushi.github.io/add-repo)
* Otherwise, copy & paste the following URL: https://raw.githubusercontent.com/keiyoushi/extensions/repo/index.min.json
# Requests
## Requests
To request a new source or bug fix, [create an issue](https://github.com/keiyoushi/extensions-source/issues/new/choose).
@ -19,7 +23,7 @@ difficult to maintain.
If you would like to see a request fulfilled and have the necessary skills to do so, consider contributing!
Issues are up-for-grabs for any developer if there is no assigned user already.
# Contributing
## Contributing
Contributions are welcome!

View File

@ -1,247 +0,0 @@
package eu.kanade.tachiyomi.multisrc.a3manga
import android.util.Base64
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
open class A3Manga(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : ParsedHttpSource() {
override val supportsLatest: Boolean = false
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/page/$page/", headers)
override fun popularMangaSelector() = ".comic-list .comic-item"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.select(".comic-title-link a").attr("href"))
title = element.select(".comic-title").text().trim()
thumbnail_url = element.select(".img-thumbnail").attr("abs:src")
}
override fun popularMangaNextPageSelector() = "li.next:not(.disabled)"
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PREFIX_ID_SEARCH) -> {
val id = query.removePrefix(PREFIX_ID_SEARCH).trim()
fetchMangaDetails(
SManga.create().apply {
url = "/truyen-tranh/$id/"
},
)
.map {
it.url = "/truyen-tranh/$id/"
MangasPage(listOf(it), false)
}
}
else -> super.fetchSearchManga(page, query, filters)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
POST(
"$baseUrl/wp-admin/admin-ajax.php",
headers,
FormBody.Builder()
.add("action", "searchtax")
.add("keyword", query)
.build(),
)
override fun searchMangaSelector(): String = throw UnsupportedOperationException()
override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<SearchResponseDto>()
if (!dto.success) {
return MangasPage(emptyList(), false)
}
val manga = dto.data
.filter { it.cstatus != "Nhóm dịch" }
.map {
SManga.create().apply {
setUrlWithoutDomain(it.link)
title = it.title
thumbnail_url = it.img
}
}
return MangasPage(manga, false)
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.select(".info-title").text()
author = document.select(".comic-info strong:contains(Tác giả) + span").text().trim()
description = document.select(".intro-container .text-justify").text().substringBefore("— Xem Thêm —")
genre = document.select(".comic-info .tags a").joinToString { tag ->
tag.text().split(' ').joinToString(separator = " ") { word ->
word.replaceFirstChar { it.titlecase() }
}
}
thumbnail_url = document.select(".img-thumbnail").attr("abs:src")
val statusString = document.select(".comic-info strong:contains(Tình trạng) + span").text()
status = when (statusString) {
"Đang tiến hành" -> SManga.ONGOING
"Trọn bộ " -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
override fun chapterListSelector(): String = ".chapter-table table tbody tr"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
name = element.select("a .hidden-sm").text()
date_upload = runCatching {
dateFormat.parse(element.select("td").last()!!.text())?.time
}.getOrNull() ?: 0
}
protected fun decodeImgList(document: Document): String {
val htmlContentScript = document.selectFirst("script:containsData(htmlContent)")?.html()
?.substringAfter("var htmlContent=\"")
?.substringBefore("\";")
?.replace("\\\"", "\"")
?.replace("\\\\", "\\")
?.replace("\\/", "/")
?: throw Exception("Couldn't find script with image data.")
val htmlContent = json.decodeFromString<CipherDto>(htmlContentScript)
val ciphertext = Base64.decode(htmlContent.ciphertext, Base64.DEFAULT)
val iv = htmlContent.iv.decodeHex()
val salt = htmlContent.salt.decodeHex()
val passwordScript = document.selectFirst("script:containsData(chapterHTML)")?.html()
?: throw Exception("Couldn't find password to decrypt image data.")
val passphrase = passwordScript.substringAfter("var chapterHTML=CryptoJSAesDecrypt('")
.substringBefore("',htmlContent")
.replace("'+'", "")
val keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM)
val spec = PBEKeySpec(passphrase.toCharArray(), salt, 999, 256)
val keyS = SecretKeySpec(keyFactory.generateSecret(spec).encoded, "AES")
val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(iv))
val imgListHtml = cipher.doFinal(ciphertext).toString(Charsets.UTF_8)
return imgListHtml
}
override fun pageListParse(document: Document): List<Page> {
val imgListHtml = decodeImgList(document)
return Jsoup.parseBodyFragment(imgListHtml).select("img").mapIndexed { idx, element ->
val encryptedUrl = element.attributes().find { it.key.startsWith("data") }?.value
val effectiveUrl = encryptedUrl?.decodeUrl() ?: element.attr("abs:src")
Page(idx, imageUrl = effectiveUrl)
}
}
private fun String.decodeUrl(): String? {
// We expect the URL to start with `https://`, where the last 3 characters are encoded.
// The length of the encoded character is not known, but it is the same across all.
// Essentially we are looking for the two encoded slashes, which tells us the length.
val patternIdx = patternsLengthCheck.indexOfFirst { pattern ->
val matchResult = pattern.find(this)
val g1 = matchResult?.groupValues?.get(1)
val g2 = matchResult?.groupValues?.get(2)
g1 == g2 && g1 != null
}
if (patternIdx == -1) {
return null
}
// With a known length we can predict all the encoded characters.
// This is a slightly more expensive pattern, hence the separation.
val matchResult = patternsSubstitution[patternIdx].find(this)
return matchResult?.destructured?.let { (colon, slash, period) ->
this
.replace(colon, ":")
.replace(slash, "/")
.replace(period, ".")
}
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
// https://stackoverflow.com/a/66614516
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
companion object {
const val KEY_ALGORITHM = "PBKDF2WithHmacSHA512"
const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS7PADDING"
const val PREFIX_ID_SEARCH = "id:"
val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.US).apply {
timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh")
}
private val patternsLengthCheck: List<Regex> = (20 downTo 1).map { i ->
"""^https.{$i}(.{$i})(.{$i})""".toRegex()
}
private val patternsSubstitution: List<Regex> = (20 downTo 1).map { i ->
"""^https(.{$i})(.{$i}).*(.{$i})(?:webp|jpeg|tiff|.{3})$""".toRegex()
}
}
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 3
baseVersionCode = 2

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,322 @@
package eu.kanade.tachiyomi.multisrc.greenshit
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferences
import keiyoushi.utils.parseAs
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.io.IOException
abstract class GreenShit(
override val name: String,
val url: String,
override val lang: String,
val scanId: Long = 1,
) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
private val isCi = System.getenv("CI") == "true"
private val preferences: SharedPreferences = getPreferences()
protected var apiUrl: String
get() = preferences.getString(API_BASE_URL_PREF, defaultApiUrl)!!
private set(value) = preferences.edit().putString(API_BASE_URL_PREF, value).apply()
private var restoreDefaultEnable: Boolean
get() = preferences.getBoolean(DEFAULT_PREF, false)
set(value) = preferences.edit().putBoolean(DEFAULT_PREF, value).apply()
override val baseUrl: String get() = when {
isCi -> defaultBaseUrl
else -> preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
}
private val defaultBaseUrl: String = url
private val defaultApiUrl: String = "https://api.sussytoons.wtf"
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::imageLocation)
.build()
init {
if (restoreDefaultEnable) {
restoreDefaultEnable = false
preferences.edit().putString(DEFAULT_BASE_URL_PREF, null).apply()
preferences.edit().putString(API_DEFAULT_BASE_URL_PREF, null).apply()
}
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { domain ->
if (domain != defaultBaseUrl) {
preferences.edit()
.putString(BASE_URL_PREF, defaultBaseUrl)
.putString(DEFAULT_BASE_URL_PREF, defaultBaseUrl)
.apply()
}
}
preferences.getString(API_DEFAULT_BASE_URL_PREF, null).let { domain ->
if (domain != defaultApiUrl) {
preferences.edit()
.putString(API_BASE_URL_PREF, defaultApiUrl)
.putString(API_DEFAULT_BASE_URL_PREF, defaultApiUrl)
.apply()
}
}
}
override fun headersBuilder() = super.headersBuilder()
.set("scan-id", scanId.toString())
// ============================= Popular ==================================
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaParse(response: Response): MangasPage {
val json = response.parseScriptToJson().let(POPULAR_JSON_REGEX::find)
?.groups?.get(1)?.value
?: return MangasPage(emptyList(), false)
val mangas = json.parseAs<ResultDto<List<MangaDto>>>().toSMangaList()
return MangasPage(mangas, false)
}
// ============================= Latest ===================================
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/obras/novos-capitulos".toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.addQueryParameter("limite", "24")
.addQueryParameter("gen_id", "4")
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val dto = response.parseAs<ResultDto<List<MangaDto>>>()
val mangas = dto.toSMangaList()
return MangasPage(mangas, dto.hasNextPage())
}
// ============================= Search ===================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/obras".toHttpUrl().newBuilder()
.addQueryParameter("obr_nome", query)
.addQueryParameter("limite", "8")
.addQueryParameter("pagina", page.toString())
.addQueryParameter("todos_generos", "true")
.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<ResultDto<List<MangaDto>>>()
return MangasPage(dto.toSMangaList(), dto.hasNextPage())
}
// ============================= Details ==================================
override fun mangaDetailsParse(response: Response): SManga {
val json = response.parseScriptToJson().let(DETAILS_CHAPTER_REGEX::find)
?.groups?.get(0)?.value
?: throw IOException("Details do mangá não foi encontrado")
return json.parseAs<ResultDto<MangaDto>>().results.toSManga()
}
// ============================= Chapters =================================
override fun chapterListParse(response: Response): List<SChapter> {
val json = response.parseScriptToJson().let(DETAILS_CHAPTER_REGEX::find)
?.groups?.get(0)?.value
?: return emptyList()
return json.parseAs<ResultDto<WrapperChapterDto>>().toSChapterList()
}
// ============================= Pages ====================================
private val pageUrlSelector = "img.chakra-image"
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
pageListParse(document).takeIf(List<Page>::isNotEmpty)?.let { return it }
val dto = extractScriptData(document)
.let(::extractJsonContent)
.let(::parseJsonToChapterPageDto)
return dto.toPageList()
}
private fun pageListParse(document: Document): List<Page> {
return document.select(pageUrlSelector).mapIndexed { index, element ->
Page(index, document.location(), element.absUrl("src"))
}
}
private fun extractScriptData(document: Document): String {
return document.select("script").map(Element::data)
.firstOrNull(pageRegex::containsMatchIn)
?: throw Exception("Failed to load pages: Script data not found")
}
private fun extractJsonContent(scriptData: String): String {
return pageRegex.find(scriptData)
?.groups?.get(1)?.value
?.let { "\"$it\"".parseAs<String>() }
?: throw Exception("Failed to extract JSON from script")
}
private fun parseJsonToChapterPageDto(jsonContent: String): ResultDto<ChapterPageDto> {
return try {
jsonContent.parseAs<ResultDto<ChapterPageDto>>()
} catch (e: Exception) {
throw Exception("Failed to load pages: ${e.message}")
}
}
override fun imageUrlParse(response: Response): String = ""
override fun imageUrlRequest(page: Page): Request {
val imageHeaders = headers.newBuilder()
.add("Referer", "$baseUrl/")
.build()
return GET(page.url, imageHeaders)
}
// ============================= Interceptors =================================
private fun imageLocation(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (response.isSuccessful) {
return response
}
response.close()
val url = request.url.newBuilder()
.dropPathSegment(4)
.build()
val newRequest = request.newBuilder()
.url(url)
.build()
return chain.proceed(newRequest)
}
// ============================= Settings ====================================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val fields = listOf(
EditTextPreference(screen.context).apply {
key = BASE_URL_PREF
title = BASE_URL_PREF_TITLE
summary = URL_PREF_SUMMARY
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "URL padrão:\n$defaultBaseUrl"
setDefaultValue(defaultBaseUrl)
},
EditTextPreference(screen.context).apply {
key = API_BASE_URL_PREF
title = API_BASE_URL_PREF_TITLE
summary = buildString {
append("Se não souber como verificar a URL da API, ")
append("busque suporte no Discord do repositório de extensões.")
appendLine(URL_PREF_SUMMARY)
append("\n⚠ A fonte não oferece suporte para essa extensão.")
}
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "URL da API padrão:\n$defaultApiUrl"
setDefaultValue(defaultApiUrl)
},
SwitchPreferenceCompat(screen.context).apply {
key = DEFAULT_PREF
title = "Redefinir configurações"
summary = buildString {
append("Habilite para redefinir as configurações padrões no próximo reinicialização da aplicação.")
appendLine("Você pode limpar os dados da extensão em Configurações > Avançado:")
appendLine("\t - Limpar os cookies")
appendLine("\t - Limpar os dados da WebView")
appendLine("\t - Limpar o banco de dados (Procure a '$name' e remova os dados)")
}
setDefaultValue(false)
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_LONG).show()
true
}
},
)
fields.forEach(screen::addPreference)
}
// ============================= Utilities ====================================
private fun Response.parseScriptToJson(): String {
val document = asJsoup()
val script = document.select("script")
.map(Element::data)
.filter(String::isNotEmpty)
.joinToString("\n")
return QuickJs.create().use {
it.evaluate(
"""
globalThis.self = globalThis;
$script
self.__next_f.map(it => it[it.length - 1]).join('')
""".trimIndent(),
) as String
}
}
private fun HttpUrl.Builder.dropPathSegment(count: Int): HttpUrl.Builder {
repeat(count) {
removePathSegment(0)
}
return this
}
companion object {
const val CDN_URL = "https://cdn.sussytoons.site"
val pageRegex = """capituloInicial.{3}(.*?)(\}\]\})""".toRegex()
val POPULAR_JSON_REGEX = """(?:"dataTop":)(\{.+totalPaginas":\d+\})(?:.+"dataF)""".toRegex()
val DETAILS_CHAPTER_REGEX = """(\{\"resultado.+"\}{3})""".toRegex()
private const val URL_PREF_SUMMARY = "Para uso temporário, se a extensão for atualizada, a alteração será perdida."
private const val BASE_URL_PREF = "overrideBaseUrl"
private const val BASE_URL_PREF_TITLE = "Editar URL da fonte"
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
private const val RESTART_APP_MESSAGE = "Reinicie o aplicativo para aplicar as alterações"
private const val API_BASE_URL_PREF = "overrideApiUrl"
private const val API_BASE_URL_PREF_TITLE = "Editar URL da API da fonte"
private const val API_DEFAULT_BASE_URL_PREF = "defaultApiUrl"
private const val DEFAULT_PREF = "defaultPref"
}
}

View File

@ -1,11 +1,19 @@
package eu.kanade.tachiyomi.extension.pt.sussyscan
package eu.kanade.tachiyomi.multisrc.greenshit
import eu.kanade.tachiyomi.extension.pt.sussyscan.SussyToons.Companion.CDN_URL
import android.annotation.SuppressLint
import eu.kanade.tachiyomi.multisrc.greenshit.GreenShit.Companion.CDN_URL
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.tryParse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.jsoup.Jsoup
import java.text.Normalizer
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class ResultDto<T>(
@ -20,19 +28,60 @@ class ResultDto<T>(
fun hasNextPage() = currentPage < lastPage
fun toSMangaList() = (results as List<MangaDto>)
.filterNot { it.slug.isNullOrBlank() }.map { it.toSManga() }
}
fun toSMangaList(): List<SManga> = (results as List<MangaDto>)
.map { it.apply { slug = it.slug ?: name.createSlug() } }
.map(MangaDto::toSManga)
@Serializable
class WrapperDto(
@SerialName("dataTop")
val popular: ResultDto<List<MangaDto>>?,
@JsonNames("atualizacoesInicial")
private val dataLatest: ResultDto<List<MangaDto>>?,
fun toSChapterList(): List<SChapter> = (results as WrapperChapterDto)
.chapters.map {
SChapter.create().apply {
name = it.name
CHAPTER_NUMBER_REGEX.find(it.name)?.groups?.get(0)?.value?.let {
chapter_number = it.toFloat()
}
url = "/capitulo/${it.id}"
date_upload = dateFormat.tryParse(it.updateAt)
}
}.sortedByDescending(SChapter::chapter_number)
) {
val latest: ResultDto<List<MangaDto>> get() = dataLatest!!
fun toPageList(): List<Page> {
val dto = (results as ChapterPageDto)
val chapter = dto.chapterNumber.let { number ->
number.takeIf { it.isNotInteger() } ?: number.toInt()
}
return dto.pages.mapIndexed { index, image ->
val imageUrl = when {
image.isWordPressContent() -> {
CDN_URL.toHttpUrl().newBuilder()
.addPathSegments("wp-content/uploads/WP-manga/data")
.addPathSegments(image.src.toPathSegment())
.build()
}
else -> {
"$CDN_URL/scans/${dto.manga.scanId}/obras/${dto.manga.id}/capitulos/$chapter/${image.src}"
.toHttpUrl()
}
}
Page(index, imageUrl = imageUrl.toString())
}
}
private fun Float.isNotInteger(): Boolean = toInt() < this
private fun String.createSlug(): String {
return Normalizer.normalize(this, Normalizer.Form.NFD)
.trim()
.replace("\\p{InCombiningDiacriticalMarks}+".toRegex(), "")
.replace("\\p{Punct}".toRegex(), "")
.replace("\\s+".toRegex(), "-")
.lowercase()
}
companion object {
@SuppressLint("SimpleDateFormat")
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
val CHAPTER_NUMBER_REGEX = """\d+(\.\d+)?""".toRegex()
}
}
@Serializable
@ -46,7 +95,7 @@ class MangaDto(
@SerialName("obr_nome")
val name: String,
@SerialName("obr_slug")
val slug: String?,
var slug: String?,
@SerialName("status")
val status: MangaStatus,
@SerialName("scan_id")
@ -105,8 +154,6 @@ class ChapterDto(
val id: Int,
@SerialName("cap_nome")
val name: String,
@SerialName("cap_numero")
val chapterNumber: Float?,
@SerialName("cap_lancado_em")
val updateAt: String,
)
@ -124,7 +171,7 @@ class ChapterPageDto(
@SerialName("obra")
val manga: MangaReferenceDto,
@SerialName("cap_numero")
val chapterNumber: Int,
val chapterNumber: Float,
) {
@Serializable
class MangaReferenceDto(
@ -139,7 +186,16 @@ class ChapterPageDto(
class PageDto(
val src: String,
@SerialName("numero")
val number: Int? = null,
val number: Float? = null,
) {
fun isWordPressContent(): Boolean = number == null
}
/**
* Normalizes path segments:
* Ex: [ "/a/b/", "/a/b", "a/b/", "a/b" ]
* Result: "a/b"
*/
private fun String.toPathSegment() = this.trim().split("/")
.filter(String::isNotEmpty)
.joinToString("/")

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 31
baseVersionCode = 33

View File

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@ -73,10 +74,10 @@ abstract class GroupLe(
override fun latestUpdatesSelector() = popularMangaSelector()
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}", headers)
GET("$baseUrl/list?sortType=rate&offset=${50 * (page - 1)}", headers)
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}", headers)
GET("$baseUrl/list?sortType=updated&offset=${50 * (page - 1)}", headers)
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
@ -103,15 +104,73 @@ abstract class GroupLe(
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url =
"$baseUrl/search/advancedResults?offset=${50 * (page - 1)}".toHttpUrl()
.newBuilder()
val url = "$baseUrl/search/advancedResults?offset=${50 * (page - 1)}"
.toHttpUrl()
.newBuilder()
if (query.isNotEmpty()) {
url.addQueryParameter("q", query)
}
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is GenreList -> filter.state.forEach { genre ->
if (genre.state != Filter.TriState.STATE_IGNORE) {
url.addQueryParameter(genre.id, arrayOf("=", "=in", "=ex")[genre.state])
}
}
is CategoryList -> filter.state.forEach { category ->
if (category.state != Filter.TriState.STATE_IGNORE) {
url.addQueryParameter(category.id, arrayOf("=", "=in", "=ex")[category.state])
}
}
is AgeList -> filter.state.forEach { age ->
if (age.state != Filter.TriState.STATE_IGNORE) {
url.addQueryParameter(age.id, arrayOf("=", "=in", "=ex")[age.state])
}
}
is MoreList -> filter.state.forEach { more ->
if (more.state != Filter.TriState.STATE_IGNORE) {
url.addQueryParameter(more.id, arrayOf("=", "=in", "=ex")[more.state])
}
}
is AdditionalFilterList -> filter.state.forEach { fils ->
if (fils.state != Filter.TriState.STATE_IGNORE) {
url.addQueryParameter(fils.id, arrayOf("=", "=in", "=ex")[fils.state])
}
}
is OrderBy -> {
url.addQueryParameter(
"sortType",
arrayOf("RATING", "POPULARITY", "YEAR", "NAME", "DATE_CREATE", "DATE_UPDATE", "USER_RATING")[filter.state],
)
}
else -> {}
}
}
return GET(url.toString().replace("=%3D", "="), headers)
}
protected class OrderBy : Filter.Select<String>(
"Сортировка",
arrayOf("По популярности", "Популярно сейчас", "По году", "По алфавиту", "Новинки", "По дате обновления", "По рейтингу"),
)
protected class Genre(name: String, val id: String) : Filter.TriState(name)
protected class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Жанры", genres)
protected class CategoryList(categories: List<Genre>) : Filter.Group<Genre>("Категории", categories)
protected class AgeList(ages: List<Genre>) : Filter.Group<Genre>("Возрастная рекомендация", ages)
protected class MoreList(moren: List<Genre>) : Filter.Group<Genre>("Прочее", moren)
protected class AdditionalFilterList(fils: List<Genre>) : Filter.Group<Genre>("Фильтры", fils)
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select(".expandable").first()!!
val rawCategory = infoElement.select("span.elem_category").text()

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 8
baseVersionCode = 9

View File

@ -127,7 +127,7 @@ abstract class Iken(
val userId = userIdRegex.find(response.body.string())?.groupValues?.get(1) ?: ""
val id = response.request.url.fragment!!
val chapterUrl = "$apiUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid=$userId"
val chapterUrl = "$apiUrl/api/chapters?postId=$id&skip=0&take=900&order=desc&userid=$userId"
val chapterResponse = client.newCall(GET(chapterUrl, headers)).execute()
val data = chapterResponse.parseAs<Post<ChapterListResponse>>()

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 20
baseVersionCode = 21

View File

@ -2,11 +2,13 @@ package eu.kanade.tachiyomi.multisrc.kemono
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.tryParse
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.double
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class KemonoFavouritesDto(
val id: String,
@ -25,7 +27,7 @@ class KemonoCreatorDto(
) {
var fav: Long = 0
val updatedDate get() = when {
updated.isString -> dateFormat.parse(updated.content)?.time ?: 0
updated.isString -> dateFormat.tryParse(updated.content)
else -> (updated.double * 1000).toLong()
}
@ -62,7 +64,7 @@ class KemonoPostDto(
private val service: String,
private val user: String,
private val title: String,
private val added: String,
private val added: String?,
private val published: String?,
private val edited: String?,
private val file: KemonoFileDto,
@ -80,13 +82,13 @@ class KemonoPostDto(
}.distinctBy { it.path }.map { it.toString() }
fun toSChapter() = SChapter.create().apply {
val postDate = dateFormat.parse(edited ?: published ?: added)
val postDate = dateFormat.tryParse(edited ?: published ?: added)
url = "/$service/user/$user/post/$id"
date_upload = postDate?.time ?: 0
date_upload = postDate
name = title.ifBlank {
val postDateString = when {
postDate != null && postDate.time != 0L -> chapterNameDateFormat.format(postDate)
postDate != 0L -> chapterNameDateFormat.format(postDate)
else -> "unknown date"
}

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 15
baseVersionCode = 17
dependencies {
api(project(":lib:i18n"))

View File

@ -59,8 +59,16 @@ abstract class Keyoapp(
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
override fun popularMangaSelector(): String =
"div.flex-col div.grid > div.group.border, div:has(h2:contains(Trending)) + div .group.overflow-hidden.grid"
open val popularMangaTitleSelector = listOf(
"Popular",
"Popularie",
"Trending",
)
override fun popularMangaSelector(): String = selector(
"div:contains(%s) + div .group.overflow-hidden.grid",
popularMangaTitleSelector,
)
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.getImageUrl("*[style*=background-image]")
@ -243,7 +251,7 @@ abstract class Keyoapp(
override fun chapterListSelector(): String {
if (!preferences.showPaidChapters) {
return "#chapters > a:not(:has(.text-sm span:matches(Upcoming))):not(:has(img[src*=Coin.svg]))"
return "#chapters > a:not(:has(.text-sm span:matches(Upcoming))):not(:has(img[alt~=Coin]))"
}
return "#chapters > a:not(:has(.text-sm span:matches(Upcoming)))"
}
@ -357,6 +365,10 @@ abstract class Keyoapp(
return now.timeInMillis
}
private fun selector(selector: String, contains: List<String>): String {
return contains.joinToString { selector.replace("%s", it) }
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_PAID_CHAPTERS_PREF

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 41
baseVersionCode = 42
dependencies {
api(project(":lib:cryptoaes"))

View File

@ -245,11 +245,13 @@ abstract class Madara(
val mangaUrl = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment(mangaSubString)
addPathSegment(query.substringAfter(URL_SEARCH_PREFIX))
addPathSegment("") // add trailing slash
}.build()
return client.newCall(GET(mangaUrl, headers))
.asObservableSuccess().map { response ->
val manga = mangaDetailsParse(response).apply {
setUrlWithoutDomain(mangaUrl.toString())
initialized = true
}
MangasPage(listOf(manga), false)
@ -977,6 +979,8 @@ abstract class Madara(
open val pageListParseSelector = "div.page-break, li.blocks-gallery-item, .reading-content .text-left:not(:has(.blocks-gallery-item)) img"
open val chapterProtectorSelector = "#chapter-protector-data"
open val chapterProtectorPasswordPrefix = "wpmangaprotectornonce='"
open val chapterProtectorDataPrefix = "chapter_data='"
override fun pageListParse(document: Document): List<Page> {
launchIO { countViews(document) }
@ -992,11 +996,11 @@ abstract class Madara(
?.let { Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) }
?: chapterProtector.html()
val password = chapterProtectorHtml
.substringAfter("wpmangaprotectornonce='")
.substringAfter(chapterProtectorPasswordPrefix)
.substringBefore("';")
val chapterData = json.parseToJsonElement(
chapterProtectorHtml
.substringAfter("chapter_data='")
.substringAfter(chapterProtectorDataPrefix)
.substringBefore("';")
.replace("\\/", "/"),
).jsonObject

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 6
baseVersionCode = 7

View File

@ -85,6 +85,9 @@ abstract class MangaBox(
private fun useAltCdnInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
if (cdnSet.isEmpty()) {
return chain.proceed(request)
}
val requestTag = request.tag(MangaBoxFallBackTag::class.java)
val originalResponse: Response? = try {
chain.proceed(request)
@ -346,11 +349,10 @@ abstract class MangaBox(
}
override fun pageListParse(document: Document): List<Page> {
val element = document.select("head > script").lastOrNull()
?: return emptyList()
val content = document.select("script:containsData(cdns =)").joinToString("\n") { it.data() }
val cdns =
extractArray(element.html(), "cdns") + extractArray(element.html(), "backupImage")
val chapterImages = extractArray(element.html(), "chapterImages")
extractArray(content, "cdns") + extractArray(content, "backupImage")
val chapterImages = extractArray(content, "chapterImages")
// Add all parsed cdns to set
cdnSet.addAll(cdns)
@ -369,6 +371,10 @@ abstract class MangaBox(
}
Page(i, document.location(), parsedUrl)
}.ifEmpty {
document.select("div.container-chapter-reader > img").mapIndexed { i, img ->
Page(i, imageUrl = img.absUrl("src"))
}
}
}

View File

@ -2,8 +2,9 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 30
baseVersionCode = 34
dependencies {
api(project(":lib:randomua"))
//noinspection UseTomlInstead
implementation("org.brotli:dec:0.1.2")
}

View File

@ -1,62 +1,71 @@
package eu.kanade.tachiyomi.multisrc.mangahub
import eu.kanade.tachiyomi.lib.randomua.UserAgentType
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.Cookie
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import okhttp3.ResponseBody.Companion.toResponseBody
import org.brotli.dec.BrotliInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.text.ParseException
import java.net.URLEncoder
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.locks.ReentrantLock
import java.util.zip.GZIPInputStream
import kotlin.random.Random
abstract class MangaHub(
override val name: String,
final override val baseUrl: String,
override val lang: String,
private val mangaSource: String,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MM-dd-yyyy", Locale.US),
) : ParsedHttpSource() {
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH),
) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
private var baseApiUrl = "https://api.mghcdn.com"
private var baseCdnUrl = "https://imgx.mghcdn.com"
private val regex = Regex("mhub_access=([^;]+)")
private val baseApiUrl = "https://api.mghcdn.com"
private val baseCdnUrl = "https://imgx.mghcdn.com"
private val baseThumbCdnUrl = "https://thumb.mghcdn.com"
private val apiRegex = Regex("mhub_access=([^;]+)")
private val spaceRegex = Regex("\\s+")
private val apiErrorRegex = Regex("""rate\s*limit|api\s*key""")
private val preferences: SharedPreferences by getPreferencesLazy()
private fun SharedPreferences.getUseGenericTitlePref(): Boolean = getBoolean(
PREF_USE_GENERIC_TITLE,
false,
)
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.setRandomUserAgent(
userAgentType = UserAgentType.DESKTOP,
filterInclude = listOf("chrome"),
)
.addInterceptor(::apiAuthInterceptor)
.rateLimit(1)
.addNetworkInterceptor(::compatEncodingInterceptor)
.build()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
@ -69,60 +78,158 @@ abstract class MangaHub(
.add("Sec-Fetch-Site", "same-origin")
.add("Upgrade-Insecure-Requests", "1")
open val json: Json by injectLazy()
private fun postRequestGraphQL(query: String, refreshUrl: String? = null): Request {
val requestHeaders = headersBuilder()
.set("Accept", "application/json")
.set("Content-Type", "application/json")
.set("Origin", baseUrl)
.set("Sec-Fetch-Dest", "empty")
.set("Sec-Fetch-Mode", "cors")
.set("Sec-Fetch-Site", "cross-site")
.removeAll("Upgrade-Insecure-Requests")
.build()
val body = buildJsonObject {
put("query", query)
}
return POST("$baseApiUrl/graphql", requestHeaders, body.toString().toRequestBody())
.newBuilder()
.tag(GraphQLTag::class.java, GraphQLTag(refreshUrl))
.build()
}
// Normally this gets handled properly but in older forks such as TachiyomiJ2K, we have to manually intercept it
// as they have an outdated implementation of NetworkHelper.
private fun compatEncodingInterceptor(chain: Interceptor.Chain): Response {
var response = chain.proceed(chain.request())
val contentEncoding = response.header("Content-Encoding")
if (contentEncoding == "gzip") {
val parsedBody = response.body.byteStream().let { gzipInputStream ->
GZIPInputStream(gzipInputStream).use { inputStream ->
val outputStream = ByteArrayOutputStream()
inputStream.copyTo(outputStream)
outputStream.toByteArray()
}
}
response = response.createNewWithCompatBody(parsedBody)
} else if (contentEncoding == "br") {
val parsedBody = response.body.byteStream().let { brotliInputStream ->
BrotliInputStream(brotliInputStream).use { inputStream ->
val outputStream = ByteArrayOutputStream()
inputStream.copyTo(outputStream)
outputStream.toByteArray()
}
}
response = response.createNewWithCompatBody(parsedBody)
}
return response
}
private fun Response.createNewWithCompatBody(outputStream: ByteArray): Response {
return this.newBuilder()
.body(outputStream.toResponseBody(this.body.contentType()))
.removeHeader("Content-Encoding")
.build()
}
private fun apiAuthInterceptor(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val request = chain.request()
val tag = request.tag(GraphQLTag::class.java)
?: return chain.proceed(request) // We won't intercept non-graphql requests (like image retrieval)
return try {
tryApiRequest(chain, request)
} catch (e: Throwable) {
val noCookie = e is MangaHubCookieNotFound
val apiError = e is ApiErrorException &&
apiErrorRegex.containsMatchIn(e.message ?: "")
if (noCookie || apiError) {
refreshApiKey(tag.refreshUrl)
tryApiRequest(chain, request)
} else {
throw e
}
}
}
private fun tryApiRequest(chain: Interceptor.Chain, request: Request): Response {
val cookie = client.cookieJar
.loadForRequest(baseUrl.toHttpUrl())
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }
?: throw MangaHubCookieNotFound()
val request =
if (originalRequest.url.toString() == "$baseApiUrl/graphql" && cookie != null) {
originalRequest.newBuilder()
.header("x-mhub-access", cookie.value)
.build()
} else {
originalRequest
}
val apiRequest = request.newBuilder()
.header("x-mhub-access", cookie.value)
.build()
return chain.proceed(request)
}
val response = chain.proceed(apiRequest)
private fun refreshApiKey(chapter: SChapter) {
val slug = "$baseUrl${chapter.url}"
.toHttpUrlOrNull()
?.pathSegments
?.get(1)
val apiResponse = response.peekBody(Long.MAX_VALUE).string()
.parseAs<ApiResponseError>()
val url = if (slug != null) {
"$baseUrl/manga/$slug".toHttpUrl()
} else {
baseUrl.toHttpUrl()
if (apiResponse.errors != null) {
response.close() // Avoid leaks
val errors = apiResponse.errors.joinToString("\n") { it.message }
throw ApiErrorException(errors)
}
val oldKey = client.cookieJar
.loadForRequest(baseUrl.toHttpUrl())
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
return response
}
for (i in 1..2) {
// Clear key cookie
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
client.cookieJar.saveFromResponse(url, listOf(cookie))
private class MangaHubCookieNotFound : IOException("mhub_access cookie not found")
private class ApiErrorException(errorMessage: String) : IOException(errorMessage)
// We try requesting again with param if the first one fails
val query = if (i == 2) "?reloadKey=1" else ""
private val lock = ReentrantLock()
private var refreshed = 0L
try {
val response = client.newCall(GET("$url$query", headers)).execute()
val returnedKey = response.headers["set-cookie"]?.let { regex.find(it)?.groupValues?.get(1) }
response.close() // Avoid potential resource leaks
private fun refreshApiKey(refreshUrl: String? = null) {
if (refreshed + 10000 < System.currentTimeMillis() && lock.tryLock()) {
val url = when {
refreshUrl != null -> refreshUrl
else -> "$baseUrl/chapter/martial-peak/chapter-${Random.nextInt(1000, 3000)}"
}.toHttpUrl()
if (returnedKey != oldKey) break; // Break out of loop since we got an allegedly valid API key
} catch (_: IOException) {
throw IOException("An error occurred while obtaining a new API key") // Show error
val oldKey = client.cookieJar
.loadForRequest(baseUrl.toHttpUrl())
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
for (i in 1..2) {
// Clear key cookie
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
client.cookieJar.saveFromResponse(url, listOf(cookie))
try {
// We try requesting again with param if the first one fails
val query = if (i == 2) "?reloadKey=1" else ""
val response = client.newCall(
GET(
"$url$query",
headers.newBuilder()
.set("Referer", "$baseUrl/manga/${url.pathSegments[1]}")
.build(),
),
).execute()
val returnedKey =
response.headers["set-cookie"]?.let { apiRegex.find(it)?.groupValues?.get(1) }
response.close() // Avoid potential resource leaks
if (returnedKey != oldKey) break // Break out of loop since we got an allegedly valid API key
} catch (_: Throwable) {
lock.unlock()
throw Exception("An error occurred while obtaining a new API key") // Show error
}
}
refreshed = System.currentTimeMillis()
lock.unlock()
} else {
lock.lock() // wait here until lock is released
lock.unlock()
}
}
@ -133,35 +240,36 @@ abstract class MangaHub(
val signature: String,
)
private fun Element.toSignature(): String {
val author = this.select("small").text()
val chNum = this.select(".col-sm-6 a:contains(#)").text()
val genres = this.select(".genre-label").joinToString { it.text() }
private fun ApiMangaSearchItem.toSignature(): String {
val author = this.author
val chNum = this.latestChapter
val genres = this.genres
return author + chNum + genres
}
// popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/popular/page/$page", headers)
private fun mangaRequest(page: Int, order: String): Request {
return postRequestGraphQL(searchQuery(mangaSource, "", "all", order, page))
}
// popular
override fun popularMangaRequest(page: Int): Request = mangaRequest(page, "POPULAR")
// often enough there will be nearly identical entries with slightly different
// titles, URLs, and image names. in order to cut these "duplicates" down,
// assign a "signature" based on author name, chapter number, and genres
// if all of those are the same, then it it's the same manga
override fun popularMangaParse(response: Response): MangasPage {
val doc = response.asJsoup()
val mangaList = response.parseAs<ApiSearchResponse>()
val mangas = doc.select(popularMangaSelector())
.map {
SMangaDTO(
it.select("h4 a").attr("abs:href"),
it.select("h4 a").text(),
it.select("img").attr("abs:src"),
it.toSignature(),
)
}
val mangas = mangaList.data.search.rows.map {
SMangaDTO(
"$baseUrl/manga/${it.slug}",
it.title,
"$baseThumbCdnUrl/${it.image}",
it.toSignature(),
)
}
.distinctBy { it.signature }
.map {
SManga.create().apply {
@ -170,221 +278,171 @@ abstract class MangaHub(
thumbnail_url = it.thumbnailUrl
}
}
return MangasPage(mangas, doc.select(popularMangaNextPageSelector()).isNotEmpty())
// Entries have a max of 30 per request
return MangasPage(mangas, mangaList.data.search.rows.count() == 30)
}
override fun popularMangaSelector() = ".col-sm-6:not(:has(a:contains(Yaoi)))"
override fun popularMangaFromElement(element: Element): SManga {
throw UnsupportedOperationException()
}
override fun popularMangaNextPageSelector() = "ul.pager li.next > a"
// latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/updates/page/$page", headers)
return mangaRequest(page, "LATEST")
}
override fun latestUpdatesParse(response: Response): MangasPage {
return popularMangaParse(response)
}
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element): SManga {
throw UnsupportedOperationException()
}
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
// search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/search/page/$page".toHttpUrl().newBuilder()
url.addQueryParameter("q", query)
var order = "POPULAR"
var genres = "all"
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is OrderBy -> {
val order = filter.values[filter.state]
url.addQueryParameter("order", order.key)
order = filter.values[filter.state].key
}
is GenreList -> {
val genre = filter.values[filter.state]
url.addQueryParameter("genre", genre.key)
genres = filter.included.joinToString(",").takeIf { it.isNotBlank() } ?: "all"
}
else -> {}
}
}
return GET(url.build(), headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga {
throw UnsupportedOperationException()
return postRequestGraphQL(searchQuery(mangaSource, query, genres, order, page))
}
override fun searchMangaParse(response: Response): MangasPage {
return popularMangaParse(response)
}
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// manga details
override fun mangaDetailsParse(document: Document): SManga {
val manga = SManga.create()
manga.title = document.select(".breadcrumb .active span").text()
manga.author = document.select("div:has(h1) span:contains(Author) + span").first()?.text()
manga.artist = document.select("div:has(h1) span:contains(Artist) + span").first()?.text()
manga.genre = document.select(".row p a").joinToString { it.text() }
manga.description = document.select(".tab-content p").first()?.text()
manga.thumbnail_url = document.select("img.img-responsive").first()
?.attr("src")
override fun mangaDetailsRequest(manga: SManga): Request {
return postRequestGraphQL(
mangaDetailsQuery(mangaSource, manga.url.removePrefix("/manga/")),
refreshUrl = "$baseUrl${manga.url}",
)
}
document.select("div:has(h1) span:contains(Status) + span").first()?.text()?.also { statusText ->
when {
statusText.contains("ongoing", true) -> manga.status = SManga.ONGOING
statusText.contains("completed", true) -> manga.status = SManga.COMPLETED
else -> manga.status = SManga.UNKNOWN
override fun mangaDetailsParse(response: Response): SManga {
val rawManga = response.parseAs<ApiMangaDetailsResponse>()
return SManga.create().apply {
title = rawManga.data.manga.title!!
author = rawManga.data.manga.author
artist = rawManga.data.manga.artist
genre = rawManga.data.manga.genres
thumbnail_url = "$baseThumbCdnUrl/${rawManga.data.manga.image}"
status = when (rawManga.data.manga.status) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
// add alternative name to manga description
document.select("h1 small").firstOrNull()?.ownText()?.let { alternativeName ->
if (alternativeName.isNotBlank()) {
manga.description = manga.description.orEmpty().let {
if (it.isBlank()) {
"Alternative Name: $alternativeName"
} else {
"$it\n\nAlternative Name: $alternativeName"
}
description = buildString {
rawManga.data.manga.description?.let(::append)
// Add alternative title
val altTitle = rawManga.data.manga.alternativeTitle
if (!altTitle.isNullOrBlank()) {
if (isNotBlank()) append("\n\n")
append("Alternative Name: $altTitle")
}
}
}
return manga
}
// chapters
override fun getMangaUrl(manga: SManga): String = "$baseUrl${manga.url}"
// Chapters
override fun chapterListRequest(manga: SManga): Request {
return postRequestGraphQL(
mangaChapterListQuery(mangaSource, manga.url.removePrefix("/manga/")),
refreshUrl = "$baseUrl${manga.url}",
)
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
val head = document.head()
return document.select(chapterListSelector()).map { chapterFromElement(it, head) }
val chapterList = response.parseAs<ApiMangaDetailsResponse>()
val useGenericTitle = preferences.getUseGenericTitlePref()
return chapterList.data.manga.chapters!!.map {
SChapter.create().apply {
val numberString = "${if (it.number % 1 == 0f) it.number.toInt() else it.number}"
name = if (!useGenericTitle) {
generateChapterName(it.title.trim().replace(spaceRegex, " "), numberString)
} else {
generateGenericChapterName(numberString)
}
url = "/${chapterList.data.manga.slug}/chapter-${it.number}"
chapter_number = it.number
date_upload = dateFormat.tryParse(it.date)
}
}.reversed() // The response is sorted in ASC format so we need to reverse it
}
override fun chapterListSelector() = ".tab-content ul li"
private fun chapterFromElement(element: Element, head: Element): SChapter {
val chapter = SChapter.create()
val potentialLinks = element.select("a[href*='$baseUrl/chapter/']:not([rel*=nofollow]):not([rel*=noreferrer])")
var visibleLink = ""
potentialLinks.forEach { a ->
val className = a.className()
val styles = head.select("style").html()
if (!styles.contains(".$className { display:none; }")) {
visibleLink = a.attr("href")
return@forEach
}
private fun generateChapterName(title: String, number: String): String {
return if (title.contains(number)) {
title
} else if (title.isNotBlank()) {
"Chapter $number - $title"
} else {
generateGenericChapterName(number)
}
chapter.setUrlWithoutDomain(visibleLink)
chapter.name = chapter.url.trimEnd('/').substringAfterLast('/').replace('-', ' ')
chapter.date_upload = element.select("small.UovLc").first()?.text()?.let { parseChapterDate(it) } ?: 0
return chapter
}
override fun chapterFromElement(element: Element): SChapter {
throw UnsupportedOperationException()
private fun generateGenericChapterName(number: String): String {
return "Chapter $number"
}
private fun parseChapterDate(date: String): Long {
val now = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
var parsedDate = 0L
when {
"just now" in date || "less than an hour" in date -> {
parsedDate = now.timeInMillis
}
// parses: "1 hour ago" and "2 hours ago"
"hour" in date -> {
val hours = date.replaceAfter(" ", "").trim().toInt()
parsedDate = now.apply { add(Calendar.HOUR, -hours) }.timeInMillis
}
// parses: "Yesterday" and "2 days ago"
"day" in date -> {
val days = date.replace("days ago", "").trim().toIntOrNull() ?: 1
parsedDate = now.apply { add(Calendar.DAY_OF_YEAR, -days) }.timeInMillis
}
// parses: "2 weeks ago"
"weeks" in date -> {
val weeks = date.replace("weeks ago", "").trim().toInt()
parsedDate = now.apply { add(Calendar.WEEK_OF_YEAR, -weeks) }.timeInMillis
}
// parses: "12-20-2019" and defaults everything that wasn't taken into account to 0
else -> {
try {
parsedDate = dateFormat.parse(date)?.time ?: 0L
} catch (e: ParseException) { /*nothing to do, parsedDate is initialized with 0L*/ }
}
}
return parsedDate
}
override fun getChapterUrl(chapter: SChapter): String = "$baseUrl/chapter${chapter.url}"
// pages
// Pages
override fun pageListRequest(chapter: SChapter): Request {
val body = buildJsonObject {
put("query", PAGES_QUERY)
put(
"variables",
buildJsonObject {
val chapterUrl = chapter.url.split("/")
val chapterUrl = chapter.url.split("/")
put("mangaSource", mangaSource)
put("slug", chapterUrl[2])
put("number", chapterUrl[3].substringAfter("-").toFloat())
},
)
}
.toString()
.toRequestBody()
return postRequestGraphQL(
pagesQuery(mangaSource, chapterUrl[1], chapterUrl[2].substringAfter("-").toFloat()),
refreshUrl = "$baseUrl/chapter${chapter.url}",
)
}
val newHeaders = headersBuilder()
.set("Accept", "application/json")
.set("Content-Type", "application/json")
.set("Origin", baseUrl)
.set("Sec-Fetch-Dest", "empty")
.set("Sec-Fetch-Mode", "cors")
.set("Sec-Fetch-Site", "cross-site")
.removeAll("Upgrade-Insecure-Requests")
override fun pageListParse(response: Response): List<Page> {
val chapterObject = response.parseAs<ApiChapterPagesResponse>()
val pages = chapterObject.data.chapter.pages.parseAs<ApiChapterPages>()
// We'll update the cookie here to match the browser's "recently" opened chapter.
// This mimics how the browser works and gives us more chance to receive a valid API key upon refresh
val now = Calendar.getInstance().time.time
val baseHttpUrl = baseUrl.toHttpUrl()
val recently = buildJsonObject {
putJsonObject((now).toString()) {
put("mangaID", chapterObject.data.chapter.mangaID)
put("number", chapterObject.data.chapter.chapterNumber)
}
}.toString()
val recentlyCookie = Cookie.Builder()
.domain(baseHttpUrl.host)
.name("recently")
.value(URLEncoder.encode(recently, "utf-8"))
.expiresAt(now + 2 * 60 * 60 * 24 * 31) // +2 months
.build()
return POST("$baseApiUrl/graphql", newHeaders, body)
}
// Add/update the cookie
client.cookieJar.saveFromResponse(baseHttpUrl, listOf(recentlyCookie))
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
super.fetchPageList(chapter)
.doOnError { refreshApiKey(chapter) }
.retry(1)
// We'll log our action to the site to further increase the chance of valid API key
val ipRequest = client.newCall(GET("https://api.ipify.org?format=json")).execute()
val ip = ipRequest.parseAs<PublicIPResponse>().ip
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException()
override fun pageListParse(response: Response): List<Page> {
val chapterObject = json.decodeFromString<ApiChapterPagesResponse>(response.body.string())
client.newCall(GET("$baseUrl/action/logHistory2/${chapterObject.data.chapter.manga.slug}/${chapterObject.data.chapter.chapterNumber}?browserID=$ip")).execute().close()
ipRequest.close()
if (chapterObject.data?.chapter == null) {
if (chapterObject.errors != null) {
val errors = chapterObject.errors.joinToString("\n") { it.message }
throw Exception(errors)
}
throw Exception("Unknown error while processing pages")
}
val pages = json.decodeFromString<ApiChapterPages>(chapterObject.data.chapter.pages)
return pages.i.mapIndexed { i, page ->
Page(i, "", "$baseCdnUrl/${pages.p}$page")
return pages.images.mapIndexed { i, page ->
Page(i, "", "$baseCdnUrl/${pages.page}$page")
}
}
@ -401,10 +459,14 @@ abstract class MangaHub(
return GET(page.url, newHeaders)
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
// filters
private class Genre(title: String, val key: String) : Filter.TriState(title) {
private class Genre(title: String, val key: String) : Filter.CheckBox(title) {
fun getGenreKey(): String {
return key
}
override fun toString(): String {
return name
}
@ -417,11 +479,14 @@ abstract class MangaHub(
}
private class OrderBy(orders: Array<Order>) : Filter.Select<Order>("Order", orders, 0)
private class GenreList(genres: Array<Genre>) : Filter.Select<Genre>("Genres", genres, 0)
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres) {
val included: List<String>
get() = state.filter { it.state }.map { it.getGenreKey() }
}
override fun getFilterList() = FilterList(
OrderBy(orderBy),
GenreList(genres),
OrderBy(orderBy),
)
private val orderBy = arrayOf(
@ -432,70 +497,119 @@ abstract class MangaHub(
Order("Completed", "COMPLETED"),
)
private val genres = arrayOf(
Genre("All Genres", "all"),
Genre("[no chapters]", "no-chapters"),
Genre("4-Koma", "4-koma"),
private val genres = listOf(
Genre("Action", "action"),
Genre("Adventure", "adventure"),
Genre("Award Winning", "award-winning"),
Genre("Comedy", "comedy"),
Genre("Cooking", "cooking"),
Genre("Crime", "crime"),
Genre("Demons", "demons"),
Genre("Doujinshi", "doujinshi"),
Genre("Adult", "adult"),
Genre("Drama", "drama"),
Genre("Ecchi", "ecchi"),
Genre("Fantasy", "fantasy"),
Genre("Food", "food"),
Genre("Game", "game"),
Genre("Gender bender", "gender-bender"),
Genre("Harem", "harem"),
Genre("Historical", "historical"),
Genre("Horror", "horror"),
Genre("Isekai", "isekai"),
Genre("Josei", "josei"),
Genre("Kids", "kids"),
Genre("Magic", "magic"),
Genre("Magical Girls", "magical-girls"),
Genre("Manhua", "manhua"),
Genre("Martial Arts", "martial-arts"),
Genre("Romance", "romance"),
Genre("Ecchi", "ecchi"),
Genre("Supernatural", "supernatural"),
Genre("Webtoons", "webtoons"),
Genre("Manhwa", "manhwa"),
Genre("Martial arts", "martial-arts"),
Genre("Fantasy", "fantasy"),
Genre("Harem", "harem"),
Genre("Shounen", "shounen"),
Genre("Manhua", "manhua"),
Genre("Mature", "mature"),
Genre("Seinen", "seinen"),
Genre("Sports", "sports"),
Genre("School Life", "school-life"),
Genre("Smut", "smut"),
Genre("Mystery", "mystery"),
Genre("Psychological", "psychological"),
Genre("Shounen ai", "shounen-ai"),
Genre("Slice of life", "slice-of-life"),
Genre("Shoujo ai", "shoujo-ai"),
Genre("Cooking", "cooking"),
Genre("Horror", "horror"),
Genre("Tragedy", "tragedy"),
Genre("Doujinshi", "doujinshi"),
Genre("Sci-Fi", "sci-fi"),
Genre("Yuri", "yuri"),
Genre("Yaoi", "yaoi"),
Genre("Shoujo", "shoujo"),
Genre("Gender bender", "gender-bender"),
Genre("Josei", "josei"),
Genre("Mecha", "mecha"),
Genre("Medical", "medical"),
Genre("Military", "military"),
Genre("Magic", "magic"),
Genre("4-Koma", "4-koma"),
Genre("Music", "music"),
Genre("Mystery", "mystery"),
Genre("One shot", "one-shot"),
Genre("Oneshot", "oneshot"),
Genre("Parody", "parody"),
Genre("Police", "police"),
Genre("Psychological", "psychological"),
Genre("Romance", "romance"),
Genre("School life", "school-life"),
Genre("Sci fi", "sci-fi"),
Genre("Seinen", "seinen"),
Genre("Shotacon", "shotacon"),
Genre("Shoujo", "shoujo"),
Genre("Shoujo ai", "shoujo-ai"),
Genre("Shoujoai", "shoujoai"),
Genre("Shounen", "shounen"),
Genre("Shounen ai", "shounen-ai"),
Genre("Shounenai", "shounenai"),
Genre("Slice of life", "slice-of-life"),
Genre("Smut", "smut"),
Genre("Space", "space"),
Genre("Sports", "sports"),
Genre("Super Power", "super-power"),
Genre("Superhero", "superhero"),
Genre("Supernatural", "supernatural"),
Genre("Thriller", "thriller"),
Genre("Tragedy", "tragedy"),
Genre("Vampire", "vampire"),
Genre("Webtoon", "webtoon"),
Genre("Webtoons", "webtoons"),
Genre("Isekai", "isekai"),
Genre("Game", "game"),
Genre("Award Winning", "award-winning"),
Genre("Oneshot", "oneshot"),
Genre("Demons", "demons"),
Genre("Military", "military"),
Genre("Police", "police"),
Genre("Super Power", "super-power"),
Genre("Food", "food"),
Genre("Kids", "kids"),
Genre("Magical Girls", "magical-girls"),
Genre("Wuxia", "wuxia"),
Genre("Yuri", "yuri"),
)
Genre("Superhero", "superhero"),
Genre("Thriller", "thriller"),
Genre("Crime", "crime"),
Genre("Philosophical", "philosophical"),
Genre("Adaptation", "adaptation"),
Genre("Full Color", "full-color"),
Genre("Crossdressing", "crossdressing"),
Genre("Reincarnation", "reincarnation"),
Genre("Manga", "manga"),
Genre("Cartoon", "cartoon"),
Genre("Survival", "survival"),
Genre("Comic", "comic"),
Genre("English", "english"),
Genre("Harlequin", "harlequin"),
Genre("Time Travel", "time-travel"),
Genre("Traditional Games", "traditional-games"),
Genre("Reverse Harem", "reverse-harem"),
Genre("Animals", "animals"),
Genre("Aliens", "aliens"),
Genre("Loli", "loli"),
Genre("Video Games", "video-games"),
Genre("Monsters", "monsters"),
Genre("Office Workers", "office-workers"),
Genre("System", "system"),
Genre("Villainess", "villainess"),
Genre("Zombies", "zombies"),
Genre("Vampires", "vampires"),
Genre("Violence", "violence"),
Genre("Monster Girls", "monster-girls"),
Genre("Anthology", "anthology"),
Genre("Ghosts", "ghosts"),
Genre("Delinquents", "delinquents"),
Genre("Post-Apocalyptic", "post-apocalyptic"),
Genre("Xianxia", "xianxia"),
Genre("Xuanhuan", "xuanhuan"),
Genre("R-18", "r-18"),
Genre("Cultivation", "cultivation"),
Genre("Rebirth", "rebirth"),
Genre("Gore", "gore"),
Genre("Russian", "russian"),
Genre("Samurai", "samurai"),
Genre("Ninja", "ninja"),
Genre("Revenge", "revenge"),
Genre("Cheat Systems", "cheat-systems"),
Genre("Dungeons", "dungeons"),
Genre("Overpowered", "overpowered"),
).sortedBy { it.toString() }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = PREF_USE_GENERIC_TITLE
title = "Use generic title"
summary = "Use generic chapter title (\"Chapter 'x'\") instead of the given one.\nNote: May require manga entry to be refreshed."
setDefaultValue(false)
}.let(screen::addPreference)
}
companion object {
private const val PREF_USE_GENERIC_TITLE = "pref_use_generic_title"
}
}

View File

@ -0,0 +1,97 @@
package eu.kanade.tachiyomi.multisrc.mangahub
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
typealias ApiChapterPagesResponse = ApiResponse<ApiChapterData>
typealias ApiSearchResponse = ApiResponse<ApiSearchObject>
typealias ApiMangaDetailsResponse = ApiResponse<ApiMangaObject>
// Base classes
@Serializable
class ApiResponse<T>(
val data: T,
)
@Serializable
class ApiResponseError(
val errors: List<ApiErrorMessages>?,
)
@Serializable
class ApiErrorMessages(
val message: String,
)
@Serializable
class PublicIPResponse(
val ip: String,
)
// Chapter metadata (pages)
@Serializable
class ApiChapterData(
val chapter: ApiChapter,
)
@Serializable
class ApiChapter(
val pages: String,
val mangaID: Int,
@SerialName("number") val chapterNumber: Float,
val manga: ApiMangaData,
)
@Serializable
class ApiChapterPages(
@SerialName("p") val page: String,
@SerialName("i") val images: List<String>,
)
// Search, Popular, Latest
@Serializable
class ApiSearchObject(
val search: ApiSearchResults,
)
@Serializable
class ApiSearchResults(
val rows: List<ApiMangaSearchItem>,
)
@Serializable
class ApiMangaSearchItem(
val title: String,
val slug: String,
val image: String,
val author: String,
val latestChapter: Float,
val genres: String,
)
// Manga Details, Chapters
@Serializable
class ApiMangaObject(
val manga: ApiMangaData,
)
@Serializable
class ApiMangaData(
val title: String?,
val status: String?,
val image: String?,
val author: String?,
val artist: String?,
val genres: String?,
val description: String?,
val alternativeTitle: String?,
val slug: String?,
val chapters: List<ApiMangaChapterList>?,
)
@Serializable
class ApiMangaChapterList(
val number: Float,
val title: String,
val date: String,
)

View File

@ -1,42 +1,70 @@
package eu.kanade.tachiyomi.multisrc.mangahub
import kotlinx.serialization.Serializable
class GraphQLTag(
val refreshUrl: String? = null,
)
private fun buildQuery(queryAction: () -> String) = queryAction().replace("%", "$")
val PAGES_QUERY = buildQuery {
val searchQuery = { mangaSource: String, query: String, genre: String, order: String, page: Int ->
"""
query(%mangaSource: MangaSource, %slug: String!, %number: Float!) {
chapter(x: %mangaSource, slug: %slug, number: %number) {
pages
{
search(x: $mangaSource, q: "$query", genre: "$genre", mod: $order, offset: ${(page - 1) * 30}) {
rows {
title,
author,
slug,
image,
genres,
latestChapter
}
}
}
""".trimIndent()
}
@Serializable
data class ApiErrorMessages(
val message: String,
)
val mangaDetailsQuery = { mangaSource: String, slug: String ->
"""
{
manga(x: $mangaSource, slug: "$slug") {
title,
slug,
status,
image,
author,
artist,
genres,
description,
alternativeTitle
}
}
""".trimIndent()
}
@Serializable
data class ApiChapterPagesResponse(
val data: ApiChapterData?,
val errors: List<ApiErrorMessages>?,
)
val mangaChapterListQuery = { mangaSource: String, slug: String ->
"""
{
manga(x: $mangaSource, slug: "$slug") {
slug,
chapters {
number,
title,
date
}
}
}
""".trimIndent()
}
@Serializable
data class ApiChapterData(
val chapter: ApiChapter?,
)
@Serializable
data class ApiChapter(
val pages: String,
)
@Serializable
data class ApiChapterPages(
val p: String,
val i: List<String>,
)
val pagesQuery = { mangaSource: String, slug: String, number: Float ->
"""
{
chapter(x: $mangaSource, slug: "$slug", number: $number) {
pages,
mangaID,
number,
manga {
slug
}
}
}
""".trimIndent()
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 3
baseVersionCode = 4

View File

@ -14,6 +14,12 @@ class CookieRedirectInterceptor(private val client: OkHttpClient) : Interceptor
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val contentType = response.header("content-type")
if (contentType != null && contentType.startsWith("image/", ignoreCase = true)) {
return response
}
// ignore requests that already have completed the JS challenge
if (response.headers["vary"] != null) return response

View File

@ -0,0 +1,8 @@
ext {
extName = 'BaoBua'
extClass = '.BaoBua'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,109 @@
package eu.kanade.tachiyomi.extension.all.baobua
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstance
import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class BaoBua() : SimpleParsedHttpSource() {
override val baseUrl = "https://www.baobua.net"
override val lang = "all"
override val name = "BaoBua"
override val supportsLatest = false
override fun simpleMangaSelector() = "article.post"
override fun simpleMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a.popunder")!!.absUrl("href"))
title = element.selectFirst("div.read-title")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
}
override fun simpleNextPageSelector(): String = "nav.pagination a.next"
// region popular
override fun popularMangaRequest(page: Int) = GET("$baseUrl?page=$page", headers)
// endregion
// region latest
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
// endregion
// region Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filter = filters.firstInstance<SourceCategorySelector>()
return filter.selectedCategory?.let {
GET(it.buildUrl(baseUrl), headers)
} ?: run {
baseUrl.toHttpUrl().newBuilder()
.addEncodedQueryParameter("q", query)
.addEncodedQueryParameter("page", page.toString())
.build()
.let { GET(it, headers) }
}
}
// region Details
override fun mangaDetailsParse(document: Document): SManga {
val trailItemsEl = document.selectFirst("div.breadcrumb-trail > ul.trail-items")!!
return SManga.create().apply {
title = trailItemsEl.selectFirst("li.trail-end")!!.text()
genre = trailItemsEl.select("li:not(.trail-end):not(.trail-begin)").joinToString { it.text() }
}
}
override fun chapterListSelector() = "html"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
chapter_number = 0F
setUrlWithoutDomain(element.selectFirst("div.breadcrumb-trail li.trail-end > a")!!.absUrl("href"))
date_upload = POST_DATE_FORMAT.tryParse(element.selectFirst("span.item-metadata.posts-date")?.text())
name = "Gallery"
}
// endregion
// region Pages
override fun pageListParse(document: Document): List<Page> {
val basePageUrl = document.selectFirst("div.breadcrumb-trail li.trail-end > a")!!.absUrl("href")
val maxPage: Int = document.selectFirst("div.nav-links > a.next.page-numbers")?.text()?.toInt() ?: 1
var pageIndex = 0
return (1..maxPage).flatMap { pageNum ->
val doc = if (pageNum == 1) {
document
} else {
client.newCall(GET("$basePageUrl?p=$pageNum", headers)).execute().asJsoup()
}
doc.select("div.entry-content.read-details img.wp-image")
.map { Page(pageIndex++, imageUrl = it.absUrl("src")) }
}
}
// endregion
override fun getFilterList(): FilterList = FilterList(
Filter.Header("NOTE: Unable to further search in the category!"),
Filter.Separator(),
SourceCategorySelector.create(baseUrl),
)
companion object {
private val POST_DATE_FORMAT = SimpleDateFormat("EEE MMM dd yyyy", Locale.US)
}
}

View File

@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.extension.all.baobua
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
abstract class SimpleParsedHttpSource : ParsedHttpSource() {
abstract fun simpleMangaSelector(): String
abstract fun simpleMangaFromElement(element: Element): SManga
abstract fun simpleNextPageSelector(): String?
// region popular
override fun popularMangaSelector() = simpleMangaSelector()
override fun popularMangaNextPageSelector() = simpleNextPageSelector()
override fun popularMangaFromElement(element: Element) = simpleMangaFromElement(element)
// endregion
// region last
override fun latestUpdatesSelector() =
if (supportsLatest) simpleMangaSelector() else throw throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) =
if (supportsLatest) simpleMangaFromElement(element) else throw throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() =
if (supportsLatest) simpleNextPageSelector() else throw throw UnsupportedOperationException()
// endregion
// region search
override fun searchMangaSelector() = simpleMangaSelector()
override fun searchMangaFromElement(element: Element) = simpleMangaFromElement(element)
override fun searchMangaNextPageSelector() = simpleNextPageSelector()
// endregion
override fun chapterListSelector() = simpleMangaSelector()
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// endregion
}

View File

@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.extension.all.baobua
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl.Companion.toHttpUrl
data class SourceCategory(private val name: String, var cat: String) {
override fun toString() = this.name
fun buildUrl(baseUrl: String): String {
return "$baseUrl/".toHttpUrl().newBuilder()
.addEncodedQueryParameter("cat", this.cat)
.build()
.toString()
}
}
class SourceCategorySelector(
name: String,
categories: List<SourceCategory>,
) : Filter.Select<SourceCategory>(name, categories.toTypedArray()) {
val selectedCategory: SourceCategory?
get() = if (state > 0) values[state] else null
companion object {
fun create(baseUrl: String): SourceCategorySelector {
val options = listOf(
SourceCategory("unselected", ""),
SourceCategory("大胸美女", "YmpydEtkNzV5NHJKcDJYVGtOVW0yZz09"),
SourceCategory("巨乳美女", "Q09EdlMvMHgweERrUitScTFTaDM4Zz09"),
SourceCategory("全裸写真", "eXZzejJPNFRVNzJqKzFDUmNzZEU2QT09"),
SourceCategory("chinese", "bG9LamJsWWdSbGcyY0FEZytldkhTZz09"),
SourceCategory("chinese models", "OCtTSEI2YzRTcWMvWUsyeDM0aHdzdUIwWDlHMERZUEZaVHUwUEVUVWo3QT0"),
SourceCategory("korean", "Tm1ydGlaZ1A2YWM3a3BvYWh6L3dIdz09"),
SourceCategory("korea", "bzRjeWR0akQrRWpxRE1xOGF6TW5Tdz09"),
SourceCategory("korean models", "TGZTVGtwOCtxTW1TQU1KYWhUb01DQT09"),
SourceCategory("big boobs", "UmFLQVkvVndGNlpPckwvZkpVaEE4UT09"),
SourceCategory("adult", "b2RFSnlwdWxyREMxVmRpcThKVXRLUT09"),
SourceCategory("nude-art", "djFqa293VmFZMEJLdDlUWndsMGtldz09"),
SourceCategory("Asian adult photo", "SHBGZHFueTVNeUlxVHRLaU53RjU2NS9VcjNxRVg3VnhqTGJoK25YaVQ1UT0"),
SourceCategory("cosplay", "OEI2c000ZDBxakwydjZIUVJaRnlMQT09"),
SourceCategory("hot", "c3VRb3RJZ2wrU2tTYmpGSUVqMnFndz09"),
SourceCategory("big breast", "dkQ3b0RiK0xpZDRlMVNSY3lUNkJXQT09"),
)
return SourceCategorySelector("Category", options)
}
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Bato.to'
extClass = '.BatoToFactory'
extVersionCode = 49
extVersionCode = 50
isNsfw = true
}

View File

@ -279,7 +279,7 @@ open class BatoTo(
manga.title = infoElement.select("h3").text().removeEntities()
manga.thumbnail_url = document.select("div.attr-cover img")
.attr("abs:src")
manga.url = infoElement.select("h3 a").attr("abs:href")
manga.setUrlWithoutDomain(infoElement.select("h3 a").attr("abs:href"))
return MangasPage(listOf(manga), false)
}
@ -405,7 +405,7 @@ open class BatoTo(
return Jsoup.parse(response.body.string(), response.request.url.toString(), Parser.xmlParser())
.select("channel > item").map { item ->
SChapter.create().apply {
url = item.selectFirst("guid")!!.text()
setUrlWithoutDomain(item.selectFirst("guid")!!.text())
name = item.selectFirst("title")!!.text()
date_upload = parseAltChapterDate(item.selectFirst("pubDate")!!.text())
}

View File

@ -1,8 +1,12 @@
ext {
extName = 'Buon Dua'
extClass = '.BuonDua'
extVersionCode = 2
extVersionCode = 4
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:randomua"))
}

View File

@ -1,6 +1,9 @@
package eu.kanade.tachiyomi.extension.all.buondua
import eu.kanade.tachiyomi.lib.randomua.UserAgentType
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
@ -8,11 +11,15 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class BuonDua() : ParsedHttpSource() {
override val baseUrl = "https://buondua.com"
@ -20,6 +27,13 @@ class BuonDua() : ParsedHttpSource() {
override val name = "Buon Dua"
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 10, 1, TimeUnit.SECONDS)
.setRandomUserAgent(UserAgentType.MOBILE)
.build()
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
// Latest
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
@ -43,10 +57,10 @@ class BuonDua() : ParsedHttpSource() {
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/hot?start=${20 * (page - 1)}")
}
override fun popularMangaSelector() = latestUpdatesSelector()
// Search
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -57,6 +71,7 @@ class BuonDua() : ParsedHttpSource() {
else -> popularMangaRequest(page)
}
}
override fun searchMangaSelector() = latestUpdatesSelector()
// Details
@ -72,34 +87,27 @@ class BuonDua() : ParsedHttpSource() {
return manga
}
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(element.select(".is-current").first()!!.attr("abs:href"))
chapter.chapter_number = 0F
chapter.name = element.select(".article-header").text()
chapter.date_upload = SimpleDateFormat("H:m DD-MM-yyyy", Locale.US).parse(element.select(".article-info > small").text())?.time ?: 0L
return chapter
}
override fun chapterListSelector() = "html"
// Pages
override fun pageListParse(document: Document): List<Page> {
val numpages = document.selectFirst(".pagination-list")!!.select(".pagination-link")
val pages = mutableListOf<Page>()
numpages.forEachIndexed { index, page ->
val doc = when (index) {
0 -> document
else -> client.newCall(GET(page.attr("abs:href"))).execute().asJsoup()
}
doc.select(".article-fulltext img").forEach {
val itUrl = it.attr("abs:src")
pages.add(Page(pages.size, "", itUrl))
override fun chapterListSelector() = throw UnsupportedOperationException()
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
override fun chapterListParse(response: Response): List<SChapter> {
val doc = response.asJsoup()
val dateUploadStr = doc.selectFirst(".article-info > small")?.text()
val dateUpload = DATE_FORMAT.tryParse(dateUploadStr)
val maxPage = doc.select("nav.pagination:first-of-type a.pagination-link").last()?.text()?.toInt() ?: 1
val basePageUrl = response.request.url
return (maxPage downTo 1).map { page ->
SChapter.create().apply {
setUrlWithoutDomain("$basePageUrl?page=$page")
name = "Page $page"
date_upload = dateUpload
}
}
return pages
}
// Pages
override fun pageListParse(document: Document): List<Page> {
return document.select(".article-fulltext img")
.mapIndexed { i, imgEl -> Page(i, imageUrl = imgEl.absUrl("src")) }
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
@ -114,4 +122,8 @@ class BuonDua() : ParsedHttpSource() {
class TagFilter : Filter.Text("Tag ID")
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
companion object {
private val DATE_FORMAT = SimpleDateFormat("H:m DD-MM-yyyy", Locale.US)
}
}

View File

@ -1,5 +1,7 @@
ignored_groups_title=Ignored Groups
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
ignored_tags_title=Ignored Tags
ignored_tags_summary=Manga with these tags won't show up when browsing.\nOne tag per line (case-insensitive)
show_alternative_titles_title=Show Alternative Titles
show_alternative_titles_on=Adds alternative titles to the description
show_alternative_titles_off=Does not show alternative titles to the description

View File

@ -1,7 +1,7 @@
ext {
extName = 'Comick'
extClass = '.ComickFactory'
extVersionCode = 55
extVersionCode = 56
isNsfw = true
}

View File

@ -79,6 +79,12 @@ abstract class Comick(
}
}.also(screen::addPreference)
EditTextPreference(screen.context).apply {
key = IGNORED_TAGS_PREF
title = intl["ignored_tags_title"]
summary = intl["ignored_tags_summary"]
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_ALTERNATIVE_TITLES_PREF
title = intl["show_alternative_titles_title"]
@ -184,6 +190,14 @@ abstract class Comick(
.orEmpty()
.toSet()
private val SharedPreferences.ignoredTags: String
get() = getString(IGNORED_TAGS_PREF, "")
?.split("\n")
?.map(String::trim)
?.filter(String::isNotEmpty)
.orEmpty()
.joinToString(",")
private val SharedPreferences.showAlternativeTitles: Boolean
get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT)
@ -243,8 +257,13 @@ abstract class Comick(
/** Popular Manga **/
override fun popularMangaRequest(page: Int): Request {
val url = "$apiUrl/v1.0/search?sort=follow&limit=$LIMIT&page=$page&tachiyomi=true"
return GET(url, headers)
return searchMangaRequest(
page = page,
query = "",
filters = FilterList(
SortFilter("follow"),
),
)
}
override fun popularMangaParse(response: Response): MangasPage {
@ -257,8 +276,13 @@ abstract class Comick(
/** Latest Manga **/
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/v1.0/search?sort=uploaded&limit=$LIMIT&page=$page&tachiyomi=true"
return GET(url, headers)
return searchMangaRequest(
page = page,
query = "",
filters = FilterList(
SortFilter("uploaded"),
),
)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
@ -316,7 +340,7 @@ abstract class Comick(
}
private fun addTagQueryParameters(builder: Builder, tags: String, parameterName: String) {
tags.split(",").forEach {
tags.split(",").filter(String::isNotEmpty).forEach {
builder.addQueryParameter(
parameterName,
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
@ -412,6 +436,7 @@ abstract class Comick(
else -> {}
}
}
addTagQueryParameters(this, preferences.ignoredTags, "excluded-tags")
addQueryParameter("tachiyomi", "true")
addQueryParameter("limit", "$LIMIT")
addQueryParameter("page", "$page")
@ -587,6 +612,7 @@ abstract class Comick(
const val SLUG_SEARCH_PREFIX = "id:"
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
private const val IGNORED_TAGS_PREF = "IgnoredTags"
private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles"
const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"

View File

@ -9,7 +9,7 @@ fun getFilters(): FilterList {
GenreFilter("Genre", getGenresList),
DemographicFilter("Demographic", getDemographicList),
TypeFilter("Type", getTypeList),
SortFilter("Sort", getSortsList),
SortFilter(),
StatusFilter("Status", getStatusList),
ContentRatingFilter("Content Rating", getContentRatingList),
CompletedFilter("Completely Scanlated?"),
@ -50,8 +50,8 @@ internal class FromYearFilter(name: String) : TextFilter(name)
internal class ToYearFilter(name: String) : TextFilter(name)
internal class SortFilter(name: String, sortList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, sortList, state)
internal class SortFilter(defaultValue: String? = null, state: Int = 0) :
SelectFilter("Sort", getSortsList, state, defaultValue)
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, statusList, state)
@ -66,8 +66,8 @@ internal open class TextFilter(name: String) : Filter.Text(name)
internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(name)
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0, defaultValue: String? = null) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: state) {
fun getValue() = vals[state].second
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'DeviantArt'
extClass = '.DeviantArt'
extVersionCode = 7
extVersionCode = 8
isNsfw = true
}

View File

@ -134,12 +134,11 @@ class DeviantArt : HttpSource(), ConfigurableSource {
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
}
return chapterList.toList().also(::indexChapterList)
return chapterList.also(::orderChapterList).toList()
}
private fun parseToChapterList(document: Document): List<SChapter> {
val items = document.select("item")
return items.map {
return document.select("item").map {
SChapter.create().apply {
setUrlWithoutDomain(it.selectFirst("link")!!.text())
name = it.selectFirst("title")!!.text()
@ -149,17 +148,15 @@ class DeviantArt : HttpSource(), ConfigurableSource {
}
}
private fun indexChapterList(chapterList: List<SChapter>) {
// DeviantArt allows users to arrange galleries arbitrarily so we will
// primitively index the list by checking the first and last dates
if (chapterList.first().date_upload > chapterList.last().date_upload) {
chapterList.forEachIndexed { i, chapter ->
chapter.chapter_number = chapterList.size - i.toFloat()
}
} else {
chapterList.forEachIndexed { i, chapter ->
chapter.chapter_number = i.toFloat() + 1
}
private fun orderChapterList(chapterList: MutableList<SChapter>) {
// In Mihon's updates tab, chapters are ordered by source instead
// of chapter number, so to avoid updates being shown in reverse,
// disregard source order and order chronologically instead
if (chapterList.first().date_upload < chapterList.last().date_upload) {
chapterList.reverse()
}
chapterList.forEachIndexed { i, chapter ->
chapter.chapter_number = chapterList.size - i.toFloat()
}
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.EternalMangasFactory'
themePkg = 'mangaesp'
baseUrl = 'https://eternalmangas.com'
overrideVersionCode = 2
overrideVersionCode = 5
isNsfw = true
}

View File

@ -2,16 +2,24 @@ package eu.kanade.tachiyomi.extension.all.eternalmangas
import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp
import eu.kanade.tachiyomi.multisrc.mangaesp.SeriesDto
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import rx.Observable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@ -26,16 +34,58 @@ open class EternalMangas(
) {
override val useApiSearch = true
override fun latestUpdatesParse(response: Response): MangasPage {
val responseData = json.decodeFromString<LatestUpdatesDto>(response.body.string())
val mangas = responseData.updates[internalLang]?.flatten()?.map { it.toSManga(seriesPath) } ?: emptyList()
return MangasPage(mangas, false)
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return super.fetchSearchManga(page, "", createSortFilter("views", false))
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return super.fetchSearchManga(page, "", createSortFilter("updated_at", false))
}
override fun List<SeriesDto>.additionalParse(): List<SeriesDto> {
return this.filter { it.language == internalLang }.toMutableList()
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/comics", headers)
}
private val dataUrl = "https://raw.githubusercontent.com/bapeey/extensions-tools/refs/heads/main/keiyoushi/eternalmangas/values.txt"
override fun searchMangaParse(
response: Response,
page: Int,
query: String,
filters: FilterList,
): MangasPage {
val (apiComicsUrl, jsonHeaders, useApi, scriptSelector, comicsRegex) = client.newCall(GET(dataUrl)).execute().body.string().split("\n")
val apiSearch = useApi == "1"
comicsList = if (apiSearch) {
val headersJson = json.parseToJsonElement(jsonHeaders).jsonObject
val apiHeaders = headersBuilder()
headersJson.forEach { (key, jsonElement) ->
var value = jsonElement.jsonPrimitive.contentOrNull.orEmpty()
if (value.startsWith("1-")) {
val match = value.substringAfter("-").toRegex().find(response.body.string())
value = match?.groupValues?.get(1).orEmpty()
} else {
value = value.substringAfter("-")
}
apiHeaders.add(key, value)
}
val apiResponse = client.newCall(GET(apiComicsUrl, apiHeaders.build())).execute()
json.decodeFromString<List<SeriesDto>>(apiResponse.body.string()).toMutableList()
} else {
val script = response.asJsoup().select(scriptSelector).joinToString { it.data() }
val jsonString = comicsRegex.toRegex().find(script)?.groupValues?.get(1)
?: throw Exception(intl["comics_list_error"])
val unescapedJson = jsonString.unescape()
json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList()
}
return parseComicsList(page, query, filters)
}
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val body = jsRedirect(response)
@ -92,7 +142,7 @@ open class EternalMangas(
private fun jsRedirect(response: Response): String {
var body = response.body.string()
val document = Jsoup.parse(body)
document.selectFirst("body > form[method=post]")?.let {
document.selectFirst("body > form[method=post], body > div[hidden] > form[method=post]")?.let {
val action = it.attr("action")
val inputs = it.select("input")
@ -106,8 +156,13 @@ open class EternalMangas(
return body
}
@Serializable
class LatestUpdatesDto(
val updates: Map<String, List<List<SeriesDto>>>,
)
private fun createSortFilter(value: String, ascending: Boolean = false): FilterList {
val sortProperties = getSortProperties()
val index = sortProperties.indexOfFirst { it.value == value }.takeIf { it >= 0 } ?: 0
return FilterList(
SortByFilter("", sortProperties).apply {
state = Filter.Sort.Selection(index, ascending)
},
)
}
}

View File

@ -1,21 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="eu.kanade.tachiyomi.multisrc.a3manga.A3MangaUrlActivity"
android:name=".all.globalcomix.GlobalComixUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<intent-filter
android:autoVerify="false"
tools:targetApi="23">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="${SOURCEHOST}" />
<data android:host="*.${SOURCEHOST}" />
<data android:pathPattern="/truyen-tranh/..*"
android:scheme="${SOURCESCHEME}" />
<data android:host="globalcomix.com" />
<data android:scheme="https" />
<data android:pathPattern="/c/..*" />
</intent-filter>
</activity>
</application>

View File

@ -0,0 +1,5 @@
data_saver=Data saver
data_saver_summary=Enables smaller, more compressed images
invalid_manga_id=Not a valid comic ID
show_locked_chapters=Show chapters with pay-walled pages
show_locked_chapters_summary=Display chapters that require an account with a premium subscription

View File

@ -0,0 +1,12 @@
ext {
extName = 'GlobalComix'
extClass = '.GlobalComixFactory'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:i18n"))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,234 @@
package eu.kanade.tachiyomi.extension.all.globalcomix
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.ChapterDataDto.Companion.createChapter
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.ChapterDto
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.ChaptersDto
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.EntityDto
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.MangaDataDto.Companion.createManga
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.MangaDto
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.MangasDto
import eu.kanade.tachiyomi.extension.all.globalcomix.dto.UnknownEntity
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.plus
import kotlinx.serialization.modules.polymorphic
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
abstract class GlobalComix(final override val lang: String, private val extLang: String = lang) :
ConfigurableSource, HttpSource() {
override val name = "GlobalComix"
override val baseUrl = webUrl
override val supportsLatest = true
private val preferences: SharedPreferences by getPreferencesLazy()
private val json = Json {
isLenient = true
ignoreUnknownKeys = true
serializersModule += SerializersModule {
polymorphic(EntityDto::class) {
defaultDeserializer { UnknownEntity.serializer() }
}
}
}
private val intl = Intl(
language = lang,
baseLanguage = english,
availableLanguages = setOf(english),
classLoader = this::class.java.classLoader!!,
createMessageFileName = { lang -> Intl.createDefaultMessageFileName(lang) },
)
final override fun headersBuilder() = super.headersBuilder().apply {
set("Referer", "$baseUrl/")
set("Origin", baseUrl)
set("x-gc-client", clientId)
set("x-gc-identmode", "cookie")
}
override val client = network.client.newBuilder()
.rateLimit(3)
.build()
private fun simpleQueryRequest(page: Int, orderBy: String?, query: String?): Request {
val url = apiSearchUrl.toHttpUrl().newBuilder()
.addQueryParameter("lang_id[]", extLang)
.addQueryParameter("p", page.toString())
orderBy?.let { url.addQueryParameter("sort", it) }
query?.let { url.addQueryParameter("q", it) }
return GET(url.build(), headers)
}
override fun popularMangaRequest(page: Int): Request =
simpleQueryRequest(page, orderBy = null, query = null)
override fun popularMangaParse(response: Response): MangasPage =
mangaListParse(response)
override fun latestUpdatesRequest(page: Int): Request =
simpleQueryRequest(page, "recent", query = null)
override fun latestUpdatesParse(response: Response): MangasPage =
mangaListParse(response)
private fun mangaListParse(response: Response): MangasPage {
val isSingleItemLookup = response.request.url.toString().startsWith(apiMangaUrl)
return if (!isSingleItemLookup) {
// Normally, the response is a paginated list of mangas
// The results property will be a JSON array
response.parseAs<MangasDto>().payload!!.let { dto ->
MangasPage(
dto.results.map { it -> it.createManga() },
dto.pagination.hasNextPage,
)
}
} else {
// However, when using the 'id:' query prefix (via the UrlActivity for example),
// the response is a single manga and the results property will be a JSON object
MangasPage(
listOf(
response.parseAs<MangaDto>().payload!!
.results
.createManga(),
),
false,
)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// If the query is a slug ID, return the manga directly
if (query.startsWith(prefixIdSearch)) {
val mangaSlugId = query.removePrefix(prefixIdSearch)
if (mangaSlugId.isEmpty()) {
throw Exception(intl["invalid_manga_id"])
}
val url = apiMangaUrl.toHttpUrl().newBuilder()
.addPathSegment(mangaSlugId)
.build()
return GET(url, headers)
}
return simpleQueryRequest(page, orderBy = "relevance", query)
}
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
override fun getMangaUrl(manga: SManga): String = "$webComicUrl/${titleToSlug(manga.title)}"
override fun mangaDetailsRequest(manga: SManga): Request {
val url = apiMangaUrl.toHttpUrl().newBuilder()
.addPathSegment(titleToSlug(manga.title))
.build()
return GET(url, headers)
}
override fun mangaDetailsParse(response: Response): SManga =
response.parseAs<MangaDto>().payload!!
.results
.createManga()
override fun chapterListRequest(manga: SManga): Request {
val url = apiSearchUrl.toHttpUrl().newBuilder()
.addPathSegment(manga.url) // manga.url contains the the comic id
.addPathSegment("releases")
.addQueryParameter("lang_id", extLang)
.addQueryParameter("all", "true")
.toString()
return GET(url, headers)
}
override fun chapterListParse(response: Response): List<SChapter> =
response.parseAs<ChaptersDto>().payload!!.results.filterNot { dto ->
dto.isPremium && !preferences.showLockedChapters
}.map { it.createChapter() }
override fun getChapterUrl(chapter: SChapter): String =
"$baseUrl/read/${chapter.url}"
override fun pageListRequest(chapter: SChapter): Request {
val chapterKey = chapter.url
val url = "$apiChapterUrl/$chapterKey"
return GET(url, headers)
}
override fun pageListParse(response: Response): List<Page> {
val chapterKey = response.request.url.pathSegments.last()
val chapterWebUrl = "$webChapterUrl/$chapterKey"
return response.parseAs<ChapterDto>()
.payload!!
.results
.page_objects!!
.map { dto -> if (preferences.useDataSaver) dto.mobile_image_url else dto.desktop_image_url }
.mapIndexed { index, url -> Page(index, "$chapterWebUrl/$index", url) }
}
override fun imageUrlParse(response: Response): String = ""
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val dataSaverPref = SwitchPreferenceCompat(screen.context).apply {
key = getDataSaverPreferenceKey(extLang)
title = intl["data_saver"]
summary = intl["data_saver_summary"]
setDefaultValue(false)
}
val showLockedChaptersPref = SwitchPreferenceCompat(screen.context).apply {
key = getShowLockedChaptersPreferenceKey(extLang)
title = intl["show_locked_chapters"]
summary = intl["show_locked_chapters_summary"]
setDefaultValue(true)
}
screen.addPreference(dataSaverPref)
screen.addPreference(showLockedChaptersPref)
}
private inline fun <reified T> Response.parseAs(): T = parseAs(json)
private val SharedPreferences.useDataSaver
get() = getBoolean(getDataSaverPreferenceKey(extLang), false)
private val SharedPreferences.showLockedChapters
get() = getBoolean(getShowLockedChaptersPreferenceKey(extLang), true)
companion object {
fun titleToSlug(title: String) = title.trim()
.lowercase(Locale.US)
.replace(titleSpecialCharactersRegex, "-")
val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex()
val dateFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
.apply { timeZone = TimeZone.getTimeZone("UTC") }
}
}

View File

@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.extension.all.globalcomix
const val lockSymbol = "🔒"
// Language codes used for translations
const val english = "en"
// JSON discriminators
const val release = "Release"
const val comic = "Comic"
const val artist = "Artist"
const val releasePage = "ReleasePage"
// Web requests
const val webUrl = "https://globalcomix.com"
const val webComicUrl = "$webUrl/c"
const val webChapterUrl = "$webUrl/read"
const val apiUrl = "https://api.globalcomix.com/v1"
const val apiMangaUrl = "$apiUrl/read"
const val apiChapterUrl = "$apiUrl/readV2"
const val apiSearchUrl = "$apiUrl/comics"
const val clientId = "gck_d0f170d5729446dcb3b55e6b3ebc7bf6"
// Search prefix for title ids
const val prefixIdSearch = "id:"
// Preferences
fun getDataSaverPreferenceKey(extLang: String): String = "dataSaver_$extLang"
fun getShowLockedChaptersPreferenceKey(extLang: String): String = "showLockedChapters_$extLang"

View File

@ -0,0 +1,92 @@
package eu.kanade.tachiyomi.extension.all.globalcomix
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class GlobalComixFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
GlobalComixAlbanian(),
GlobalComixArabic(),
GlobalComixBulgarian(),
GlobalComixBengali(),
GlobalComixBrazilianPortuguese(),
GlobalComixChineseMandarin(),
GlobalComixCzech(),
GlobalComixGerman(),
GlobalComixDanish(),
GlobalComixGreek(),
GlobalComixEnglish(),
GlobalComixSpanish(),
GlobalComixPersian(),
GlobalComixFinnish(),
GlobalComixFilipino(),
GlobalComixFrench(),
GlobalComixHindi(),
GlobalComixHungarian(),
GlobalComixIndonesian(),
GlobalComixItalian(),
GlobalComixHebrew(),
GlobalComixJapanese(),
GlobalComixKorean(),
GlobalComixLatvian(),
GlobalComixMalay(),
GlobalComixDutch(),
GlobalComixNorwegian(),
GlobalComixPolish(),
GlobalComixPortugese(),
GlobalComixRomanian(),
GlobalComixRussian(),
GlobalComixSwedish(),
GlobalComixSlovak(),
GlobalComixSlovenian(),
GlobalComixTamil(),
GlobalComixThai(),
GlobalComixTurkish(),
GlobalComixUkrainian(),
GlobalComixUrdu(),
GlobalComixVietnamese(),
GlobalComixChineseCantonese(),
)
}
class GlobalComixAlbanian : GlobalComix("al")
class GlobalComixArabic : GlobalComix("ar")
class GlobalComixBulgarian : GlobalComix("bg")
class GlobalComixBengali : GlobalComix("bn")
class GlobalComixBrazilianPortuguese : GlobalComix("pt-BR", "br")
class GlobalComixChineseMandarin : GlobalComix("zh-Hans", "cn")
class GlobalComixCzech : GlobalComix("cs", "cz")
class GlobalComixGerman : GlobalComix("de")
class GlobalComixDanish : GlobalComix("dk")
class GlobalComixGreek : GlobalComix("el")
class GlobalComixEnglish : GlobalComix("en")
class GlobalComixSpanish : GlobalComix("es")
class GlobalComixPersian : GlobalComix("fa")
class GlobalComixFinnish : GlobalComix("fi")
class GlobalComixFilipino : GlobalComix("fil", "fo")
class GlobalComixFrench : GlobalComix("fr")
class GlobalComixHindi : GlobalComix("hi")
class GlobalComixHungarian : GlobalComix("hu")
class GlobalComixIndonesian : GlobalComix("id")
class GlobalComixItalian : GlobalComix("it")
class GlobalComixHebrew : GlobalComix("he", "iw")
class GlobalComixJapanese : GlobalComix("ja", "jp")
class GlobalComixKorean : GlobalComix("ko", "kr")
class GlobalComixLatvian : GlobalComix("lv")
class GlobalComixMalay : GlobalComix("ms", "my")
class GlobalComixDutch : GlobalComix("nl")
class GlobalComixNorwegian : GlobalComix("no")
class GlobalComixPolish : GlobalComix("pl")
class GlobalComixPortugese : GlobalComix("pt")
class GlobalComixRomanian : GlobalComix("ro")
class GlobalComixRussian : GlobalComix("ru")
class GlobalComixSwedish : GlobalComix("sv", "se")
class GlobalComixSlovak : GlobalComix("sk")
class GlobalComixSlovenian : GlobalComix("sl")
class GlobalComixTamil : GlobalComix("ta")
class GlobalComixThai : GlobalComix("th")
class GlobalComixTurkish : GlobalComix("tr")
class GlobalComixUkrainian : GlobalComix("uk", "ua")
class GlobalComixUrdu : GlobalComix("ur")
class GlobalComixVietnamese : GlobalComix("vi")
class GlobalComixChineseCantonese : GlobalComix("zh-Hant", "zh")

View File

@ -0,0 +1,45 @@
package eu.kanade.tachiyomi.extension.all.globalcomix
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import kotlin.system.exitProcess
/**
* Springboard that accepts https://globalcomix.com/c/xxx intents and redirects them to
* the main tachiyomi process. The idea is to not install the intent filter unless
* you have this extension installed, but still let the main tachiyomi app control
* things.
*/
class GlobalComixUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
// Supported path: /c/title-slug
if (pathSegments != null && pathSegments.size > 1) {
val titleId = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", prefixIdSearch + titleId)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("GlobalComixUrlActivity", e.toString())
}
} else {
Log.e("GlobalComixUrlActivity", "Received data URL is unsupported: ${intent?.data}")
Toast.makeText(this, "This URL cannot be handled by the GlobalComix extension.", Toast.LENGTH_SHORT).show()
}
finish()
exitProcess(0)
}
}

View File

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import eu.kanade.tachiyomi.extension.all.globalcomix.artist
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
@SerialName(artist)
class ArtistDto(
val name: String, // Slug
val roman_name: String?,
) : EntityDto()

View File

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import eu.kanade.tachiyomi.extension.all.globalcomix.GlobalComix.Companion.dateFormatter
import eu.kanade.tachiyomi.extension.all.globalcomix.lockSymbol
import eu.kanade.tachiyomi.extension.all.globalcomix.release
import eu.kanade.tachiyomi.source.model.SChapter
import keiyoushi.utils.tryParse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
typealias ChapterDto = ResponseDto<ChapterDataDto>
typealias ChaptersDto = PaginatedResponseDto<ChapterDataDto>
@Suppress("PropertyName")
@Serializable
@SerialName(release)
class ChapterDataDto(
val title: String,
val chapter: String, // Stringified number
val key: String, // UUID, required for /readV2 endpoint
val premium_only: Int? = 0,
val published_time: String,
// Only available when calling the /readV2 endpoint
val page_objects: List<PageDataDto>?,
) : EntityDto() {
val isPremium: Boolean
get() = premium_only == 1
companion object {
/**
* Create an [SChapter] instance from the JSON DTO element.
*/
fun ChapterDataDto.createChapter(): SChapter {
val chapterName = mutableListOf<String>()
if (isPremium) {
chapterName.add(lockSymbol)
}
chapter.let {
if (it.isNotEmpty()) {
chapterName.add("Ch.$it")
}
}
title.let {
if (it.isNotEmpty()) {
if (chapterName.isNotEmpty()) {
chapterName.add("-")
}
chapterName.add(it)
}
}
return SChapter.create().apply {
url = key
name = chapterName.joinToString(" ")
chapter_number = chapter.toFloatOrNull() ?: 0f
date_upload = dateFormatter.tryParse(published_time)
}
}
}
}

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import kotlinx.serialization.Serializable
@Serializable
sealed class EntityDto {
val id: Long = -1
}
@Serializable
class UnknownEntity() : EntityDto()

View File

@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import eu.kanade.tachiyomi.extension.all.globalcomix.comic
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
typealias MangaDto = ResponseDto<MangaDataDto>
typealias MangasDto = PaginatedResponseDto<MangaDataDto>
@Suppress("PropertyName")
@Serializable
@SerialName(comic)
class MangaDataDto(
val name: String,
val description: String?,
val status_name: String?,
val category_name: String?,
val image_url: String?,
val artist: ArtistDto,
) : EntityDto() {
companion object {
/**
* Create an [SManga] instance from the JSON DTO element.
*/
fun MangaDataDto.createManga(): SManga =
SManga.create().also {
it.initialized = true
it.url = id.toString()
it.description = description
it.author = artist.let { it.roman_name ?: it.name }
it.status = status_name?.let(::convertStatus) ?: SManga.UNKNOWN
it.genre = category_name
it.title = name
it.thumbnail_url = image_url
}
private fun convertStatus(status: String): Int {
return when (status) {
"Ongoing" -> SManga.ONGOING
"Preview" -> SManga.ONGOING
"Finished" -> SManga.COMPLETED
"On hold" -> SManga.ON_HIATUS
"Cancelled" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
}
}

View File

@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import eu.kanade.tachiyomi.extension.all.globalcomix.releasePage
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Suppress("PropertyName")
@Serializable
@SerialName(releasePage)
class PageDataDto(
val is_page_paid: Boolean,
val desktop_image_url: String,
val mobile_image_url: String,
) : EntityDto()

View File

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.extension.all.globalcomix.dto
import kotlinx.serialization.Serializable
@Serializable
class PaginatedResponseDto<T : EntityDto>(
val payload: PaginatedPayloadDto<T>? = null,
)
@Serializable
class PaginatedPayloadDto<T : EntityDto>(
val results: List<T> = emptyList(),
val pagination: PaginationStateDto,
)
@Serializable
class ResponseDto<T : EntityDto>(
val payload: PayloadDto<T>? = null,
)
@Serializable
class PayloadDto<T : EntityDto>(
val results: T,
)
@Suppress("PropertyName")
@Serializable
class PaginationStateDto(
val page: Int = 1,
val per_page: Int = 0,
val total_pages: Int = 0,
val total_results: Int = 0,
) {
val hasNextPage: Boolean
get() = page < total_pages
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Hitomi'
extClass = '.HitomiFactory'
extVersionCode = 38
extVersionCode = 39
isNsfw = true
}

View File

@ -20,7 +20,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
@ -52,7 +52,7 @@ class Hitomi(
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::updateImageUrlInterceptor)
.addInterceptor(::imageUrlInterceptor)
.apply {
interceptors().add(0, ::streamResetRetry)
}
@ -491,18 +491,20 @@ class Hitomi(
}.awaitAll().filterNotNull()
}
private suspend fun Gallery.toSManga() = SManga.create().apply {
private fun Gallery.toSManga() = SManga.create().apply {
title = this@toSManga.title
url = galleryurl
author = groups?.joinToString { it.formatted } ?: artists?.joinToString { it.formatted }
artist = artists?.joinToString { it.formatted }
genre = tags?.joinToString { it.formatted }
thumbnail_url = files.first().let {
val hash = it.hash
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId)
"https://${subDomain}tn.$cdnDomain/avifbigtn/${thumbPathFromHash(hash)}/$hash.avif"
HttpUrl.Builder().apply {
scheme("https")
host(IMAGE_LOOPBACK_HOST)
addQueryParameter(IMAGE_THUMBNAIL, "true")
addQueryParameter(IMAGE_GIF, it.isGif.toString())
fragment(it.hash)
}.toString()
}
description = buildString {
japaneseTitle?.let {
@ -571,11 +573,13 @@ class Hitomi(
.substringBefore(".")
return gallery.files.mapIndexed { idx, img ->
// actual logic in updateImageUrlInterceptor
val imageUrl = "http://127.0.0.1".toHttpUrl().newBuilder()
.fragment(img.hash)
.build()
.toString()
// actual logic in imageUrlInterceptor
val imageUrl = HttpUrl.Builder().apply {
scheme("https")
host(IMAGE_LOOPBACK_HOST)
addQueryParameter(IMAGE_GIF, img.isGif.toString())
fragment(img.hash)
}.toString()
Page(
idx,
@ -677,18 +681,38 @@ class Hitomi(
}
}
private fun updateImageUrlInterceptor(chain: Interceptor.Chain): Response {
private fun imageUrlInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.host != "127.0.0.1") {
if (request.url.host != IMAGE_LOOPBACK_HOST) {
return chain.proceed(request)
}
val hash = request.url.fragment!!
val commonId = runBlocking { commonImageId() }
val imageId = imageIdFromHash(hash)
val subDomain = runBlocking { (subdomainOffset(imageId) + 1) }
val isThumbnail = request.url.queryParameter(IMAGE_THUMBNAIL) == "true"
val isGif = request.url.queryParameter(IMAGE_GIF) == "true"
val imageUrl = "https://a$subDomain.$cdnDomain/$commonId$imageId/$hash.avif"
val type = if (isGif) {
"webp"
} else {
"avif"
}
val imageId = imageIdFromHash(hash)
val subDomainOffset = runBlocking { subdomainOffset(imageId) }
val imageUrl = if (isThumbnail) {
val subDomain = "${'a' + subDomainOffset}tn"
"https://$subDomain.$cdnDomain/${type}bigtn/${thumbPathFromHash(hash)}/$hash.$type"
} else {
val commonId = runBlocking { commonImageId() }
val subDomain = if (isGif) {
"w${subDomainOffset + 1}"
} else {
"a${subDomainOffset + 1}"
}
"https://$subDomain.$cdnDomain/$commonId$imageId/$hash.$type"
}
val newRequest = request.newBuilder()
.url(imageUrl)
@ -705,3 +729,7 @@ class Hitomi(
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
}
const val IMAGE_LOOPBACK_HOST = "127.0.0.1"
const val IMAGE_THUMBNAIL = "is_thumbnail"
const val IMAGE_GIF = "is_gif"

View File

@ -22,7 +22,10 @@ class Gallery(
@Serializable
class ImageFile(
val hash: String,
)
private val name: String,
) {
val isGif get() = name.endsWith(".gif")
}
@Serializable
class Tag(

View File

@ -3,7 +3,7 @@ ext {
extClass = '.KdtScans'
themePkg = 'madara'
baseUrl = 'https://kdtscans.com'
overrideVersionCode = 0
overrideVersionCode = 2
isNsfw = true
}

View File

@ -4,14 +4,11 @@ import eu.kanade.tachiyomi.multisrc.madara.Madara
import eu.kanade.tachiyomi.source.model.SManga
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class KdtScans : Madara(
"KDT Scans",
"https://kdtscans.com",
"all",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("es")),
) {
override val useNewChapterEndpoint = true
override val fetchGenres = false
@ -22,6 +19,8 @@ class KdtScans : Madara(
}
}
override fun searchMangaSelector() = "div.c-tabs-item__content:not(:contains([LN]))"
override fun searchMangaFromElement(element: Element): SManga {
return super.searchMangaFromElement(element).apply {
title = title.cleanupTitle()
@ -37,5 +36,7 @@ class KdtScans : Madara(
private fun String.cleanupTitle() = replace(titleCleanupRegex, "").trim()
private val titleCleanupRegex =
Regex("""^\[(ESPAÑOL|English)\]\s+(\s+)?""", RegexOption.IGNORE_CASE)
Regex("""^\[(ESPAÑOL|English|HD|VIP)\]\s+(\s+)?""", RegexOption.IGNORE_CASE)
override fun chapterListSelector() = "li.wp-manga-chapter:not(:has(.required-login))"
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Kiutaku'
extClass = '.Kiutaku'
extVersionCode = 2
extVersionCode = 3
isNsfw = true
}

View File

@ -78,7 +78,12 @@ class Kiutaku : ParsedHttpSource() {
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/?search=$query&start=${getPage(page)}", headers)
val url = baseUrl.toHttpUrl().newBuilder()
.addQueryParameter("search", query)
.addQueryParameter("start", getPage(page).toString())
.build()
return GET(url, headers)
}
override fun searchMangaSelector() = popularMangaSelector()

View File

@ -1,7 +1,7 @@
ext {
extName = 'SchaleNetwork'
extClass = '.KoharuFactory'
extVersionCode = 13
extVersionCode = 14
isNsfw = true
}

View File

@ -12,6 +12,8 @@ import eu.kanade.tachiyomi.extension.all.koharu.Koharu.Companion.authorization
import eu.kanade.tachiyomi.extension.all.koharu.Koharu.Companion.token
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.OkHttpClient
@ -120,7 +122,9 @@ class TurnstileInterceptor(
try {
val noRedirectClient = client.newBuilder().followRedirects(false).build()
val authHeaders = authHeaders(authHeader)
val response = noRedirectClient.newCall(POST(authUrl, authHeaders)).execute()
val response = runBlocking(Dispatchers.IO) {
noRedirectClient.newCall(POST(authUrl, authHeaders)).execute()
}
response.use {
if (response.isSuccessful) {
with(response) {
@ -176,7 +180,9 @@ class TurnstileInterceptor(
try {
val noRedirectClient = client.newBuilder().followRedirects(false).build()
val authHeaders = authHeaders("Bearer $token")
val response = noRedirectClient.newCall(GET(authUrl, authHeaders)).execute()
val response = runBlocking(Dispatchers.IO) {
noRedirectClient.newCall(GET(authUrl, authHeaders)).execute()
}
response.use {
if (response.isSuccessful) {
return true

View File

@ -1,7 +1,7 @@
ext {
extName = 'Komga'
extClass = '.KomgaFactory'
extVersionCode = 61
extVersionCode = 63
}
apply from: "$rootDir/common.gradle"

View File

@ -134,7 +134,12 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
else -> "series"
}
val url = "$baseUrl/api/v1/$type?search=$query&page=${page - 1}&deleted=false".toHttpUrl().newBuilder()
val url = "$baseUrl/api/v1".toHttpUrl().newBuilder()
.addPathSegments(type)
.addQueryParameter("search", query)
.addQueryParameter("page", (page - 1).toString())
.addQueryParameter("deleted", "false")
val filterList = filters.ifEmpty { getFilterList() }
val defaultLibraries = defaultLibraries
@ -183,7 +188,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
override fun getMangaUrl(manga: SManga) = manga.url.replace("/api/v1", "")
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url)
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url, headers)
override fun mangaDetailsParse(response: Response): SManga {
return if (response.isFromReadList()) {
@ -254,7 +259,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
.sortedByDescending { it.chapter_number }
}
override fun pageListRequest(chapter: SChapter) = GET("${chapter.url}/pages")
override fun pageListRequest(chapter: SChapter) = GET("${chapter.url}/pages", headers)
override fun pageListParse(response: Response): List<Page> {
val pages = response.parseAs<List<PageDto>>()
@ -467,17 +472,17 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
scope.launch {
try {
libraries = client.newCall(GET("$baseUrl/api/v1/libraries")).await().parseAs()
libraries = client.newCall(GET("$baseUrl/api/v1/libraries", headers)).await().parseAs()
collections = client
.newCall(GET("$baseUrl/api/v1/collections?unpaged=true"))
.newCall(GET("$baseUrl/api/v1/collections?unpaged=true", headers))
.await()
.parseAs<PageWrapperDto<CollectionDto>>()
.content
genres = client.newCall(GET("$baseUrl/api/v1/genres")).await().parseAs()
tags = client.newCall(GET("$baseUrl/api/v1/tags")).await().parseAs()
publishers = client.newCall(GET("$baseUrl/api/v1/publishers")).await().parseAs()
genres = client.newCall(GET("$baseUrl/api/v1/genres", headers)).await().parseAs()
tags = client.newCall(GET("$baseUrl/api/v1/tags", headers)).await().parseAs()
publishers = client.newCall(GET("$baseUrl/api/v1/publishers", headers)).await().parseAs()
authors = client
.newCall(GET("$baseUrl/api/v1/authors"))
.newCall(GET("$baseUrl/api/v1/authors", headers))
.await()
.parseAs<List<AuthorDto>>()
.groupBy { it.role }

View File

@ -38,7 +38,7 @@ class SeriesDto(
metadata.status == "HIATUS" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
genre = (metadata.genres + metadata.tags + booksMetadata.tags).distinct().joinToString(", ")
genre = (metadata.genres + metadata.tags + booksMetadata.tags).sorted().distinct().joinToString(", ")
description = metadata.summary.ifBlank { booksMetadata.summary }
booksMetadata.authors.groupBy({ it.role }, { it.name }).let { map ->
author = map["writer"]?.distinct()?.joinToString()

View File

@ -1,7 +1,7 @@
ext {
extName = 'MangaFire'
extClass = '.MangaFireFactory'
extVersionCode = 11
extVersionCode = 12
isNsfw = true
}

View File

@ -45,6 +45,9 @@ class MangaFire(
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request {

View File

@ -13,7 +13,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="mangahosted.org"
android:host="mangago.fit"
android:pathPattern="/.*/..*" />
</intent-filter>

View File

@ -1,7 +1,7 @@
ext {
extName = 'Manga Hosted'
extClass = '.MangaHostedFactory'
extVersionCode = 2
extVersionCode = 3
isNsfw = true
}

View File

@ -26,7 +26,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
override val name: String = "Manga Hosted${langOption.nameSuffix}"
override val baseUrl: String = "https://mangahosted.org"
override val baseUrl: String = "https://mangago.fit/${langOption.infix}"
override val supportsLatest = true
@ -80,7 +80,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(SEARCH_PREFIX)) {
val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
val url = "$baseUrl/${query.substringAfter(SEARCH_PREFIX)}"
return client.newCall(GET(url, headers))
.asObservableSuccess().map { response ->
val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() }
@ -184,7 +184,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
title = dto.title
thumbnail_url = dto.thumbnailUrl
status = dto.status
url = "/${langOption.infix}/${dto.slug}"
url = "/${dto.slug}"
genre = dto.genres
initialized = true
}
@ -195,7 +195,7 @@ class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
companion object {
const val SEARCH_PREFIX = "slug:"
val baseApiUrl = "https://api.novelfull.us"
val baseApiUrl = "https://api.mangago.fit"
val apiUrl = "$baseApiUrl/api"
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
}

View File

@ -23,7 +23,7 @@ val languages = listOf(
LanguageOption("id", "manga-indo", "id"),
LanguageOption("it", "manga-italia", "manga-it"),
LanguageOption("ja", "mangaraw", "raw"),
LanguageOption("pt-BR", "manga-br", orderBy = "ASC"),
LanguageOption("pt-BR", "manga-br"),
LanguageOption("ru", "manga-ru", "mangaru"),
LanguageOption("ru", "manga-ru-hentai", "hentai", " +18"),
LanguageOption("ru", "manga-ru-yaoi", "yaoi", " +18 Yaoi"),

View File

@ -1,7 +1,7 @@
ext {
extName = 'MangaPark'
extClass = '.MangaParkFactory'
extVersionCode = 21
extVersionCode = 22
isNsfw = true
}

View File

@ -17,20 +17,19 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstanceOrNull
import keiyoushi.utils.getPreferences
import keiyoushi.utils.parseAs
import keiyoushi.utils.toJsonString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
@ -54,13 +53,15 @@ class MangaPark(
private val apiUrl = "$baseUrl/apo/"
private val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::siteSettingsInterceptor)
.addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
.rateLimitHost(apiUrl.toHttpUrl(), 1)
.build()
override val client = network.cloudflareClient.newBuilder().apply {
if (preference.getBoolean(ENABLE_NSFW, true)) {
addInterceptor(::siteSettingsInterceptor)
addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
}
rateLimitHost(apiUrl.toHttpUrl(), 1)
// intentionally after rate limit interceptor so thumbnails are not rate limited
addInterceptor(::thumbnailDomainInterceptor)
}.build()
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
@ -96,8 +97,10 @@ class MangaPark(
override fun searchMangaParse(response: Response): MangasPage {
val result = response.parseAs<SearchResponse>()
val pageAsCover = preference.getString(UNCENSORED_COVER_PREF, "off")!!
val shortenTitle = preference.getBoolean(SHORTEN_TITLE_PREF, false)
val entries = result.data.searchComics.items.map { it.data.toSManga() }
val entries = result.data.searchComics.items.map { it.data.toSManga(shortenTitle, pageAsCover) }
val hasNextPage = entries.size == size
return MangasPage(entries, hasNextPage)
@ -164,8 +167,10 @@ class MangaPark(
override fun mangaDetailsParse(response: Response): SManga {
val result = response.parseAs<DetailsResponse>()
val pageAsCover = preference.getString(UNCENSORED_COVER_PREF, "off")!!
val shortenTitle = preference.getBoolean(SHORTEN_TITLE_PREF, false)
return result.data.comic.data.toSManga()
return result.data.comic.data.toSManga(shortenTitle, pageAsCover)
}
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.substringBeforeLast("#")
@ -220,7 +225,7 @@ class MangaPark(
summary = "%s"
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, "Restart Tachiyomi to apply changes", Toast.LENGTH_LONG).show()
Toast.makeText(screen.context, "Restart the app to apply changes", Toast.LENGTH_LONG).show()
true
}
}.also(screen::addPreference)
@ -231,16 +236,34 @@ class MangaPark(
summary = "Refresh chapter list to apply changes"
setDefaultValue(false)
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = ENABLE_NSFW
title = "Enable NSFW content"
summary = "Clear Cookies & Restart the app to apply changes."
setDefaultValue(true)
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = SHORTEN_TITLE_PREF
title = "Remove extra information from title"
summary = "Clear database to apply changes\n\n" +
"Note: doesn't not work for entries in library"
setDefaultValue(false)
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = UNCENSORED_COVER_PREF
title = "Attempt to use Uncensored Cover for Hentai"
summary = "Uses first or last chapter page as cover"
entries = arrayOf("Off", "First Chapter", "Last Chapter")
entryValues = arrayOf("off", "first", "last")
setDefaultValue("off")
}.also(screen::addPreference)
}
private inline fun <reified T> Response.parseAs(): T =
use { body.string() }.let(json::decodeFromString)
private inline fun <reified T> List<*>.firstInstanceOrNull(): T? =
filterIsInstance<T>().firstOrNull()
private inline fun <reified T : Any> T.toJsonRequestBody() =
json.encodeToString(this).toRequestBody(JSON_MEDIA_TYPE)
toJsonString().toRequestBody(JSON_MEDIA_TYPE)
private val cookiesNotSet = AtomicBoolean(true)
private val latch = CountDownLatch(1)
@ -271,6 +294,25 @@ class MangaPark(
return chain.proceed(request)
}
private fun thumbnailDomainInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url
return if (url.host == THUMBNAIL_LOOPBACK_HOST) {
val newUrl = url.newBuilder()
.host(domain)
.build()
val newRequest = request.newBuilder()
.url(newUrl)
.build()
chain.proceed(newRequest)
} else {
chain.proceed(request)
}
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
@ -298,6 +340,11 @@ class MangaPark(
"mpark.to",
)
private const val ENABLE_NSFW = "pref_nsfw"
private const val DUPLICATE_CHAPTER_PREF_KEY = "pref_dup_chapters"
private const val SHORTEN_TITLE_PREF = "pref_shorten_title"
private const val UNCENSORED_COVER_PREF = "pref_uncensored_cover"
}
}
const val THUMBNAIL_LOOPBACK_HOST = "127.0.0.1"

View File

@ -38,33 +38,68 @@ class MangaParkComic(
private val originalStatus: String? = null,
private val uploadStatus: String? = null,
private val summary: String? = null,
private val extraInfo: String? = null,
@SerialName("urlCoverOri") private val cover: String? = null,
private val urlPath: String,
@SerialName("max_chapterNode") private val latestChapter: Data<ImageFiles>? = null,
@SerialName("first_chapterNode") private val firstChapter: Data<ImageFiles>? = null,
) {
fun toSManga() = SManga.create().apply {
fun toSManga(shortenTitle: Boolean, pageAsCover: String) = SManga.create().apply {
url = "$urlPath#$id"
title = name
thumbnail_url = cover
title = if (shortenTitle) {
var shortName = name
while (shortenTitleRegex.containsMatchIn(shortName)) {
shortName = shortName.replace(shortenTitleRegex, "").trim()
}
shortName
} else {
name
}
thumbnail_url = run {
val coverUrl = cover?.let {
when {
it.startsWith("http") -> it
it.startsWith("/") -> "https://$THUMBNAIL_LOOPBACK_HOST$it"
else -> null
}
}
if (pageAsCover != "off" && useLatestPageAsCover(genres)) {
if (pageAsCover == "first") {
firstChapter?.data?.imageFile?.urlList?.firstOrNull() ?: coverUrl
} else {
latestChapter?.data?.imageFile?.urlList?.firstOrNull() ?: coverUrl
}
} else {
coverUrl
}
}
author = authors?.joinToString()
artist = artists?.joinToString()
description = buildString {
val desc = summary?.let { Jsoup.parse(it).text() }
val names = altNames?.takeUnless { it.isEmpty() }
?.joinToString("\n") { "${it.trim()}" }
if (desc.isNullOrEmpty()) {
if (!names.isNullOrEmpty()) {
append("Alternative Names:\n", names)
}
} else {
append(desc)
if (!names.isNullOrEmpty()) {
append("\n\nAlternative Names:\n", names)
}
if (shortenTitle) {
append(name)
append("\n\n")
}
}
summary?.also {
append(Jsoup.parse(it).wholeText().trim())
append("\n\n")
}
extraInfo?.takeUnless(String::isBlank)?.also {
append("Extra Info:\n")
append(Jsoup.parse(it).wholeText().trim())
append("\n\n")
}
altNames?.takeUnless(List<String>::isEmpty)
?.joinToString(
prefix = "Alternative Names:\n",
separator = "\n",
) { "${it.trim()}" }
?.also(::append)
}.trim()
genre = genres?.joinToString { it.replace("_", " ").toCamelCase() }
status = when (originalStatus) {
status = when (originalStatus ?: uploadStatus) {
"ongoing" -> SManga.ONGOING
"completed" -> {
if (uploadStatus == "ongoing") {
@ -96,6 +131,14 @@ class MangaParkComic(
}
return result.toString()
}
private fun useLatestPageAsCover(genres: List<String>?): Boolean {
return genres.orEmpty().let {
it.contains("hentai") && !it.contains("webtoon")
}
}
private val shortenTitleRegex = Regex("""^(\[[^]]+\])|^(\([^)]+\))|^(\{[^}]+\})|(\[[^]]+\])${'$'}|(\([^)]+\))${'$'}|(\{[^}]+\})${'$'}""")
}
}

View File

@ -25,8 +25,23 @@ val SEARCH_QUERY = buildQuery {
originalStatus
uploadStatus
summary
extraInfo
urlCoverOri
urlPath
max_chapterNode {
data {
imageFile {
urlList
}
}
}
first_chapterNode {
data {
imageFile {
urlList
}
}
}
}
}
}
@ -52,8 +67,23 @@ val DETAILS_QUERY = buildQuery {
originalStatus
uploadStatus
summary
extraInfo
urlCoverOri
urlPath
max_chapterNode {
data {
imageFile {
urlList
}
}
}
first_chapterNode {
data {
imageFile {
urlList
}
}
}
}
}
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.Manhwa18CcFactory'
themePkg = 'madara'
baseUrl = 'https://manhwa18.cc'
overrideVersionCode = 5
overrideVersionCode = 6
isNsfw = true
}

View File

@ -5,9 +5,8 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
@ -28,7 +27,7 @@ class Manhwa18CcEN : Manhwa18Cc("Manhwa18.cc", "https://manhwa18.cc", "en") {
class Manhwa18CcKO : Manhwa18Cc("Manhwa18.cc", "https://manhwa18.cc", "ko") {
override fun popularMangaSelector() = "div.manga-item:has(h3 a[title$='Raw'])"
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/raw/$page")
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/raw/$page", headers)
}
abstract class Manhwa18Cc(
@ -45,9 +44,9 @@ abstract class Manhwa18Cc(
override fun popularMangaNextPageSelector() = "ul.pagination li.next a"
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/webtoons/$page?orderby=trending")
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/webtoons/$page?orderby=trending", headers)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/webtoons/$page")
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/webtoons/$page", headers)
override fun searchMangaSelector() = popularMangaSelector()
@ -60,7 +59,12 @@ abstract class Manhwa18Cc(
// "No results found" message. So this fix redirect to popular page.
if (query.isBlank()) return popularMangaRequest(page)
return GET("$baseUrl/search?q=$query&page=$page")
val url = "$baseUrl/search".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("page", page.toString())
.build()
return GET(url, headers)
}
override val mangaSubString = "webtoon"
@ -72,16 +76,4 @@ abstract class Manhwa18Cc(
override fun chapterDateSelector() = "span.chapter-time"
override val pageListParseSelector = "div.read-content img"
override fun pageListParse(document: Document): List<Page> {
return document.select(pageListParseSelector).mapIndexed { index, element ->
Page(
index,
document.location(),
element?.let {
it.absUrl(if (it.hasAttr("data-src")) "data-src" else "src")
},
)
}
}
}

View File

@ -0,0 +1,8 @@
ext {
extName = 'MissKon'
extClass = '.MissKon'
extVersionCode = 2
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,126 @@
package eu.kanade.tachiyomi.extension.all.misskon
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.firstInstance
import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class MissKon() : SimpleParsedHttpSource() {
override val baseUrl = "https://misskon.com"
override val lang = "all"
override val name = "MissKon"
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 10, 1, TimeUnit.SECONDS)
.build()
override fun simpleMangaSelector() = "article.item-list"
override fun simpleMangaFromElement(element: Element): SManga {
val titleEL = element.selectFirst(".post-box-title")!!
return SManga.create().apply {
title = titleEL.text()
thumbnail_url = element.selectFirst(".post-thumbnail img")?.absUrl("data-src")
setUrlWithoutDomain(titleEL.selectFirst("a")!!.absUrl("href"))
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
}
}
override fun simpleNextPageSelector(): String? = null
// region popular
override fun popularMangaRequest(page: Int) = GET("$baseUrl/top3/", headers)
// endregion
// region latest
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page", headers)
override fun latestUpdatesNextPageSelector() = ".current + a.page"
// endregion
// region Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filter = filters.firstInstance<SourceCategorySelector>()
return filter.selectedCategory?.let {
GET("$baseUrl${it.url}", headers)
} ?: run {
"$baseUrl/page/$page/".toHttpUrl().newBuilder()
.addEncodedQueryParameter("s", query)
.build()
.let { GET(it, headers) }
}
}
override fun searchMangaNextPageSelector() = "div.content > div.pagination > span.current + a"
// endregion
// region Details
override fun mangaDetailsParse(document: Document): SManga {
val postInnerEl = document.selectFirst("article > .post-inner")!!
return SManga.create().apply {
title = postInnerEl.select(".post-title").text()
genre = postInnerEl.select(".post-tag > a").joinToString { it.text() }
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
}
}
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
override fun chapterListParse(response: Response): List<SChapter> {
val doc = response.asJsoup()
val dateUploadStr = doc.selectFirst(".entry img")?.absUrl("data-src")
?.let { url ->
FULL_DATE_REGEX.find(url)?.groupValues?.get(1)
?: YEAR_MONTH_REGEX.find(url)?.groupValues?.get(1)?.let { "$it/01" }
}
val dateUpload = FULL_DATE_FORMAT.tryParse(dateUploadStr)
val maxPage = doc.select("div.page-link:first-of-type a.post-page-numbers").last()?.text()?.toInt() ?: 1
val basePageUrl = response.request.url.toString()
return (maxPage downTo 1).map { page ->
SChapter.create().apply {
setUrlWithoutDomain("$basePageUrl/$page")
name = "Page $page"
date_upload = dateUpload
}
}
}
// endregion
// region Pages
override fun pageListParse(document: Document): List<Page> {
return document.select("div.post-inner > div.entry > p > img")
.mapIndexed { i, imgEl -> Page(i, imageUrl = imgEl.absUrl("data-src")) }
}
// endregion
override fun getFilterList(): FilterList = FilterList(
Filter.Header("NOTE: Unable to further search in the category!"),
Filter.Separator(),
SourceCategorySelector.create(),
)
companion object {
private val FULL_DATE_REGEX = Regex("""/(\d{4}/\d{2}/\d{2})/""")
private val YEAR_MONTH_REGEX = Regex("""/(\d{4}/\d{2})/""")
private val FULL_DATE_FORMAT = SimpleDateFormat("yyyy/MM/dd", Locale.US)
}
}

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