Compare commits

...

224 Commits

Author SHA1 Message Date
Yush0DAN c2b107a8bd
MangaSwat: Update domain (#3975)
CI / Prepare job (push) Successful in 6s Details
CI / Build individual modules (push) Failing after 4m40s Details
CI / Publish repo (push) Has been skipped Details
* Update domain

* add configurable url

* Update src/ar/mangaswat/src/eu/kanade/tachiyomi/extension/ar/mangaswat/MangaSwat.kt

* Update MangaSwat.kt

* Update MangaSwat.kt

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:40:58 +01:00
ringosham 7614a8d083
Remove Manga Diyari (#3983) 2024-07-14 14:40:58 +01:00
AwkwardPeak7 d11495d8a1
Mangapark: duplicate chapters & unblock site blocked genres (#3979)
* fetch duplicate chapters

+ small refactor
  => data class -> class

* site settings to not block "Hentai" genre

* user name as scanlator for first choice

* forgor

* try not to make duplicate calls

concurrency my beloved

* move fetch genre call to `getFilterList`
2024-07-14 14:40:58 +01:00
Vetle Ledaal 33afc95944
Add Pijamalı Koi (#3973)
* Add Pijamalı Koi

* Remove extra path segment unconditionally

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:40:58 +01:00
Vetle Ledaal 3862f49ad7
Add Nyrax Manga (#3971)
* Add Nyrax Manga

* Set page url for Referer, actually set isNsfw

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:40:58 +01:00
AwkwardPeak7 39475d8916
Add Taadd (#3943)
* Add Taadd

* cleanup chapter names

* remove unused

* webview urls

* improve chapter title cleanup
2024-07-14 14:40:58 +01:00
CriosChan cb4c648b19
AnimeSama: Fix Website changes (#3939)
* AnimeSama: Full Rewrite

* AnimeSama: Fix "Special" chapters

* AnimeSama: Changes asked by AwkwardPeak7

* AnimeSama: Maybe better containsMultipleTimes function

* AnimeSama: Changes from AwkwardPeak7
2024-07-14 14:40:58 +01:00
Evrey cb1d65d02c
add a Dark Science extension (#3435)
* add a Dark Science extension

* add a work-around to not have the app think there’s missing chapters

* applied some changes suggested in PR #3435

* rework chapter fetching to not block, fix URL invariant for SManga

* cleanup

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:40:58 +01:00
Vetle Ledaal 13474b911f
Add King of Scans (#3974) 2024-07-14 14:40:58 +01:00
Vetle Ledaal 8c19cc40ca
Add ToomTam-Manga (#3970) 2024-07-14 14:40:58 +01:00
Vetle Ledaal 9fb6efe03d
Add Manhuascan.us (#3965) 2024-07-14 14:40:58 +01:00
kana-shii f99c22a81d
Missing icons for some pt-br extensions (#3964)
icons
2024-07-14 14:40:58 +01:00
kana-shii b19127100b
algodao doce url (#3963) 2024-07-14 14:40:58 +01:00
Vetle Ledaal e411215caf
Add Futari (#3959) 2024-07-14 14:40:58 +01:00
Vetle Ledaal 979b248658
Add KomikGO (#3958) 2024-07-14 14:40:58 +01:00
Vetle Ledaal 9d336d1645
Add Komikindo (#3956) 2024-07-14 14:40:58 +01:00
Vetle Ledaal d6420c1545
Add Doujinku (#3954) 2024-07-14 14:40:58 +01:00
Vetle Ledaal 0a11da38bf
Add Doujin69 (#3953) 2024-07-14 14:40:58 +01:00
Yush0DAN 1e44e00a0b
MHScans: Update domain (#3947)
Update domain
2024-07-14 14:40:58 +01:00
Vetle Ledaal 7871c9a134
Add Fecomic (#3573)
* Add Fecomic

* clarify how the 301 redirect is skipped in chapterFromElement

* also https-ify thumbnail URL

* trigger CI
2024-07-14 14:40:58 +01:00
Yush0DAN 37a6515ba7
Fix SamuraiScan: Update domain (#3937)
* Update domain

* Revert "Update domain"

* Update domain
2024-07-14 14:40:58 +01:00
inipew c77fc83f7d
Shinigami: Update domain (#3933) 2024-07-14 14:40:58 +01:00
Vetle Ledaal 9bfc114777
Add Asura Scans TR (#3909) 2024-07-14 14:40:58 +01:00
Yush0DAN 7ae20d9e2e
Reset Scans: Update domain (#3923)
Update domain
2024-07-14 14:40:58 +01:00
KenjieDec ef68b19ebe
MangaHen Fix Pages (#3919)
Fix Pages
2024-07-14 14:40:58 +01:00
Vetle Ledaal 556ddb2d80
Add KumoTran (#3907) 2024-07-14 14:40:58 +01:00
Yush0DAN 1d09677e3b
Teamlanhlung: Update domain (#3906)
Update domain
2024-07-14 14:40:58 +01:00
ringosham 2075d64909
[skip ci] Update debugger instructions (#3884)
* Update Android debugger instructions

* Bold text

* Wording changes

* Wording change
2024-07-14 14:40:58 +01:00
Vetle Ledaal 28ccd54565
Add Manga168 (#3892)
* Add Manga168

* Simplify color genre

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:40:58 +01:00
Vetle Ledaal 121f0591db
Add MangaBuff (#3891)
* Add MangaBuff

* style

* Update src/ru/mangabuff/src/eu/kanade/tachiyomi/extension/ru/mangabuff/MangaBuff.kt

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

* PR comments

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

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:40:58 +01:00
AwkwardPeak7 5b920b207a
Webnovel: update upload time url (#3880) 2024-07-14 14:40:58 +01:00
Vetle Ledaal 74c35b8734
Add Siimanga (#3904) 2024-07-14 14:40:56 +01:00
Vetle Ledaal 35d16330b1
Add Dark-Scan.com (#3903) 2024-07-14 14:40:56 +01:00
Vetle Ledaal 027c22b5de
Ver Manhwas: update domain (#3902) 2024-07-14 14:40:56 +01:00
Vetle Ledaal fc20cb2228
RawXZ: update domain (#3901) 2024-07-14 14:40:56 +01:00
Vetle Ledaal 0eb1915ee0
Remove Ghost Fansub (#3900) 2024-07-14 14:40:56 +01:00
Vetle Ledaal c71cb97a49
ApollComics: update domain (#3899) 2024-07-14 14:40:56 +01:00
Yush0DAN 50e0f8eb8c
Blazescans: Update domain (#3897)
* Update domain

* remove character (")
2024-07-14 14:40:56 +01:00
Vetle Ledaal 5a87d9f16e
Komik Cast: update domain (#3894) 2024-07-14 14:40:56 +01:00
Vetle Ledaal 75fe6a5d71
SummerToon: update domain (#3893) 2024-07-14 14:40:56 +01:00
Chopper a916c6a4a1
GenzToons(Surya Scans): Update domain, icons and extension name (#3883)
* Update domain, icons and extension name

* Cleanup
2024-07-14 14:40:56 +01:00
Vetle Ledaal 8dc7c90005
Add Hentaing (#3885)
* Add Hentaing

* remove page if imageUrl is null also
2024-07-14 14:40:56 +01:00
KenjieDec 102b91f959
Add ColoredManga (#3843)
* Add ColoredManga

- Weird site, very hard to fetch

* Remove unused dependecy

* Apply suggestions, Better chapter name

- Apply AwkwardPeak's suggestions
- Remove genre list

* Appy suggestions, thumbnail fix, chapter name fix

- Apply AwkwardPeak's suggestions
- Thumbnail url might change ( Example: Kuroko's Basketball )
- Chapter name removing all zeros before number fix ( Example: Chapter 001 -> Chapter 1, but Chapter 106 -> Chapter 16 )

* Update build.gradle

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

* Increase versionId

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:40:56 +01:00
ringosham 5cebd2a63a
Rewritten Nicovideo Seiga for new domain and API (#3841)
* Rewritten Nicomanga for new domain

* Fix typos to use API URL instead

* Fixed missing WebView and many parsing issues

* Preserve newlines when displaying description

* Bump version ID

* Wrapped all requests to DTOs

* Minor refactor

* Applying all requested changes

* Fixed displaying of error message

* Applying requested changes

* Remove "data"

* I forgor

* Remove redundant code and add headers
2024-07-14 14:40:56 +01:00
Secozzi 1518e5b867
readcomiconline: fix page parsing (#3876)
fix rguard script
2024-07-14 14:40:56 +01:00
Chopper 6f6de5c4b3
UmiManga: Update icons (#3871)
* Update icon

* Bump version
2024-07-14 14:40:56 +01:00
bapeey ab6f97849e
SenshiManga: Update api url (#3870)
* update api url

* Fix search
2024-07-14 14:40:56 +01:00
bapeey 6f9ef9cfdf
ArthurScan: Fix images content type (#3867)
bruh moment
2024-07-14 14:40:56 +01:00
Chopper 0f2a2f619a
Galaxy: Update domain (AR) (#3866)
Update domain
2024-07-14 14:40:56 +01:00
bapeey 0a894b790c
IkigaiMangas: Update domain (#3865)
Update domain
2024-07-14 14:40:56 +01:00
Chopper 0018f3d8d7
Remove UltraLightScans (#3863) 2024-07-14 14:40:56 +01:00
Chopper 96b4c07988
KomikCast: Update domain (#3862)
* Update domain

* Disable filters in key queries
2024-07-14 14:40:56 +01:00
Chopper 3c60b13b82
ManhwaLandMom: Update domain and icons (#3860)
Update domain and icons
2024-07-14 14:40:56 +01:00
Chopper 6047e02345
Siimanga: Migrate theme (#3857)
* Migrate theme

* Update icon

* Bump version

* Enable project filter

* Remove id
2024-07-14 14:40:56 +01:00
AwkwardPeak7 7d607486c4
TCB scans: fix page list and refactor (#3793)
* TCB scans: fix page list and refactor

* rename state pref

* fix .5 chapter number

* remove redundant + in regex
2024-07-14 14:40:56 +01:00
KirinRaikage 99539e261e
Epsilon Scans: Update domains (#3855) 2024-07-14 14:40:55 +01:00
KenjieDec 89387d0f08
Add MangaHen (#3818)
* Add MangaHen

* Fix Title and Author

* space

* Fixed Chapter Images Quality, Apply Suggestions

- Use real images instead of thumbnail/preview images
- Apply AwkwardPeak's suggestions

* Wrong Location1

* Wrong Location2

* Wrong Location3

* Apply suggestion

- Apply AwkwardPeak's suggestion

* curly bracket

* space
2024-07-14 14:40:55 +01:00
Yush0DAN 077c62543e
Plot Twist No Fansub: Update domain and fix chapter list (#3848)
Update domain
2024-07-14 14:40:55 +01:00
Yush0DAN 7445751170
Luminous Scan: Update domain (#3847)
* Update domain

* minor change
2024-07-14 14:40:55 +01:00
Yush0DAN 63f4b9af12
SenpaiEdiciones: Update domain (#3842)
Update domain
2024-07-14 14:40:55 +01:00
KenjieDec 2dbf798f0c
Add Panda Chaika Extension (#3801)
* Add Panda Chaika Extension

- Add "Panda Chaika" extension from panda.chaika.moe

* Add Support for Zip64

- Add support for Zip64 type of .zip ( large zip [ size/pages ]  ) -> For Example: https://panda.chaika.moe/archive/49406/
- Use Little Endian for All signatures
- Apply AwkwardPeak7's suggestions

* Fix null Genres?

* Fix "null" genre if there's no genre

* Fix mistakes caused by previous commit

Sorry...

* Improve description readability

- Sorry for the commits spam
- Make manga description more readable

* Fix Broken Filters, Apply Suggestions

* Apply suggestions

- Apply AwkwardPeak's suggestions
2024-07-14 14:40:55 +01:00
bapeey 4d764ef0a9
SamuraiScan; update domain (#3839)
update domain
2024-07-14 14:40:55 +01:00
bapeey e17e45ca77
MangaForFree.com: Fix images not loading (#3837)
fix
2024-07-14 14:40:55 +01:00
Vetle Ledaal 6986a260a8
Add MangaPeak (#3834) 2024-07-14 14:40:55 +01:00
Chopper 689e4a1ba1
Lib: Cleanup - Fix comment indentation (#3827)
Cleanup
2024-07-14 14:40:55 +01:00
Chopper 5b0a105213
Hentaiteca: Add random UA (#3819)
* Add random UA

* Fix WebView UA

* Add useLoadMoreRequest

* Fix date format

* Remove hardcode UA
2024-07-14 14:40:55 +01:00
AwkwardPeak7 b5b65b7be4
FlixScans: rewrite for new site (#3808)
* FlixScans: rewrite for new site

* remove log

* filters

* remove commented

* dates

* rebrand, remove multisrc
2024-07-14 14:40:55 +01:00
Vetle Ledaal 5942e0944e
TonizuToon: update domain (#3812) 2024-07-14 14:40:55 +01:00
KenjieDec ad666e8aa7
Add 3Hentai (#3822)
- Almost the same as nhentai, worse tags search
2024-07-14 14:40:55 +01:00
Chopper 96cde60458
Remove YomuMangas (#3803) 2024-07-14 14:40:55 +01:00
Chopper c26616dfc5
Remove FleksyScans (#3802) 2024-07-14 14:40:55 +01:00
Vetle Ledaal 26f1973c8a
Komiktap: update domain (#3800) 2024-07-14 14:40:55 +01:00
bapeey 12285c5235
Fix Brakeout (#3796)
* two themes at the same time 💀

* bump
2024-07-14 14:40:55 +01:00
Vetle Ledaal 6fb12f2659
ScamberTraslator: update domain (#3795) 2024-07-14 14:40:55 +01:00
Vetle Ledaal fe13a3ac13
Manwa: update domain (#3785) 2024-07-14 14:40:55 +01:00
Chopper cdddaead3e
Add Epikman (#3790) 2024-07-14 14:40:55 +01:00
Vetle Ledaal d9d939ede3
Manga Demon: update domain (#3784) 2024-07-14 14:40:55 +01:00
Vetle Ledaal fe23485b11
Nivera Fansub: update domain (#3783) 2024-07-14 14:40:55 +01:00
Vetle Ledaal 30d9450b38
Manhua Espanol: update domain (#3782) 2024-07-14 14:40:55 +01:00
Luqman af9252e798
MG Komik: fix cf block, pagination (#3777) 2024-07-14 14:40:55 +01:00
Smol Ame 72905b0433
Remove "Manga Keyfi" (#3772) 2024-07-14 14:40:55 +01:00
Smol Ame 8447cc8635
Remove "Merlin Shoujo" (#3771) 2024-07-14 14:40:55 +01:00
KenjieDec 780089af90
Pururin: Add Tags Filter & Sort Filter (#3769)
* Add Tags Filter

- Similar to Pururin's Advanced Search 
- Some description changes
- Change page filter format ( now more similar to nhentai's )

* Fix Japanese language id
2024-07-14 14:40:55 +01:00
Smol Ame 82b7531d00
Fix Adu Manga URL & date format (#3763)
* Fixed baseURL mismatch

* Replaced `Locale` in `dateFormat` to `en`

* Removed unnecessary `dateFormat` override
2024-07-14 14:40:55 +01:00
bapeey 56773e3686
Traducciones Moonlight: Fix pages not found (#3766)
another one
2024-07-14 14:40:55 +01:00
bapeey dcd3bc015d
LectorTMO: Fix only 1 page showing occasionally (#3761)
* Test

* Lint

* what

* Fixes
2024-07-14 14:40:55 +01:00
KenjieDec 592645fa9d
Update Spyfakku (#3712)
* Delete Dto

* Update SpyFakku

* Remove some non-null assert,

* Wrap date parsing around try catch

* test

* Use API

* Remove unused property

* Update

* Update SpyFakku.kt
2024-07-14 14:40:55 +01:00
bapeey 73984b1dcf
Add TuMangas.net (#3748)
* Add TuMangas.net

* use build

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

* Apply suggestions from code review

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

* to lazy to wake up

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:40:55 +01:00
Yush0DAN 445ce09211
Yugen Mangás: Update domain and fix pagelist (#3736)
* Update domain and fix pagelist

* minor change

* minor change
2024-07-14 14:40:55 +01:00
Chopper c8825e0bc4
UnionMangas: Fix page loading (#3745)
Fix page loading
2024-07-14 14:40:55 +01:00
inipew 4b996a2976
Shinigami: update domain (#3740) 2024-07-14 14:40:55 +01:00
Vetle Ledaal 56adddffa3
Add Ayatoon (#3739) 2024-07-14 14:40:55 +01:00
Chopper b6b0803bb8
Manhastro: Increase the timeout (#3732)
* Increase the timeout

* Bump version

* Update mangaSubString, add useLoadMoreRequest and change rateLimit

* Cleanup
2024-07-14 14:40:55 +01:00
bapeey be0f6c90fd
BlackoutComics: Fix selectors (#3729)
fix
2024-07-14 14:40:55 +01:00
Vetle Ledaal 947c34f4c8
Add Manga Oku TR (#3723)
* Add Manga Oku TR

* fix PR comment
2024-07-14 14:40:55 +01:00
Vetle Ledaal bd622ff86a
Add Gölge Bahçesi (#3722)
* Add Gölge Bahçesi

* fix PR comment
2024-07-14 14:40:55 +01:00
Yush0DAN 5a91a014ca
MANGARAW+: update domain (#3727)
update domain
2024-07-14 14:40:55 +01:00
Luqman dc3f37a284
Remove: Shea Manga (#3725)
has been dead for quite long time
2024-07-14 14:40:55 +01:00
Vetle Ledaal 0765cd7f3d
Add Comic 21 (#3721)
* Add Comic 21

* actually set `isNsfw`
2024-07-14 14:40:55 +01:00
Vetle Ledaal a2b8ecbc12
Add Tanuki-Manga (#3720) 2024-07-14 14:40:55 +01:00
Vetle Ledaal b11603f86b
Add Doujins.lat (#3715) 2024-07-14 14:40:55 +01:00
Vetle Ledaal e5f63b46c7
Add Etheral Radiance (#3714) 2024-07-14 14:40:55 +01:00
AlphaBoom b6cba48ba7
Jinman: Fix failed to load image (#3706) 2024-07-14 14:40:49 +01:00
Vetle Ledaal bfbdf30ab9
Add Toon FR (#3705) 2024-07-14 14:40:49 +01:00
AwkwardPeak7 4aa62d9b06
add AgiToon (#3701)
* add AgiToon

* some changes
2024-07-14 14:40:49 +01:00
Luqman 16d776056b
Pojok Manga: update domain, tweak rate limit (#3700) 2024-07-14 14:40:49 +01:00
Chopper 14eefec146
HiperDex: Update domain (#3688)
* Update domain

* Refactoring of preference URL settings

* Add useLoadMoreRequest
2024-07-14 14:40:49 +01:00
Yush0DAN d55da9562b
ManhwaWeb: Filter out chapters without url (#3694)
fix chapter null
2024-07-14 14:40:49 +01:00
inipew 24b07673f9
Komikcast: update domain (#3693) 2024-07-14 14:40:49 +01:00
Chopper 8f7c89db70
Add DeccalScans (#3690)
* Add DeccalScans

* Cleanup
2024-07-14 14:40:49 +01:00
Chopper d075391602
LuraToon: Add login warning (#3689)
* Add login warning

* Remove interceptor and disable latest tab

* Fix lint

* Fix
2024-07-14 14:40:49 +01:00
Chopper d6512ee2f5
Add AncientComics (#3687) 2024-07-14 14:40:49 +01:00
Chopper 15f0c1ff34
Remove RogMangas (#3684) 2024-07-14 14:40:49 +01:00
Chopper 51e5bd5027
Remove MangasOnline (#3683) 2024-07-14 14:40:48 +01:00
Vetle Ledaal 10afe22672
Add Asura Scans.gg (unoriginal) (#3678) 2024-07-14 14:40:48 +01:00
bapeey 0088545740
SlimeRead: Fix downloads (#3676)
whats happening with br sources
2024-07-14 14:40:48 +01:00
KirinRaikage 5e93a65f4d
Raijin Scans: Update domain (#3673) 2024-07-14 14:40:48 +01:00
Chopper 314424aa43
SSSScanlator: Add random UA (#3669)
* Add connection timeout and random UA

* Remove jetpackCDN path

* Fix lint

* Add readTimeout

* Remove 'when' statement

* Remove 'lazy' in 'getSharedPreferences'
2024-07-14 14:40:48 +01:00
bapeey e46b669169
NoblesseTranslations: Update domain and add override preference (#3668)
* Update domain + override preference

* Remove lazy
2024-07-14 14:40:48 +01:00
AwkwardPeak7 02ddcb00e6
add Comicfuz (#3600)
* ComicFuz

* points info

* search and payed chapter indicator

* save full urls

* client side cache with search and latest

* final cleanup and icons

* image quality and isNsfw

* tag search
2024-07-14 14:40:48 +01:00
bapeey 47b60ed24d
ManhwaWeb: Fix chapters not found and webview (#3671)
* Fix ManhwaWeb

* more fix
2024-07-14 14:40:48 +01:00
bapeey 731bcf021d
DarkScans: Update domain (#3662)
* Update domain

* why AS breaks my adb
2024-07-14 14:40:48 +01:00
bapeey b6d14247af
ArgosComics: Fix downloads (#3661)
Fix
2024-07-14 14:40:48 +01:00
bapeey 96335c8575
SamuraiScan: Change mangaSubstring (#3660)
change mangasubstring
2024-07-14 14:40:48 +01:00
Chopper 590c0bb2dc
ArgosComics: Update domain (#3655)
Update domain
2024-07-14 14:40:48 +01:00
bapeey 795c6f73e2
Add LMTO Online (#3654)
Add
2024-07-14 14:40:48 +01:00
Vetle Ledaal 4428cd7351
Roumanwu: update domain (#3645) 2024-07-14 14:40:48 +01:00
Vetle Ledaal c6df567713
Sussy Scan: update domain (#3644) 2024-07-14 14:40:48 +01:00
bapeey eab9ce09d2
Add UkiyoToon (#3639) 2024-07-14 14:40:48 +01:00
bapeey d83d23685f
DemonSect: Fix downloads (#3634)
Fix downloads
2024-07-14 14:40:48 +01:00
Chopper c2797eed13
FlowerMangaDotCom: Update domain (#3628)
* Update domain

* Remove unused import
2024-07-14 14:40:48 +01:00
Chopper cd4ef71b1c
Add ManhwaList (#3627) 2024-07-14 14:40:48 +01:00
Chopper f814878dbb
Add SpiderScans (#3625) 2024-07-14 14:40:48 +01:00
bapeey ff3f613886
IkigaiMangas: Update domain (#3610)
Update domain
2024-07-14 14:40:48 +01:00
AlphaBoom 1bbb002b4f
Add MangaGun (#3593)
* Add MangaGun

* save cookie by cookiejar

* Update src/ja/mangagun/build.gradle

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:40:48 +01:00
bapeey a13690f1e5
JeazScan: Update domain (#3613)
Update domain
2024-07-14 14:40:48 +01:00
bapeey f241b03d61
TresDaosScan: Change theme (#3611)
Update domain
2024-07-14 14:40:48 +01:00
Vetle Ledaal 353c7e086f
Remove Tecno Scan (#3606) 2024-07-14 14:40:48 +01:00
Vetle Ledaal 4ed4e369a5
Add Atemporal (#3582)
* Add Atemporal

* use cookieinterceptor

* trailing newline
2024-07-14 14:40:48 +01:00
Chopper 7465cbf353
Multsrc: TerraScan - Add MangaBR (#3568)
* Add lib-multsrc and mangaBR

* Cleanup

* Remove extra newline

* Remove filter separator
2024-07-14 14:40:48 +01:00
Vetle Ledaal c7d347ed43
Reset Scans: update domain (#3586) 2024-07-14 14:39:17 +01:00
Vetle Ledaal 953704c200
HentaiNexus: set thumbnail on manga parse (#3580) 2024-07-14 14:39:17 +01:00
Vetle Ledaal 2e5e25118e
Sussy Scan: update domain (#3577) 2024-07-14 14:39:17 +01:00
Vetle Ledaal 43ef813e39
MangaGeko: update domain (#3576) 2024-07-14 14:39:17 +01:00
Vetle Ledaal dacef7b01c
Kai Scans -> Umi Scans: update domain (#3575) 2024-07-14 14:39:17 +01:00
Vetle Ledaal 9588f571d1
Meitua.top: update domain (#3574) 2024-07-14 14:39:17 +01:00
AwkwardPeak7 b6c20bf147
fix page list (#3564) 2024-07-14 14:39:17 +01:00
AwkwardPeak7 927897c78b
Hitomi: improve speed (#3560)
* Hitomi: use set to improve filtering speed

* keep random sort

* optimize cache control
2024-07-14 14:39:17 +01:00
bapeey e710fee5c5
AquaScans: Fix cloudflare on chapter open (#3554)
fix cf
2024-07-14 14:39:17 +01:00
Vetle Ledaal e0d4925ae9
Add Jellyring (#3552) 2024-07-14 14:39:17 +01:00
Vetle Ledaal 19c013a9cf
Add Argos Comics.com.br (#3551) 2024-07-14 14:39:17 +01:00
mohamedotaku 54b7de7eba
Update Url Mangapro "ar" (#3548)
* change url mangapro "ar"

* change url mangapro "ar"
2024-07-14 14:39:17 +01:00
Chopper 837d6b515f
Remove LerMangaOnline (#3547) 2024-07-14 14:39:17 +01:00
Chopper 6b09f203d7
Remove LiManga (#3546) 2024-07-14 14:39:17 +01:00
Chopper 10c539c869
Remove MangasChan (#3545) 2024-07-14 14:39:16 +01:00
KenjieDec a910431f44
Added SpyFakku (#3538)
* Added SpyFakku

* Changes

- Combined SortFilter and OrderFilter into One Filter.Sort 
- Remove useless regex
- Filter out empty tags
- Rate limit added 
- etc
2024-07-14 14:39:16 +01:00
Chopper 434a03bd78
Remove ZinMangaIO and Move theme to Individual Extension (#3520)
* Remove ZinMangaIO

* Move likemanga to individual extension

* Cleanup

* Remove breakline

* Add named parameter

* Undo removing headersBuilder
2024-07-14 14:39:16 +01:00
bapeey c33fe52809
RightDarkScan: Update domain (#3535)
Update domain
2024-07-14 14:39:16 +01:00
Chopper 0c3332edf7
Remove A3Manga(vi) (#3519) 2024-07-14 14:39:16 +01:00
Chopper c322f75562
Remove Pewpiece (#3518) 2024-07-14 14:39:16 +01:00
Chopper 770f7ba5a0
Remove BoysLove (#3517) 2024-07-14 14:39:16 +01:00
bapeey 6617b33598
MNS: Show message on error (#3516)
* message

* check on all pages
2024-07-14 14:39:16 +01:00
Chopper 29624cca71
Add AsemiFansub (#3507) 2024-07-14 14:39:16 +01:00
Chopper a75d6f4670
Add Kedito (#3506)
* Add Kedito

* Add rateLimit
2024-07-14 14:39:16 +01:00
Fermín Cirella b94598c7f6
HentaiNexus: Update decryption method (#3502)
HN: Update decryption method
2024-07-14 14:39:16 +01:00
Smol Ame 4cdce2bc1b
Magus Manga: Update domain & icons (#3491)
* MagusManga: Update domain

* MagusManga: Update icons

* MagusManga: Swap to `MangaThemesiaALt`

* MagusManga: Use LS `wafffCookieInterceptor` code for chapter page loading issue
2024-07-14 14:39:16 +01:00
Smol Ame 4c4a178b9e
WPComics: Add `monthWords` and `yearWords` (#3477)
* WPComics: Add `monthWords` and `yearWords`

* Simplified month & year
2024-07-14 14:39:16 +01:00
Eshlender 88205d95b0
[RU]LibGroup filtered excess request if branchesCount is one (#3474)
* [RU]LibGroup filtered excess request if branchesCount is only alone

* rephrasing the message

* dto realization

* no need null

* fix null
2024-07-14 14:39:16 +01:00
Chopper 09868c7506
UzayManga: Migrate to ParsedHttpSource (#3487)
* Migrate to ParsedHttpSource

* Add UnsupportedOperationException

* Cleanup

* Update icons
2024-07-14 14:39:16 +01:00
AwkwardPeak7 1f4c56a57c
LS: use regex for cookie extraction (#3441)
* LuaScan: use regex for cookie extraction

* retry anyway
2024-07-14 14:39:16 +01:00
Eshlender 681bde548b
[RU]GroupLe fix wrong detect changing url (#3492)
* [RU]GroupLe fix wrong detect changing url

* change message

* domain change alt message
2024-07-14 14:39:16 +01:00
Chopper 5e0c731898
Add MugiwarasOficial (#3489) 2024-07-14 14:39:16 +01:00
bapeey 03c18aa430
TempleScan(esp): Fix pages not found (#3484)
js redirect
2024-07-14 14:39:16 +01:00
AwkwardPeak7 df5f859c1e
akuma: fix chapter urls (#3475) 2024-07-14 14:39:16 +01:00
Smol Ame d2cb60d570
XXManhwa: Update domain (#3473) 2024-07-14 14:39:16 +01:00
Smol Ame bdcd561891
LxHentai: Update domain (#3472) 2024-07-14 14:39:16 +01:00
Chopper 7d37aeac62
Add VapoScans (#3471)
* Add VapoScans

* Fix chapterUrl

* Cleanup

* Add searchManga
2024-07-14 14:39:16 +01:00
Denis Akazuki b4b811cbc4
AllHentai: Update domain (#3464) 2024-07-14 14:39:16 +01:00
Chopper f1364e9c91
Add MangaKun (#3463) 2024-07-14 14:39:16 +01:00
Chopper 058aeaa54e
Hiperdex: Add randomUA (#3459)
Add randomUA
2024-07-14 14:39:16 +01:00
Chopper ae52c158c3
LegacyScans: Migrate to HttpSource (#3451)
* Migrate LegacyScans to HttpSource

* Fix searchMangaParse

* Remove invalid genre

* Refactoring

* Fix offsets

* Fix lint
2024-07-14 14:39:16 +01:00
nedius f98063068b
LibGroup: Fix 404 when opening chapters in webview (#3442)
* LibGroup: Fix 404 when opening chapters in webview

* bump version

* remove comment

* api ratelimit
2024-07-14 14:39:16 +01:00
AwkwardPeak7 eb6b5d39d7
update selector (#3457)
* JapScan: update selector

* hello mr japscan, hope you are having fun
2024-07-14 14:39:16 +01:00
Smol Ame 9945575661
TruyenVN: Update domain (#3456) 2024-07-14 14:39:16 +01:00
Smol Ame 13f372bd12
SayHentai: Update domain (#3455) 2024-07-14 14:39:16 +01:00
Smol Ame af196dc76d
Add Arven Scans (#3454)
* Add Arven Scans

* Swapped version code back to 0

* Swapped icons for non-outlined version
2024-07-14 14:39:16 +01:00
Vetle Ledaal 9f164f1b58
Mangalink: update domain (#3291)
* Mangalink: update domain

* force update baseUrl if changed in extension
2024-07-14 14:39:16 +01:00
KenjieDec 254087d912
Akuma Tags Fix (#3390)
* Fix

- Removed "Pages" filter — They don't work
- Added "Other Tags" filter
- Added Filter limit warning
- Fixed problem on tags with spaces ( e.g. bxg brxxsts )

* Extension version: 2 -> 3

* typo fix

* Change line endings, make prefrence default

- Line endings: CRLF -> LF
- Iconified Tag Preference -> Iconified Tag is default

* Missing space

* Fix1

* Change

* Change2

* Change3

- Removed override for fetchChapterList and provide implementation in chapterListParse
- Filter out empty tags ( For example: user can put 2 commas together )

* Change4

Moved Date Format to Class val

* try catch for date parse

* Update Akuma.kt

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:39:16 +01:00
DOTX 25c9212526
KomikCast: Fix duplicate images (#3434) 2024-07-14 14:39:16 +01:00
Fansubs.cat fc316415eb
Rename "Fansubs.cat - Hentai" as "Hentai.cat" (#3430)
Rename "Fansubs.cat - Hentai" as "Hentai.cat", update logos and use the new API endpoint
2024-07-14 14:39:16 +01:00
bapeey fd68b8e9b8
RavenManga: Fix pages not found (#3439)
js redirect
2024-07-14 14:39:16 +01:00
inipew 99ee352f49
Shinigami: update domain (#3436) 2024-07-14 14:39:16 +01:00
Vetle Ledaal b58cbfae1d
Add Aisha (#3431) 2024-07-14 14:39:16 +01:00
Chopper 163cb85833
LoadingArtist: Fix json parser (#3426)
Fix json parser
2024-07-14 14:39:16 +01:00
KenjieDec 73ff0e28e9
Coomer Fix "Low Resolution" Preference error (#3417)
* Fix

* Change version

* space

* Change1
2024-07-14 14:39:16 +01:00
Eshlender 2f234f7a93
[RU]Senkuro fix many request, low rateLimit (#3405)
* [RU]Senkuro fix many request, low rateLimit

* https://github.com/keiyoushi/extensions-source
2024-07-14 14:39:16 +01:00
sinkableShip d7c93faeb1
Add Manga Toshokan Z (#3346)
* working basic function without known problem for now

* add filter and some changes

* add logo

* Apply suggestions from code review

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

* fix bugs for manga id published by registered user and change search manga request to use api with page option instead

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:39:16 +01:00
Mylloon f29eb16762
fix: Scan VF (#3395)
* fix Scan VF

* increase version code

* override function
2024-07-14 14:39:16 +01:00
mohamedotaku a4dece2c77
update url GalaxyManga "ar" (#3419) 2024-07-14 14:39:16 +01:00
bapeey 8d5ab22c9a
HouseMangas: Rebrand to "Visor Mangas" (#3418)
Rebrand
2024-07-14 14:39:16 +01:00
Cuong M. Tran 7f87376a73
Update GalleryAdults (#3416) 2024-07-14 14:39:16 +01:00
Chopper ce5160d32a
AquaManga: Update domain (#3414)
Update domain
2024-07-14 14:39:16 +01:00
bapeey acaa33bc24
MangaFire: Fix volume entries (#3404)
Is like if someone deleted this on purpose
2024-07-14 14:39:16 +01:00
Chopper d1e9584966
Union Manga: bugfix (#3391)
* Fix popularManga and detailsManga

* Cleanup

* Fix searchManga and chapter pages

* Rename and move mangaDeatilsDto

* Fix lint

* Fix available languages

* Fix latestUpdate

* Fix russian pages

* Fix getMangaUrl

* Update icons

* Remove try/catch unneeded

* Bump versionId

* Add compatibility with previous version

* Cleanup

* Refactoring getMangaUrl
2024-07-14 14:39:16 +01:00
Smol Ame c26fee2dcd
MANGARAW+: Update domain (#3368)
* Updated domain URL, substring, and added override for thumbnails

* Added rest of `imageFromElement`

* Updated `else` with suggestion

* Use suggested `imageFromElement` lines
2024-07-14 14:39:16 +01:00
AwkwardPeak7 9b1633f28e
HentaiEra: fix 302 redirect and headers (#3403)
missed in the pr review, oops
2024-07-14 14:39:16 +01:00
Chopper 00ff44e696
Add HaremDeKira (#3402)
* Add HaremDeKira

* Remove unused import
2024-07-14 14:39:16 +01:00
Chopper 05dfff4465
Add FenixProject (#3401)
* Add FenixProject

* Add rateLimit
2024-07-14 14:39:15 +01:00
Chopper cae812f456
Add ArgosHentai (#3398)
* Add ArgosHentai

* Remove unused import
2024-07-14 14:39:15 +01:00
Chopper 8106a2bab9
ArgosComics: bugfix (#3396)
* Update mangaSubString

* Add rateLimit and refactoring latestUpdate

* Remove unused import

* Remove isNsfw
2024-07-14 14:39:15 +01:00
KenjieDec 41e43420b5
Add HentaiEra (#3382)
* Add HentaiEra

* typo fix
2024-07-14 14:39:15 +01:00
AwkwardPeak7 76fe2af9ca
Hitomi: small refactor (#3389)
* Hitomi: small refactor

* bump

* coauthor

Co-authored-by: ZIDOUZI <53157536+ZIDOUZI@users.noreply.github.com>

* fix potential oom and optimize language query

when some sort is applied, it will already fetch correct language so no need for separate query

---------

Co-authored-by: ZIDOUZI <53157536+ZIDOUZI@users.noreply.github.com>
2024-07-14 14:39:15 +01:00
Chopper 61b0ab972d
Add LadronCorps (#3367)
* Add LadronCorps

* Cleanup

* Rename source

* Add rateLimit

* Add support for deep linking

* Fix searchManga

* Add icons

* Cleanup

* Add update_strategy

* Add chapter date

* Add status

* Fix package name

* Cleanup

* Rename function

* Cleanup

* Fix days parser

* Change to kotlinx serialization

* Resolve url paths in DTO

* Remove JSONObject

* Throws error field missing
2024-07-14 14:39:15 +01:00
nedius 270e70125c
Rewrite LibGroup to use new api (#3219)
* Rewrote LibGroup to use api instead of parsing document

* apply suggestions

* quick fixes

* change preferences variable to functions

* Make getToken sync

* safe & load token

* return new token when refreshing
2024-07-14 14:39:15 +01:00
stevenyomi e65117d877
Remove unnecessary files in generated APKs (#3383)
* Removed `kotlin-tooling-metadata.json`, see https://togithub.com/Kotlin/kotlinx.coroutines/issues/3158#issuecomment-1023151105
  This file includes Gradle version code, so Gradle updates will no longer cause binary changes.

* Removed "META-INF/version-control-info.textproto" introduced in AGP 8.4.1, which causes the binaries to change on each commit.

AGP updates will still cause binaries changes because it produces a required `app-metadata.properties` under `META-INF`,
and the `META-INF/MANIFEST.MF` file in the signature includes AGP version.

It is possible to empty `app-metadata.properties`: https://stackoverflow.com/questions/77745443/how-to-stop-gradle-from-generating-app-metadata-properties-at-compile-time
2024-07-14 14:39:15 +01:00
KenjieDec 95e4d83106
Hitomi Add "Type" Filter (#3355)
* Add "Type" Filter

- Added "Type" Filter
- Popular Section: Today -> Year
- Fixed issue where manga is skipped if language is null

* Change1

* *

* Change2

- Use List instead of Set
2024-07-14 14:39:15 +01:00
bapeey 0a170de120
Noblesse Translation: update domain (#3373)
update domain
2024-07-14 14:39:15 +01:00
Smol Ame 6b77db8c62
Teamlanhlung: Update domain (#3370)
Update domain
2024-07-14 14:39:15 +01:00
bapeey 030ebd09c6
MangaFire: Fix chapter name (#3369)
fix chapter name
2024-07-14 14:39:15 +01:00
Smol Ame b64d60e7ed
Dream Scan: Fix language locale (#3365)
Fix mishap with "br" instead of "BR"
2024-07-14 14:39:15 +01:00
AwkwardPeak7 8e28453769
LuaScans: set js cookie (#3361)
* LuaScans: set js cookie

* bump
2024-07-14 14:39:15 +01:00
AwkwardPeak7 66edae8b60
LuraToon: bypass chapter link redirect (#3359)
LuraToon: fix chapter links redirect
2024-07-14 14:39:15 +01:00
ZIDOUZI ac3a77ef28
Akuma: Add Filters, Add Factory for specific language, Improve manga description and tags (#3351)
* 1. Add language for manga description
2. Add a custom preference to choice if show gender as text or icon

* fix code style

* revert extra formatting

* change preference into switch

* remove unused import, increase extVersionCode

* Add filter, split into specific languages

* Apply suggestions from code review

* Change Filter style, Add Filter, Add exclude syntax

* Change Code Style

* Remove RateLimit Preference, Remove Syntax Filter

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
2024-07-14 14:39:15 +01:00
stevenyomi c66844143c
Update AGP to 8.4.1 (#3353) 2024-07-14 14:39:15 +01:00
955 changed files with 11992 additions and 4548 deletions

View File

@ -720,6 +720,10 @@ And for a release build of Tachiyomi:
### Android Debugger
> [!IMPORTANT]
> If you didn't build the main app from source with debug enabled and are using a release/beta APK, you **need** a rooted device.
> If you are using an emulator instead, make sure you choose a profile **without** Google Play.
You can leverage the Android Debugger to step through your extension while debugging.
You *cannot* simply use Android Studio's `Debug 'module.name'` -> this will most likely result in an

View File

@ -63,6 +63,7 @@ android {
release {
signingConfig signingConfigs.release
minifyEnabled false
vcsInfo.include false
}
}
@ -74,6 +75,10 @@ android {
buildConfig true
}
packaging {
resources.excludes.add("kotlin-tooling-metadata.json")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

View File

@ -4,7 +4,7 @@ coroutines_version = "1.6.4"
serialization_version = "1.4.0"
[libraries]
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.2.1" }
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.4.1" }
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }

View File

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

View File

@ -30,11 +30,10 @@ abstract class FansubsCat(
override val name: String,
override val baseUrl: String,
override val lang: String,
val apiBaseUrl: String,
val isHentaiSite: Boolean,
) : HttpSource() {
private val apiBaseUrl = "https://api.fansubs.cat"
override val supportsLatest = true
override fun headersBuilder(): Headers.Builder = Headers.Builder()
@ -91,7 +90,7 @@ abstract class FansubsCat(
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$apiBaseUrl/manga/popular/$page?hentai=$isHentaiSite", headers)
return GET("$apiBaseUrl/manga/popular/$page", headers)
}
override fun popularMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
@ -99,7 +98,7 @@ abstract class FansubsCat(
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$apiBaseUrl/manga/recent/$page?hentai=$isHentaiSite", headers)
return GET("$apiBaseUrl/manga/recent/$page", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = parseMangaFromJson(response)
@ -110,13 +109,15 @@ abstract class FansubsCat(
val filterList = if (filters.isEmpty()) getFilterList() else filters
val mangaTypeFilter = filterList.find { it is MangaTypeFilter } as MangaTypeFilter
val stateFilter = filterList.find { it is StateFilter } as StateFilter
val demographyFilter = filterList.find { it is DemographyFilter } as DemographyFilter
val genreFilter = filterList.find { it is GenreTagFilter } as GenreTagFilter
val themeFilter = filterList.find { it is ThemeTagFilter } as ThemeTagFilter
val builder = "$apiBaseUrl/manga/search/$page?hentai=$isHentaiSite".toHttpUrl().newBuilder()
val builder = "$apiBaseUrl/manga/search/$page".toHttpUrl().newBuilder()
mangaTypeFilter.addQueryParameter(builder)
stateFilter.addQueryParameter(builder)
demographyFilter.addQueryParameter(builder)
if (!isHentaiSite) {
val demographyFilter = filterList.find { it is DemographyFilter } as DemographyFilter
demographyFilter.addQueryParameter(builder)
}
genreFilter.addQueryParameter(builder)
themeFilter.addQueryParameter(builder)
if (query.isNotBlank()) {
@ -131,7 +132,7 @@ abstract class FansubsCat(
override fun mangaDetailsRequest(manga: SManga): Request {
return GET(
"$apiBaseUrl/manga/details/${manga.url.substringAfterLast('/')}?hentai=$isHentaiSite",
"$apiBaseUrl/manga/details/${manga.url.substringAfterLast('/')}",
headers,
)
}
@ -166,7 +167,7 @@ abstract class FansubsCat(
override fun chapterListRequest(manga: SManga): Request {
return GET(
"$apiBaseUrl/manga/chapters/${manga.url.substringAfterLast('/')}?hentai=$isHentaiSite",
"$apiBaseUrl/manga/chapters/${manga.url.substringAfterLast('/')}",
headers,
)
}
@ -178,7 +179,7 @@ abstract class FansubsCat(
override fun pageListRequest(chapter: SChapter): Request {
return GET(
"$apiBaseUrl/manga/pages/${chapter.url.substringAfterLast('/')}?hentai=$isHentaiSite",
"$apiBaseUrl/manga/pages/${chapter.url.substringAfterLast('/')}",
headers,
)
}

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 6

View File

@ -1,251 +0,0 @@
package eu.kanade.tachiyomi.multisrc.flixscans
import android.util.Log
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.io.IOException
abstract class FlixScans(
override val name: String,
override val baseUrl: String,
override val lang: String,
protected val apiUrl: String = "$baseUrl/api/v1",
protected val cdnUrl: String = baseUrl.replace("://", "://media.").plus("/"),
) : HttpSource() {
override val supportsLatest = true
protected open val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int): Request {
return GET("$apiUrl/webtoon/pages/home/romance", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val result = response.parseAs<HomeDto>()
val entries = (result.hot + result.topAll + result.topMonth + result.topWeek)
.distinctBy { it.id }
.map { it.toSManga(cdnUrl) }
return MangasPage(entries, false)
}
override fun latestUpdatesRequest(page: Int): Request {
return GET("$apiUrl/search/advance?page=$page&serie_type=webtoon", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val result = response.parseAs<ApiResponse<BrowseSeries>>()
val entries = result.data.map { it.toSManga(cdnUrl) }
val hasNextPage = result.lastPage > result.currentPage
return MangasPage(entries, hasNextPage)
}
private var fetchGenreList: List<GenreHolder> = emptyList()
private var fetchGenreCallOngoing = false
private var fetchGenreFailed = false
private var fetchGenreAttempt = 0
private fun fetchGenre() {
if (fetchGenreAttempt < 3 && (fetchGenreList.isEmpty() || fetchGenreFailed) && !fetchGenreCallOngoing) {
fetchGenreCallOngoing = true
// fetch genre asynchronously as it sometimes hangs
client.newCall(fetchGenreRequest()).enqueue(fetchGenreCallback)
}
}
private val fetchGenreCallback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
fetchGenreAttempt++
fetchGenreFailed = true
fetchGenreCallOngoing = false
e.message?.let { Log.e("$name Filters", it) }
}
override fun onResponse(call: Call, response: Response) {
fetchGenreCallOngoing = false
fetchGenreAttempt++
if (!response.isSuccessful) {
fetchGenreFailed = true
response.close()
return
}
val parsed = runCatching {
response.use(::fetchGenreParse)
}
fetchGenreFailed = parsed.isFailure
fetchGenreList = parsed.getOrElse {
Log.e("$name Filters", it.stackTraceToString())
emptyList()
}
}
}
private fun fetchGenreRequest(): Request {
return GET("$apiUrl/search/genres", headers)
}
private fun fetchGenreParse(response: Response): List<GenreHolder> {
return response.parseAs<List<GenreHolder>>()
}
override fun getFilterList(): FilterList {
fetchGenre()
val filters: MutableList<Filter<*>> = mutableListOf(
Filter.Header("Ignored when using Text Search"),
MainGenreFilter(),
TypeFilter(),
StatusFilter(),
)
filters += if (fetchGenreList.isNotEmpty()) {
listOf(
GenreFilter("Genre", fetchGenreList),
)
} else {
listOf(
Filter.Separator(),
Filter.Header("Press 'reset' to attempt to show Genres"),
)
}
return FilterList(filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = "$apiUrl/search/serie".toHttpUrl().newBuilder()
.addPathSegment(query.trim())
.addQueryParameter("page", page.toString())
.build()
return GET(url, headers)
}
val advSearchUrl = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegments("search/advance")
addQueryParameter("page", page.toString())
addQueryParameter("serie_type", "webtoon")
filters.forEach { filter ->
when (filter) {
is GenreFilter -> {
filter.checked.let {
if (it.isNotEmpty()) {
addQueryParameter("genres", it.joinToString(","))
}
}
}
is MainGenreFilter -> {
if (filter.state > 0) {
addQueryParameter("main_genres", filter.selected)
}
}
is TypeFilter -> {
if (filter.state > 0) {
addQueryParameter("type", filter.selected)
}
}
is StatusFilter -> {
if (filter.state > 0) {
addQueryParameter("status", filter.selected)
}
}
else -> {}
}
}
}.build()
return GET(advSearchUrl, headers)
}
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
override fun mangaDetailsRequest(manga: SManga): Request {
val (prefix, id) = getPrefixIdFromUrl(manga.url)
return GET("$apiUrl/webtoon/series/$id/$prefix", headers)
}
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
override fun mangaDetailsParse(response: Response): SManga {
val result = response.parseAs<SeriesResponse>()
return result.serie.toSManga(cdnUrl)
}
override fun chapterListRequest(manga: SManga): Request {
val (prefix, id) = getPrefixIdFromUrl(manga.url)
return GET("$apiUrl/webtoon/chapters/$id-desc#$prefix", headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val chapters = response.parseAs<List<Chapter>>()
val prefix = response.request.url.fragment!!
return chapters.map { it.toSChapter(prefix) }
}
override fun pageListRequest(chapter: SChapter): Request {
val (prefix, id) = getPrefixIdFromUrl(chapter.url)
return GET("$apiUrl/webtoon/chapters/chapter/$id/$prefix", headers)
}
protected fun getPrefixIdFromUrl(url: String): Pair<String, String> {
return with(url.substringAfterLast("/")) {
val split = split("-")
split[0] to split[1]
}
}
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url
override fun pageListParse(response: Response): List<Page> {
val result = response.parseAs<PageListResponse>()
return result.chapter.chapterData.webtoon.mapIndexed { i, img ->
Page(i, "", cdnUrl + img)
}
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
protected inline fun <reified T> Response.parseAs(): T =
use { body.string() }.let(json::decodeFromString)
}

View File

@ -1,142 +0,0 @@
package eu.kanade.tachiyomi.multisrc.flixscans
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
data class ApiResponse<T>(
val data: List<T>,
@SerialName("current_page") val currentPage: Int,
@SerialName("last_page") val lastPage: Int,
)
@Serializable
data class HomeDto(
val hot: List<BrowseSeries>,
val topWeek: List<BrowseSeries>,
val topMonth: List<BrowseSeries>,
val topAll: List<BrowseSeries>,
)
@Serializable
data class BrowseSeries(
val id: Int,
val title: String,
val slug: String,
val prefix: Int,
val thumbnail: String?,
) {
fun toSManga(cdnUrl: String) = SManga.create().apply {
title = this@BrowseSeries.title
url = "/series/$prefix-$id-$slug"
thumbnail_url = thumbnail?.let { cdnUrl + it }
}
}
@Serializable
data class SearchInput(
val title: String,
)
@Serializable
data class GenreHolder(
val name: String,
val id: Int,
)
@Serializable
data class SeriesResponse(
val serie: Series,
)
@Serializable
data class Series(
val id: Int,
val title: String,
val slug: String,
val prefix: Int,
val thumbnail: String?,
val story: String?,
val serieType: String?,
val mainGenres: String?,
val otherNames: List<String>? = emptyList(),
val status: String?,
val type: String?,
val authors: List<GenreHolder>? = emptyList(),
val artists: List<GenreHolder>? = emptyList(),
val genres: List<GenreHolder>? = emptyList(),
) {
fun toSManga(cdnUrl: String) = SManga.create().apply {
title = this@Series.title
url = "/series/$prefix-$id-$slug"
thumbnail_url = cdnUrl + thumbnail
author = authors?.joinToString { it.name.trim() }
artist = artists?.joinToString { it.name.trim() }
genre = (otherGenres + genres?.map { it.name.trim() }.orEmpty())
.distinct().joinToString { it.trim() }
description = story?.let { Jsoup.parse(it).text() }
if (otherNames?.isNotEmpty() == true) {
if (description.isNullOrEmpty()) {
description = "Alternative Names:\n"
} else {
description += "\n\nAlternative Names:\n"
}
description += otherNames.joinToString("\n") { "${it.trim()}" }
}
status = when (this@Series.status?.trim()) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
"onhold" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
private val otherGenres = listOfNotNull(serieType, mainGenres, type)
.map { word ->
word.trim().replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
}
}
@Serializable
data class Chapter(
val id: Int,
val name: String,
val slug: String,
val createdAt: String? = null,
) {
fun toSChapter(prefix: String) = SChapter.create().apply {
url = "/read/webtoon/$prefix-$id-$slug"
name = this@Chapter.name
date_upload = runCatching { dateFormat.parse(createdAt!!)!!.time }.getOrDefault(0L)
}
companion object {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.ENGLISH)
}
}
@Serializable
data class PageListResponse(
val chapter: ChapterPages,
)
@Serializable
data class ChapterPages(
val chapterData: ChapterPageData,
)
@Serializable
data class ChapterPageData(
val webtoon: List<String>,
)

View File

@ -1,62 +0,0 @@
package eu.kanade.tachiyomi.multisrc.flixscans
import eu.kanade.tachiyomi.source.model.Filter
abstract class SelectFilter(
name: String,
private val options: List<String>,
) : Filter.Select<String>(
name,
options.toTypedArray(),
) {
val selected get() = options[state]
}
class CheckBoxFilter(
name: String,
val id: String,
) : Filter.CheckBox(name)
class GenreFilter(
name: String,
private val genres: List<GenreHolder>,
) : Filter.Group<CheckBoxFilter>(
name,
genres.map { CheckBoxFilter(it.name.trim(), it.id.toString()) },
) {
val checked get() = state.filter { it.state }.map { it.id }
}
class MainGenreFilter : SelectFilter(
"Main Genre",
listOf(
"",
"fantasy",
"romance",
"action",
"drama",
),
)
class TypeFilter : SelectFilter(
"Type",
listOf(
"",
"manhwa",
"manhua",
"manga",
"comic",
),
)
class StatusFilter : SelectFilter(
"Status",
listOf(
"",
"ongoing",
"completed",
"droped",
"onhold",
"soon",
),
)

View File

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

View File

@ -294,7 +294,7 @@ abstract class GalleryAdults(
val categoryFilters = filters.filterIsInstance<CategoryFilters>().firstOrNull()
// Only for query string or multiple tags
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
val url = "$baseUrl/search/".toHttpUrl().newBuilder().apply {
getSortOrderURIs().forEachIndexed { index, pair ->
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
}
@ -310,7 +310,7 @@ abstract class GalleryAdults(
addEncodedQueryParameter(intermediateSearchKey, buildQueryString(selectedGenres.map { it.name }, query))
addPageUri(page)
}
return GET(url.build())
return GET(url.build(), headers)
}
protected open val advancedSearchKey = "key"
@ -331,7 +331,7 @@ abstract class GalleryAdults(
// Advanced search
val advancedSearchFilters = filters.filterIsInstance<AdvancedTextFilter>()
val url = "$baseUrl/$advancedSearchUri".toHttpUrl().newBuilder().apply {
val url = "$baseUrl/$advancedSearchUri/".toHttpUrl().newBuilder().apply {
getSortOrderURIs().forEachIndexed { index, pair ->
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
}
@ -379,7 +379,7 @@ abstract class GalleryAdults(
addEncodedQueryParameter(advancedSearchKey, keys.joinToString("+"))
addPageUri(page)
}
return GET(url.build())
return GET(url.build(), headers)
}
/**

View File

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

View File

@ -53,7 +53,13 @@ abstract class GroupLe(
.contains("internal/redirect") or (response.code == 301)
)
) {
throw IOException("Ссылка на мангу была изменена. Перемигрируйте мангу на тот же (или смежный с GroupLe) источник или передобавьте из Поисковика/Каталога.")
if (originalRequest.url.toString().contains("/list?")) {
throw IOException("Смените домен: Поисковик > Расширения > $name > ⚙\uFE0F")
}
throw IOException(
"URL серии изменился. Перенесите/мигрируйте с $name " +
"на $name (или смежный с GroupLe), чтобы список глав обновился",
)
}
response
}

View File

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

View File

@ -194,12 +194,14 @@ open class Kemono(
override fun imageRequest(page: Page): Request {
val imageUrl = page.imageUrl!!
if (!preferences.getBoolean(USE_LOW_RES_IMG, false)) return GET(imageUrl, headers)
val index = imageUrl.indexOf('/', startIndex = 8) // https://
val index = imageUrl.indexOf('/', 8)
val url = buildString {
append(imageUrl, 0, index)
append("/thumbnail")
append(imageUrl, index, imageUrl.length)
append("/thumbnail/data")
append(imageUrl.substring(index))
}
return GET(url, headers)
}

View File

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

View File

@ -319,7 +319,7 @@ abstract class LectorTmo(
return GET(chapter.url, tmoHeaders)
}
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
override fun pageListParse(document: Document): List<Page> {
var doc = redirectToReadPage(document)
val currentUrl = doc.location()
@ -336,21 +336,24 @@ abstract class LectorTmo(
.build()
doc = client.newCall(GET(newUrl, redirectHeaders)).execute().asJsoup()
}
val imagesScript = doc.selectFirst("script:containsData(var dirPath):containsData(var images)")
doc.select("div.viewer-container img:not(noscript img)").forEach {
add(
Page(
size,
doc.location(),
it.let {
if (it.hasAttr("data-src")) {
it.attr("abs:data-src")
} else {
it.attr("abs:src")
}
},
),
)
imagesScript?.data()?.let {
val dirPath = DIRPATH_REGEX.find(imagesScript.data())?.groupValues?.get(1)
val images = IMAGES_REGEX.find(imagesScript.data())?.groupValues?.get(1)?.split(",")?.map { img ->
img.trim().removeSurrounding("\"")
}
if (dirPath != null && images != null) {
return images.mapIndexed { i, img ->
Page(i, doc.location(), "$dirPath$img")
}
}
}
doc.select("div.viewer-container img:not(noscript img)").let {
return it.mapIndexed { i, img ->
Page(i, doc.location(), img.imgAttr())
}
}
}
@ -420,6 +423,13 @@ abstract class LectorTmo(
return document
}
private fun Element.imgAttr(): String {
return when {
this.hasAttr("data-src") -> this.attr("abs:data-src")
else -> this.attr("abs:src")
}
}
private fun String.unescapeUrl(): String {
return if (this.startsWith("http:\\/\\/") || this.startsWith("https:\\/\\/")) {
this.replace("\\/", "/")
@ -605,6 +615,9 @@ abstract class LectorTmo(
}
companion object {
val DIRPATH_REGEX = """var\s+dirPath\s*=\s*'(.*?)'\s*;""".toRegex()
val IMAGES_REGEX = """var\s+images\s*=.*\[(.*?)\]\s*'\s*\)\s*;""".toRegex()
private const val SCANLATOR_PREF = "scanlatorPref"
private const val SCANLATOR_PREF_TITLE = "Mostrar todos los scanlator"
private const val SCANLATOR_PREF_SUMMARY = "Se mostraran capítulos repetidos pero con diferentes Scanlators"

View File

@ -15,9 +15,9 @@
<!-- LibUrlActivity sites can be added here. -->
<data
android:host="v2.slashlib.me"
android:pathPattern="/..*/v..*"
android:scheme="https" />
android:host="${SOURCEHOST}"
android:pathPattern="/ru/manga/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>

View File

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

View File

@ -0,0 +1,295 @@
package eu.kanade.tachiyomi.multisrc.libgroup
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class Data<T>(
val data: T,
)
@Serializable
class Constants(
@SerialName("ageRestriction") val ageRestrictions: List<IdLabelSiteType>,
@SerialName("format") val formats: List<IdNameSiteType>,
val genres: List<IdNameSiteType>,
val imageServers: List<ImageServer>,
@SerialName("scanlateStatus") val scanlateStatuses: List<IdLabelSiteType>,
@SerialName("status") val titleStatuses: List<IdLabelSiteType>,
val tags: List<IdNameSiteType>,
val types: List<IdLabelSiteType>,
) {
@Serializable
class IdLabelSiteType(
val id: Int,
val label: String,
@SerialName("site_ids") val siteIds: List<Int>,
)
@Serializable
class IdNameSiteType(
val id: Int,
val name: String,
@SerialName("site_ids") val siteIds: List<Int>,
)
@Serializable
class ImageServer(
val id: String,
val label: String,
val url: String,
@SerialName("site_ids") val siteIds: List<Int>,
)
fun getServer(isServers: String?, siteId: Int): ImageServer =
if (!isServers.isNullOrBlank()) {
imageServers.first { it.id == isServers && it.siteIds.contains(siteId) }
} else {
imageServers.first { it.siteIds.contains(siteId) }
}
fun getCategories(siteId: Int): List<IdLabelSiteType> = types.filter { it.siteIds.contains(siteId) }
fun getFormats(siteId: Int): List<IdNameSiteType> = formats.filter { it.siteIds.contains(siteId) }
fun getGenres(siteId: Int): List<IdNameSiteType> = genres.filter { it.siteIds.contains(siteId) }
fun getTags(siteId: Int): List<IdNameSiteType> = tags.filter { it.siteIds.contains(siteId) }
fun getScanlateStatuses(siteId: Int): List<IdLabelSiteType> = scanlateStatuses.filter { it.siteIds.contains(siteId) }
fun getTitleStatuses(siteId: Int): List<IdLabelSiteType> = titleStatuses.filter { it.siteIds.contains(siteId) }
fun getAgeRestrictions(siteId: Int): List<IdLabelSiteType> = ageRestrictions.filter { it.siteIds.contains(siteId) }
}
@Serializable
class MangasPageDto(
val data: List<MangaShort>,
val meta: MangaPageMeta,
) {
@Serializable
class MangaPageMeta(
@SerialName("has_next_page") val hasNextPage: Boolean,
)
fun mapToSManga(isEng: String): List<SManga> {
return this.data.map { it.toSManga(isEng) }
}
}
@Serializable
class MangaShort(
val name: String,
@SerialName("rus_name") val rusName: String?,
@SerialName("eng_name") val engName: String?,
@SerialName("slug_url") val slugUrl: String,
val cover: Cover,
) {
@Serializable
data class Cover(
val default: String?,
)
fun toSManga(isEng: String) = SManga.create().apply {
title = getSelectedLanguage(isEng, rusName, engName, name)
thumbnail_url = cover.default.orEmpty()
url = "/$slugUrl"
}
}
@Serializable
class Manga(
val type: LabelType,
val ageRestriction: LabelType,
val rating: Rating,
val genres: List<NameType>,
val tags: List<NameType>,
@SerialName("rus_name") val rusName: String?,
@SerialName("eng_name") val engName: String?,
val name: String,
val cover: MangaShort.Cover,
val authors: List<NameType>,
val artists: List<NameType>,
val status: LabelType,
val scanlateStatus: LabelType,
@SerialName("is_licensed") val isLicensed: Boolean,
val otherNames: List<String>,
val summary: String,
) {
@Serializable
class LabelType(
val label: String,
)
@Serializable
class NameType(
val name: String,
)
@Serializable
class Rating(
val average: Float,
val votes: Int,
)
fun toSManga(isEng: String): SManga = SManga.create().apply {
title = getSelectedLanguage(isEng, rusName, engName, name)
thumbnail_url = cover.default
author = authors.joinToString { it.name }
artist = artists.joinToString { it.name }
status = parseStatus(isLicensed, scanlateStatus.label, this@Manga.status.label)
genre = type.label.ifBlank { "Манга" } + ", " + ageRestriction.label + ", " +
genres.joinToString { it.name.trim() } + ", " + tags.joinToString { it.name.trim() }
description = getOppositeLanguage(isEng, rusName, engName) + rating.average.parseAverage() + " " + rating.average +
" (голосов: " + rating.votes + ")\n" + otherNames.joinAltNames() + summary
}
private fun Float.parseAverage(): String {
return when {
this > 9.5 -> "★★★★★"
this > 8.5 -> "★★★★✬"
this > 7.5 -> "★★★★☆"
this > 6.5 -> "★★★✬☆"
this > 5.5 -> "★★★☆☆"
this > 4.5 -> "★★✬☆☆"
this > 3.5 -> "★★☆☆☆"
this > 2.5 -> "★✬☆☆☆"
this > 1.5 -> "★☆☆☆☆"
this > 0.5 -> "✬☆☆☆☆"
else -> "☆☆☆☆☆"
}
}
private fun parseStatus(isLicensed: Boolean, statusTranslate: String, statusTitle: String): Int = when {
isLicensed -> SManga.LICENSED
statusTranslate == "Завершён" && statusTitle == "Приостановлен" || statusTranslate == "Заморожен" || statusTranslate == "Заброшен" -> SManga.ON_HIATUS
statusTranslate == "Завершён" && statusTitle == "Выпуск прекращён" -> SManga.CANCELLED
statusTranslate == "Продолжается" -> SManga.ONGOING
statusTranslate == "Выходит" -> SManga.ONGOING
statusTranslate == "Завершён" -> SManga.COMPLETED
statusTranslate == "Вышло" -> SManga.PUBLISHING_FINISHED
else -> when (statusTitle) {
"Онгоинг" -> SManga.ONGOING
"Анонс" -> SManga.ONGOING
"Завершён" -> SManga.COMPLETED
"Приостановлен" -> SManga.ON_HIATUS
"Выпуск прекращён" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
private fun List<String>.joinAltNames(): String = when {
this.isNotEmpty() -> "Альтернативные названия:\n" + this.joinToString(" / ") + "\n\n"
else -> ""
}
}
private fun getSelectedLanguage(isEng: String, rusName: String?, engName: String?, name: String): String = when {
isEng == "rus" && rusName.orEmpty().isNotEmpty() -> rusName!!
isEng == "eng" && engName.orEmpty().isNotEmpty() -> engName!!
else -> name
}
private fun getOppositeLanguage(isEng: String, rusName: String?, engName: String?): String = when {
isEng == "eng" && rusName.orEmpty().isNotEmpty() -> rusName + "\n"
isEng == "rus" && engName.orEmpty().isNotEmpty() -> engName + "\n"
else -> ""
}
@Serializable
class Chapter(
val id: Int,
@SerialName("branches_count") val branchesCount: Int,
val branches: List<Branch>,
val name: String?,
val number: String,
val volume: String,
@SerialName("item_number") val itemNumber: Float?,
) {
@Serializable
class Branch(
@SerialName("branch_id") val branchId: Int?,
@SerialName("created_at") val createdAt: String,
val teams: List<Team>,
val user: User,
) {
@Serializable
class Team(
val name: String,
)
@Serializable
class User(
val username: String,
)
}
private fun first(branchId: Int? = null): Branch? {
return runCatching { if (branchId != null) branches.first { it.branchId == branchId } else branches.first() }.getOrNull()
}
private fun getTeamName(branchId: Int? = null): String? {
return runCatching { first(branchId)!!.teams.first().name }.getOrNull()
}
private fun getUserName(branchId: Int? = null): String? {
return runCatching { first(branchId)!!.user.username }.getOrNull()
}
fun toSChapter(slugUrl: String, branchId: Int? = null, isScanUser: Boolean): SChapter = SChapter.create().apply {
val chapterName = "Том $volume. Глава $number"
name = if (this@Chapter.name.isNullOrBlank()) chapterName else "$chapterName - ${this@Chapter.name}"
val branchStr = if (branchId != null) "&branch_id=$branchId" else ""
url = "/$slugUrl/chapter?$branchStr&volume=$volume&number=$number"
scanlator = getTeamName(branchId) ?: if (isScanUser) getUserName(branchId) else null
date_upload = runCatching { LibGroup.simpleDateFormat.parse(first(branchId)!!.createdAt)!!.time }.getOrDefault(0L)
chapter_number = itemNumber ?: -1f
}
}
fun List<Chapter>.getBranchCount(): Int = this.maxOf { chapter -> chapter.branches.size }
@Serializable
class Branch(
val id: Int,
)
@Serializable
class Pages(
val pages: List<MangaPage>,
) {
@Serializable
class MangaPage(
val slug: Int,
val url: String,
)
fun toPageList(): List<Page> = pages.map { Page(it.slug, it.url) }
}
@Serializable
class AuthToken(
private val auth: Auth,
private val token: Token,
) {
@Serializable
class Auth(
val id: Int,
)
@Serializable
class Token(
val timestamp: Long,
@SerialName("expires_in") val expiresIn: Long,
@SerialName("token_type") val tokenType: String,
@SerialName("access_token") val accessToken: String,
)
fun isExpired(): Boolean {
val currentTime = System.currentTimeMillis()
val expiresIn = token.timestamp + (token.expiresIn * 1000)
return expiresIn < currentTime
}
fun getToken(): String = "${token.tokenType} ${token.accessToken}"
fun getUserId(): Int = auth.id
}

View File

@ -19,7 +19,7 @@ class LibUrlActivity : Activity() {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 0) {
val titleid = pathSegments[0]
val titleid = pathSegments[2]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${LibGroup.PREFIX_SLUG_SEARCH}$titleid")

View File

@ -615,6 +615,7 @@ abstract class Madara(
"Đã hoàn thành",
"Завершено",
"Tamamlanan",
"Complété",
)
protected val ongoingStatusList: Array<String> = arrayOf(
@ -622,7 +623,7 @@ abstract class Madara(
"Em Andamento", "En cours", "En Cours", "En cours de publication", "Ativo", "Lançando", "Đang Tiến Hành", "Devam Ediyor",
"Devam ediyor", "In Corso", "In Arrivo", "مستمرة", "مستمر", "En Curso", "En curso", "Emision",
"Curso", "En marcha", "Publicandose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo",
"Đang làm", "Em postagem", "Devam Eden", "Em progresso",
"Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso",
)
protected val hiatusStatusList: Array<String> = arrayOf(
@ -635,6 +636,7 @@ abstract class Madara(
"متوقف",
"En Pause",
"Заморожено",
"En attente",
)
protected val canceledStatusList: Array<String> = arrayOf(
@ -646,6 +648,7 @@ abstract class Madara(
"ملغي",
"Abandonné",
"Заброшено",
"Annulé",
)
override fun mangaDetailsParse(document: Document): SManga {

View File

@ -208,7 +208,7 @@ constructor(
override fun searchMangaNextPageSelector(): String? = ".pagination a[rel=next]"
protected fun parseSearchDirectory(page: Int): MangasPage {
protected open fun parseSearchDirectory(page: Int): MangasPage {
val manga = searchDirectory.subList((page - 1) * 24, min(page * 24, searchDirectory.size))
.map {
SManga.create().apply {

View File

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

View File

@ -36,12 +36,12 @@ abstract class Senkuro(
override val supportsLatest = false
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", "Tachiyomi (+https://github.com/tachiyomiorg/tachiyomi)")
.add("User-Agent", "Tachiyomi (+https://github.com/keiyoushi/extensions-source)")
.add("Content-Type", "application/json")
override val client: OkHttpClient =
network.client.newBuilder()
.rateLimit(5)
.rateLimit(3)
.build()
private inline fun <reified T : Any> T.toJsonRequestBody(): RequestBody =

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="eu.kanade.tachiyomi.multisrc.terrascan.TerraScanUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<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}"
android:pathPattern="/manga/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,245 @@
package eu.kanade.tachiyomi.multisrc.terrascan
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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 okhttp3.HttpUrl.Companion.toHttpUrl
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
abstract class TerraScan(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd-MM-yyyy", Locale("pt", "BR")),
) : ParsedHttpSource() {
override val supportsLatest: Boolean = true
override val client = network.cloudflareClient
private val noRedirectClient = network.cloudflareClient.newBuilder()
.followRedirects(false)
.build()
private val json: Json by injectLazy()
private var genresList: List<Genre> = emptyList()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga?q=p&page=$page", headers)
open val popularMangaTitleSelector: String = "p, h3"
open val popularMangaThumbnailSelector: String = "img"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst(popularMangaTitleSelector)!!.ownText()
thumbnail_url = element.selectFirst(popularMangaThumbnailSelector)?.srcAttr()
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
}
override fun popularMangaNextPageSelector() = ".pagination > .page-item:not(.disabled):last-child"
override fun popularMangaSelector(): String = ".series-paginated .grid-item-series, .series-paginated .series"
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
if (genresList.isEmpty()) {
genresList = parseGenres(document)
}
val mangas = document.select(popularMangaSelector())
.map(::popularMangaFromElement)
return MangasPage(mangas, document.selectFirst(popularMangaNextPageSelector()) != null)
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/manga?q=u&page=$page", headers)
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesSelector() = popularMangaSelector()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(URL_SEARCH_PREFIX)) {
val slug = query.substringAfter(URL_SEARCH_PREFIX)
return client.newCall(GET("$baseUrl/manga/$slug", headers))
.asObservableSuccess().map { response ->
MangasPage(listOf(mangaDetailsParse(response)), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder()
if (query.isNotBlank()) {
url.addPathSegment("search")
.addQueryParameter("q", query)
return GET(url.build(), headers)
}
url.addPathSegment("manga")
filters.forEach { filter ->
when (filter) {
is GenreFilter -> {
filter.state.forEach {
if (it.state) {
url.addQueryParameter(it.query, it.value)
}
}
}
else -> {}
}
}
url.addQueryParameter("page", "$page")
return GET(url.build(), headers)
}
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = null
override fun searchMangaSelector() = ".col-6.col-sm-3.col-md-3.col-lg-2.p-1"
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.url.pathSegments.contains("search")) {
return searchByQueryMangaParse(response)
}
return super.searchMangaParse(response)
}
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<out Any>>()
if (genresList.isNotEmpty()) {
filters += GenreFilter(
title = "Gêneros",
genres = genresList,
)
} else {
filters += Filter.Header("Aperte 'Redefinir' mostrar os gêneros disponíveis")
}
return FilterList(filters)
}
open val mangaDetailsContainerSelector: String = "main"
open val mangaDetailsTitleSelector: String = "h1"
open val mangaDetailsThumbnailSelector: String = "img"
open val mangaDetailsDescriptionSelector: String = "p"
open val mangaDetailsGenreSelector: String = ".card:has(h5:contains(Categorias)) a, .card:has(h5:contains(Categorias)) div"
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
with(document.selectFirst(mangaDetailsContainerSelector)!!) {
title = selectFirst(mangaDetailsTitleSelector)!!.text()
thumbnail_url = selectFirst(mangaDetailsThumbnailSelector)?.absUrl("href")
description = selectFirst(mangaDetailsDescriptionSelector)?.text()
genre = document.select(mangaDetailsGenreSelector)
.joinToString { it.ownText() }
}
setUrlWithoutDomain(document.location())
}
override fun chapterFromElement(element: Element) = SChapter.create().apply {
with(element.selectFirst("h5")!!) {
name = ownText()
date_upload = selectFirst("div")!!.ownText().toDate()
}
setUrlWithoutDomain(element.absUrl("href"))
}
override fun chapterListSelector() = ".col-chapter a"
override fun pageListParse(document: Document): List<Page> {
val mangaChapterUrl = document.location()
val maxPage = findPageCount(mangaChapterUrl)
return (1..maxPage).map { page -> Page(page - 1, "$mangaChapterUrl/$page") }
}
override fun imageUrlParse(document: Document) = document.selectFirst("main img")!!.srcAttr()
private fun searchByQueryMangaParse(response: Response): MangasPage {
val fragment = Jsoup.parseBodyFragment(
json.decodeFromString<String>(response.body.string()),
baseUrl,
)
return MangasPage(
mangas = fragment.select(searchMangaSelector()).map(::searchMangaFromElement),
hasNextPage = false,
)
}
private fun findPageCount(pageUrl: String): Int {
var lowerBound = 1
var upperBound = 100
while (lowerBound <= upperBound) {
val midpoint = lowerBound + (upperBound - lowerBound) / 2
val request = Request.Builder().apply {
url("$pageUrl/$midpoint")
headers(headers)
head()
}.build()
val response = try {
noRedirectClient.newCall(request).execute()
} catch (e: Exception) {
throw Exception("Failed to fetch $pageUrl")
}
if (response.code == 302) {
upperBound = midpoint - 1
} else {
lowerBound = midpoint + 1
}
}
return lowerBound
}
private fun Element.srcAttr(): String = when {
hasAttr("data-src") -> absUrl("data-src")
else -> absUrl("src")
}
private fun String.toDate() = try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
open val genreFilterSelector: String = "form div > div:has(input) div"
private fun parseGenres(document: Document): List<Genre> {
return document.select(genreFilterSelector)
.map { element ->
val input = element.selectFirst("input")!!
Genre(
name = element.selectFirst("label")!!.ownText(),
query = input.attr("name"),
value = input.attr("value"),
)
}
}
companion object {
const val URL_SEARCH_PREFIX = "slug:"
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.pt.mangaterra
package eu.kanade.tachiyomi.multisrc.terrascan
import eu.kanade.tachiyomi.source.model.Filter

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.pt.mangaterra
package eu.kanade.tachiyomi.multisrc.terrascan
import android.app.Activity
import android.content.ActivityNotFoundException
@ -7,7 +7,7 @@ import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class MangaTerraUrlActivity : Activity() {
class TerraScanUrlActivity : Activity() {
private val tag = javaClass.simpleName
@ -35,5 +35,5 @@ class MangaTerraUrlActivity : Activity() {
}
private fun slug(pathSegments: List<String>) =
"${MangaTerra.PREFIX_SEARCH}${pathSegments[pathSegments.size - 1]}"
"${TerraScan.URL_SEARCH_PREFIX}${pathSegments[pathSegments.size - 1]}"
}

View File

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

View File

@ -163,6 +163,8 @@ abstract class WPComics(
val minuteWords = listOf("minute", "phút")
val hourWords = listOf("hour", "giờ")
val dayWords = listOf("day", "ngày")
val monthWords = listOf("month", "tháng")
val yearWords = listOf("year", "năm")
val agoWords = listOf("ago", "trước")
return try {
@ -171,6 +173,8 @@ abstract class WPComics(
val calendar = Calendar.getInstance()
when {
yearWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.YEAR, -trimmedDate[0].toInt()) }
monthWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) }
dayWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
hourWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
minuteWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }

View File

@ -8,9 +8,9 @@ import androidx.preference.PreferenceScreen
import okhttp3.Headers
/**
* Helper function to return UserAgentType based on SharedPreference value
*/
/**
* Helper function to return UserAgentType based on SharedPreference value
*/
fun SharedPreferences.getPrefUAType(): UserAgentType {
return when (getString(PREF_KEY_RANDOM_UA, "off")) {
"mobile" -> UserAgentType.MOBILE

View File

@ -1,7 +1,7 @@
ext {
extName = 'Akuma'
extClass = '.Akuma'
extVersionCode = 1
extClass = '.AkumaFactory'
extVersionCode = 4
isNsfw = true
}

View File

@ -21,15 +21,20 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.io.IOException
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class Akuma : ParsedHttpSource() {
class Akuma(
override val lang: String,
private val akumaLang: String,
) : ParsedHttpSource() {
override val name = "Akuma"
override val baseUrl = "https://akuma.moe"
override val lang = "all"
override val supportsLatest = false
private var nextHash: String? = null
@ -38,6 +43,9 @@ class Akuma : ParsedHttpSource() {
private val ddosGuardIntercept = DDosGuardInterceptor(network.client)
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(ddosGuardIntercept)
.addInterceptor(::tokenInterceptor)
@ -102,12 +110,19 @@ class Akuma : ParsedHttpSource() {
.add("view", "3")
.build()
return if (page == 1) {
val url = baseUrl.toHttpUrlOrNull()!!.newBuilder()
if (page == 1) {
nextHash = null
POST(baseUrl, headers, payload)
} else {
POST("$baseUrl/?cursor=$nextHash", headers, payload)
url.addQueryParameter("cursor", nextHash)
}
if (lang != "all") {
// append like `q=language:english$`
url.addQueryParameter("q", "language:$akumaLang$")
}
return POST(url.toString(), headers, payload)
}
override fun popularMangaSelector() = ".post-loop li"
@ -116,6 +131,10 @@ class Akuma : ParsedHttpSource() {
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
if (document.text().contains("Max keywords of 3 exceeded.")) {
throw Exception("Login required for more than 3 filters")
} else if (document.text().contains("Max keywords of 8 exceeded.")) throw Exception("Only max of 8 filters are allowed")
val mangas = document.select(popularMangaSelector()).map { element ->
popularMangaFromElement(element)
}
@ -154,8 +173,39 @@ class Akuma : ParsedHttpSource() {
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val request = popularMangaRequest(page)
val finalQuery: MutableList<String> = mutableListOf(query)
if (lang != "all") {
finalQuery.add("language: $akumaLang$")
}
filters.forEach { filter ->
when (filter) {
is TextFilter -> {
if (filter.state.isNotEmpty()) {
finalQuery.addAll(
filter.state.split(",").filter { it.isNotBlank() }.map {
(if (it.trim().startsWith("-")) "-" else "") + "${filter.tag}:\"${it.trim().replace("-", "")}\""
},
)
}
}
is OptionFilter -> {
if (filter.state > 0) finalQuery.add("opt:${filter.getValue()}")
}
is CategoryFilter -> {
filter.state.forEach {
when {
it.isIncluded() -> finalQuery.add("category:\"${it.name}\"")
it.isExcluded() -> finalQuery.add("-category:\"${it.name}\"")
}
}
}
else -> {}
}
}
val url = request.url.newBuilder()
.addQueryParameter("q", query.trim())
.setQueryParameter("q", finalQuery.joinToString(" "))
.build()
return request.newBuilder()
@ -168,24 +218,62 @@ class Akuma : ParsedHttpSource() {
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.select(".entry-title").text()
thumbnail_url = document.select(".img-thumbnail").attr("abs:src")
author = document.select("li.meta-data > span.artist + span.value").text()
genre = document.select(".info-list a").joinToString { it.text() }
description = document.select(".pages span.value").text() + " Pages"
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.COMPLETED
override fun mangaDetailsParse(document: Document) = with(document) {
SManga.create().apply {
title = select(".entry-title").text()
thumbnail_url = select(".img-thumbnail").attr("abs:src")
author = select(".group~.value").eachText().joinToString()
artist = select(".artist~.value").eachText().joinToString()
val characters = select(".character~.value").eachText()
val parodies = select(".parody~.value").eachText()
val males = select(".male~.value")
.map { "${it.text()}" }
val females = select(".female~.value")
.map { "${it.text()}" }
val others = select(".other~.value")
.map { "${it.text()}" }
// show all in tags for quickly searching
genre = (males + females + others).joinToString()
description = buildString {
append(
"Full English and Japanese title: \n",
select(".entry-title").text(),
"\n",
select(".entry-title+span").text(),
"\n\n",
)
// translated should show up in the description
append("Language: ", select(".language~.value").eachText().joinToString(), "\n")
append("Pages: ", select(".pages .value").text(), "\n")
append("Upload Date: ", select(".date .value>time").text().replace(" ", ", ") + " UTC", "\n")
append("Categories: ", selectFirst(".info-list .value")?.text() ?: "Unknown", "\n\n")
// show followings for easy to reference
parodies.takeIf { it.isNotEmpty() }?.let { append("Parodies: ", parodies.joinToString(), "\n") }
characters.takeIf { it.isNotEmpty() }?.let { append("Characters: ", characters.joinToString(), "\n") }
}
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.UNKNOWN
}
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.just(
listOf(
SChapter.create().apply {
url = "${manga.url}/1"
name = "Chapter"
},
),
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return listOf(
SChapter.create().apply {
setUrlWithoutDomain("${response.request.url}/1")
name = "Chapter"
date_upload = try {
dateFormat.parse(document.select(".date .value>time").text())!!.time
} catch (_: ParseException) {
0L
}
},
)
}
@ -201,6 +289,8 @@ class Akuma : ParsedHttpSource() {
pageList.add(Page(i, "$url/$i"))
}
pageList[0].imageUrl = imageUrlParse(document)
return pageList
}
@ -208,6 +298,8 @@ class Akuma : ParsedHttpSource() {
return document.select(".entry-content img").attr("abs:src")
}
override fun getFilterList(): FilterList = getFilters()
companion object {
const val PREFIX_ID = "id:"
}

View File

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.extension.all.akuma
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class AkumaFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
Akuma("all", "all"),
Akuma("en", "english"),
Akuma("id", "indonesian"),
Akuma("jv", "javanese"),
Akuma("ca", "catalan"),
Akuma("ceb", "cebuano"),
Akuma("cs", "czech"),
Akuma("da", "danish"),
Akuma("de", "german"),
Akuma("et", "estonian"),
Akuma("es", "spanish"),
Akuma("eo", "esperanto"),
Akuma("fr", "french"),
Akuma("it", "italian"),
Akuma("hi", "hindi"),
Akuma("hu", "hungarian"),
Akuma("nl", "dutch"),
Akuma("pl", "polish"),
Akuma("pt", "portuguese"),
Akuma("vi", "vietnamese"),
Akuma("tr", "turkish"),
Akuma("ru", "russian"),
Akuma("uk", "ukrainian"),
Akuma("ar", "arabic"),
Akuma("ko", "korean"),
Akuma("zh", "chinese"),
Akuma("ja", "japanese"),
)
}

View File

@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.extension.all.akuma
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TextFilter("Female Tags", "female"),
TextFilter("Male Tags", "male"),
TextFilter("Other Tags", "other"),
CategoryFilter(),
TextFilter("Groups", "group"),
TextFilter("Artists", "artist"),
TextFilter("Parody", "parody"),
TextFilter("Characters", "character"),
Filter.Separator(),
Filter.Header("Search in favorites, read, or commented"),
OptionFilter(),
)
}
internal class TextFilter(name: String, val tag: String) : Filter.Text(name)
internal class OptionFilter(val value: List<Pair<String, String>> = options) : Filter.Select<String>("Options", options.map { it.first }.toTypedArray()) {
fun getValue() = options[state].second
}
internal open class TagTriState(name: String) : Filter.TriState(name)
internal class CategoryFilter() :
Filter.Group<Filter.TriState>("Categories", categoryList.map { TagTriState(it) })
private val categoryList = listOf(
"Doujinshi",
"Manga",
"Image Set",
"Artist CG",
"Game CG",
"Western",
"Non-H",
"Cosplay",
"Misc",
)
private val options = listOf(
"None" to "",
"Favorited only" to "favorited",
"Read only" to "read",
"Commented only" to "commented",
)

View File

@ -18,6 +18,15 @@ class AsmHentai(
lang = lang,
) {
override val supportsLatest = mangaLang.isNotBlank()
override val supportSpeechless: Boolean = true
override fun Element.mangaLang() =
select("a:has(.flag)").attr("href")
.removeSuffix("/").substringAfterLast("/")
.let {
// Include Speechless in search results
if (it == LANGUAGE_SPEECHLESS) mangaLang else it
}
override fun Element.mangaUrl() =
selectFirst(".image a")?.attr("abs:href")
@ -25,10 +34,6 @@ class AsmHentai(
override fun Element.mangaThumbnail() =
selectFirst(".image img")?.imgAttr()
override fun Element.mangaLang() =
select("a:has(.flag)").attr("href")
.removeSuffix("/").substringAfterLast("/")
override fun popularMangaSelector() = ".preview_item"
override val favoritePath = "inc/user.php?act=favs"

View File

@ -0,0 +1,7 @@
ext {
extName = 'Galaxy'
extClass = '.GalaxyFactory'
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,327 @@
package eu.kanade.tachiyomi.extension.all.galaxy
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import java.util.Calendar
abstract class Galaxy(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : HttpSource() {
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int): Request {
return if (page == 1) {
GET("$baseUrl/webtoons/romance/home", headers)
} else {
GET("$baseUrl/webtoons/action/home", headers)
}
}
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val entries = document.select(
"""div.tabs div[wire:snapshot*=App\\Models\\Serie], main div:has(h2:matches(Today\'s Hot|الرائج اليوم)) a[wire:snapshot*=App\\Models\\Serie]""",
).map { element ->
SManga.create().apply {
setUrlWithoutDomain(
if (element.tagName().equals("a")) {
element.absUrl("href")
} else {
element.selectFirst("a")!!.absUrl("href")
},
)
thumbnail_url = element.selectFirst("img")?.absUrl("src")
title = element.selectFirst("div.text-sm")!!.text()
}
}.distinctBy { it.url }
return MangasPage(entries, response.request.url.pathSegments.getOrNull(1) == "romance")
}
override fun latestUpdatesRequest(page: Int): Request {
val url = "$baseUrl/latest?serie_type=webtoon&main_genres=romance" +
if (page > 1) {
"&page=$page"
} else {
""
}
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val entries = document.select("div[wire:snapshot*=App\\\\Models\\\\Serie]").map { element ->
SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
thumbnail_url = element.selectFirst("img")?.absUrl("src")
title = element.select("div.flex a[href*=/series/]").last()!!.text()
}
}
val hasNextPage = document.selectFirst("[role=navigation] button[wire:click*=nextPage]") != null
return MangasPage(entries, hasNextPage)
}
private var filters: List<FilterData> = emptyList()
private val scope = CoroutineScope(Dispatchers.IO)
protected fun launchIO(block: () -> Unit) = scope.launch {
try {
block()
} catch (_: Exception) { }
}
override fun getFilterList(): FilterList {
launchIO {
if (filters.isEmpty()) {
val document = client.newCall(GET("$baseUrl/search", headers)).execute().asJsoup()
val mainGenre = FilterData(
displayName = document.select("label[for$=main_genres]").text(),
options = document.select("select[wire:model.live=main_genres] option").map {
it.text() to it.attr("value")
},
queryParameter = "main_genres",
)
val typeFilter = FilterData(
displayName = document.select("label[for$=type]").text(),
options = document.select("select[wire:model.live=type] option").map {
it.text() to it.attr("value")
},
queryParameter = "type",
)
val statusFilter = FilterData(
displayName = document.select("label[for$=status]").text(),
options = document.select("select[wire:model.live=status] option").map {
it.text() to it.attr("value")
},
queryParameter = "status",
)
val genreFilter = FilterData(
displayName = if (lang == "ar") {
"التصنيفات"
} else {
"Genre"
},
options = document.select("div[x-data*=genre] > div").map {
it.text() to it.attr("wire:key")
},
queryParameter = "genre",
)
filters = listOf(mainGenre, typeFilter, statusFilter, genreFilter)
}
}
val filters: List<Filter<*>> = filters.map {
SelectFilter(
it.displayName,
it.options,
it.queryParameter,
)
}.ifEmpty {
listOf(
Filter.Header("Press 'reset' to load filters"),
)
}
return FilterList(filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
addQueryParameter("serie_type", "webtoon")
addQueryParameter("title", query.trim())
filters.filterIsInstance<SelectFilter>().forEach {
it.addFilterParameter(this)
}
if (page > 1) {
addQueryParameter("page", page.toString())
}
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
return SManga.create().apply {
title = document.select("#full_model h3").text()
thumbnail_url = document.selectFirst("main img[src*=series/webtoon]")?.absUrl("src")
status = when (document.getQueryParam("status")) {
"ongoing", "soon" -> SManga.ONGOING
"completed", "droped" -> SManga.COMPLETED
"onhold" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
genre = buildList {
document.getQueryParam("type")
?.capitalize()?.let(::add)
document.select("#full_model a[href*=search?genre]")
.eachText().let(::addAll)
}.joinToString()
author = document.select("#full_model [wire:key^=a-]").eachText().joinToString()
artist = document.select("#full_model [wire:key^=r-]").eachText().joinToString()
description = buildString {
append(document.select("#full_model p").text().trim())
append("\n\nAlternative Names:\n")
document.select("#full_model [wire:key^=n-]")
.joinToString("\n") { "${it.text().trim().removeMdEscaped()}" }
.let(::append)
}.trim()
}
}
private fun Document.getQueryParam(queryParam: String): String? {
return selectFirst("#full_model a[href*=search?$queryParam]")
?.absUrl("href")?.toHttpUrlOrNull()?.queryParameter(queryParam)
}
private fun String.capitalize(): String {
val result = StringBuilder(length)
var capitalize = true
for (char in this) {
result.append(
if (capitalize) {
char.uppercase()
} else {
char.lowercase()
},
)
capitalize = char.isWhitespace()
}
return result.toString()
}
private val mdRegex = Regex("""&amp;#(\d+);""")
private fun String.removeMdEscaped(): String {
val char = mdRegex.find(this)?.groupValues?.get(1)?.toIntOrNull()
?: return this
return replaceFirst(mdRegex, Char(char).toString())
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select("a[href*=/read/]:not([type=button])").map { element ->
SChapter.create().apply {
setUrlWithoutDomain(element.absUrl("href"))
name = element.select("span.font-normal").text()
date_upload = element.selectFirst("div:not(:has(> svg)) > span.text-xs")
?.text().parseRelativeDate()
}
}
}
protected open fun String?.parseRelativeDate(): Long {
this ?: return 0L
val number = Regex("""(\d+)""").find(this)?.value?.toIntOrNull() ?: 0
val cal = Calendar.getInstance()
return when {
listOf("second", "ثانية").any { contains(it, true) } -> {
cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
}
contains("دقيقتين", true) -> {
cal.apply { add(Calendar.MINUTE, -2) }.timeInMillis
}
listOf("minute", "دقائق").any { contains(it, true) } -> {
cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
}
contains("ساعتان", true) -> {
cal.apply { add(Calendar.HOUR, -2) }.timeInMillis
}
listOf("hour", "ساعات").any { contains(it, true) } -> {
cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
}
contains("يوم", true) -> {
cal.apply { add(Calendar.DAY_OF_YEAR, -1) }.timeInMillis
}
contains("يومين", true) -> {
cal.apply { add(Calendar.DAY_OF_YEAR, -2) }.timeInMillis
}
listOf("day", "أيام").any { contains(it, true) } -> {
cal.apply { add(Calendar.DAY_OF_YEAR, -number) }.timeInMillis
}
contains("أسبوع", true) -> {
cal.apply { add(Calendar.WEEK_OF_YEAR, -1) }.timeInMillis
}
contains("أسبوعين", true) -> {
cal.apply { add(Calendar.WEEK_OF_YEAR, -2) }.timeInMillis
}
listOf("week", "أسابيع").any { contains(it, true) } -> {
cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis
}
contains("شهر", true) -> {
cal.apply { add(Calendar.MONTH, -1) }.timeInMillis
}
contains("شهرين", true) -> {
cal.apply { add(Calendar.MONTH, -2) }.timeInMillis
}
listOf("month", "أشهر").any { contains(it, true) } -> {
cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
}
contains("سنة", true) -> {
cal.apply { add(Calendar.YEAR, -1) }.timeInMillis
}
contains("سنتان", true) -> {
cal.apply { add(Calendar.YEAR, -2) }.timeInMillis
}
listOf("year", "سنوات").any { contains(it, true) } -> {
cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
}
else -> 0L
}
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return document.select("[wire:key^=image] img").mapIndexed { idx, img ->
Page(idx, imageUrl = img.absUrl("src"))
}
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
}

View File

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.extension.all.galaxy
import eu.kanade.tachiyomi.source.SourceFactory
class GalaxyFactory : SourceFactory {
class GalaxyWebtoon : Galaxy("Galaxy Webtoon", "https://galaxyaction.net", "en") {
override val id = 2602904659965278831
}
class GalaxyManga : Galaxy("Galaxy Manga", "https://ayoub-zrr.xyz", "ar") {
override val id = 2729515745226258240
}
override fun createSources() = listOf(
GalaxyWebtoon(),
GalaxyManga(),
)
}

View File

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.extension.all.galaxy
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
class SelectFilter(
name: String,
private val options: List<Pair<String, String>>,
private val queryParam: String,
) : Filter.Select<String>(
name,
buildList {
add("")
addAll(options.map { it.first })
}.toTypedArray(),
) {
fun addFilterParameter(url: HttpUrl.Builder) {
if (state == 0) return
url.addQueryParameter(queryParam, options[state - 1].second)
}
}
class FilterData(
val displayName: String,
val options: List<Pair<String, String>>,
val queryParameter: String,
)

View File

@ -1,8 +1,8 @@
ext {
extName = 'Ler Mangá Online'
extClass = '.LerMangaOnline'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"
ext {
extName = '3Hentai'
extClass = '.Hentai3Factory'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,207 @@
package eu.kanade.tachiyomi.extension.all.hentai3
import eu.kanade.tachiyomi.network.GET
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.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class Hentai3(
override val lang: String = "all",
private val searchLang: String = "",
) : HttpSource() {
override val name = "3Hentai"
override val baseUrl = "https://3hentai.net"
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.set("referer", "$baseUrl/")
.set("origin", baseUrl)
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/${if (searchLang.isNotEmpty()) "language/$searchLang/${if (page > 1) page else ""}?" else "search?q=pages%3A>0&pages=$page&"}sort=popular", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val doc = response.asJsoup()
val mangas = doc.select("a[href*=/d/]").map(::popularMangaFromElement)
val hasNextPage = doc.selectFirst("a[rel=next]") != null
return MangasPage(mangas, hasNextPage)
}
private fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.selectFirst("div")!!.ownText()
setUrlWithoutDomain(element.absUrl("href"))
thumbnail_url = element.selectFirst("img:not([class])")!!.absUrl("src")
}
}
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/${if (searchLang.isNotEmpty()) "language/$searchLang/$page" else "search?q=pages%3A>0&pages=$page"}", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val tags = mutableListOf<String>()
var singleTag: Pair<String, String>? = null
var sort = ""
if (searchLang.isNotEmpty()) tags.add("language:$searchLang")
filters.forEach {
when (it) {
is SelectFilter -> sort = it.getValue()
is TextFilter -> {
if (it.state.isNotEmpty()) {
val splitted = it.state.split(",").filter(String::isNotBlank)
if (splitted.size < 2 && it.type != "tags") {
singleTag = it.type to it.state.replace(" ", "-")
} else {
splitted.map { tag ->
val trimmed = tag.trim().lowercase()
tags.add(
buildString {
if (trimmed.startsWith('-')) append("-")
append(it.type, ":'")
append(trimmed.removePrefix("-"), if (it.specific.isNotEmpty()) " (${it.specific})'" else "'")
},
)
}
}
}
}
else -> {}
}
}
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (singleTag != null) {
addPathSegment(singleTag!!.first)
addPathSegment(singleTag!!.second)
if (page > 1) addPathSegment(page.toString())
} else {
addPathSegment("search")
addQueryParameter(
"q",
when {
tags.isNotEmpty() -> tags.joinToString()
query.isNotEmpty() -> query
else -> "page:>0"
},
)
if (page > 1) addQueryParameter("page", page.toString())
}
addQueryParameter("sort", sort)
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
// Details
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
fun String.capitalizeEach() = this.split(" ").joinToString(" ") { s ->
s.replaceFirstChar { sr ->
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
}
}
return SManga.create().apply {
val authors = document.select("a[href*=/groups/]").eachText().joinToString()
val artists = document.select("a[href*=/artists/]").eachText().joinToString()
initialized = true
title = document.select("h1 > span").text()
author = authors.ifEmpty { artists }
artist = artists.ifEmpty { authors }
genre = document.select("a[href*=/tags/]").eachText().joinToString {
val capitalized = it.capitalizeEach()
if (capitalized.contains("male")) {
capitalized.replace("(female)", "").replace("(male)", "")
} else {
"$capitalized"
}
}
description = buildString {
document.select("a[href*=/characters/]").eachText().joinToString().ifEmpty { null }?.let {
append("Characters: ", it.capitalizeEach(), "\n\n")
}
document.select("a[href*=/series/]").eachText().joinToString().ifEmpty { null }?.let {
append("Series: ", it.capitalizeEach(), "\n\n")
}
document.select("a[href*=/groups/]").eachText().joinToString().ifEmpty { null }?.let {
append("Groups: ", it.capitalizeEach(), "\n\n")
}
document.select("a[href*=/language/]").eachText().joinToString().ifEmpty { null }?.let {
append("Languages: ", it.capitalizeEach(), "\n\n")
}
append(document.select("div.tag-container:contains(pages:)").text(), "\n")
}
thumbnail_url = document.selectFirst("img[src*=thumbnail].w-96")?.absUrl("src")
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
}
}
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
// Chapters
override fun chapterListParse(response: Response): List<SChapter> {
val doc = response.asJsoup()
return listOf(
SChapter.create().apply {
name = "Chapter"
setUrlWithoutDomain(response.request.url.toString())
date_upload = try {
dateFormat.parse(doc.select("time").text())!!.time
} catch (_: ParseException) {
0L
}
},
)
}
// Pages
override fun pageListParse(response: Response): List<Page> {
val images = response.asJsoup().select("img:not([class], [src*=thumb], [src*=cover])")
return images.mapIndexed { index, image ->
val imageUrl = image.absUrl("src")
Page(index, imageUrl = imageUrl.replace(Regex("t(?=\\.)"), ""))
}
}
override fun getFilterList() = getFilters()
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
}

View File

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.extension.all.hentai3
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class Hentai3Factory : SourceFactory {
override fun createSources(): List<Source> = listOf(
Hentai3("all", ""),
Hentai3("en", "english"),
Hentai3("ja", "japanese"),
Hentai3("ko", "korean"),
Hentai3("zh", "chinese"),
Hentai3("mo", "mongolian"),
Hentai3("es", "spanish"),
Hentai3("pt", "Portuguese"),
Hentai3("id", "indonesian"),
Hentai3("jv", "javanese"),
Hentai3("tl", "tagalog"),
Hentai3("vi", "vietnamese"),
Hentai3("th", "thai"),
Hentai3("my", "burmese"),
Hentai3("tr", "turkish"),
Hentai3("ru", "russian"),
Hentai3("uk", "ukrainian"),
Hentai3("po", "polish"),
Hentai3("fi", "finnish"),
Hentai3("de", "german"),
Hentai3("it", "italian"),
Hentai3("fr", "french"),
Hentai3("nl", "dutch"),
Hentai3("cs", "czech"),
Hentai3("hu", "hungarian"),
Hentai3("bg", "bulgarian"),
Hentai3("is", "icelandic"),
Hentai3("la", "latin"),
Hentai3("ar", "arabic"),
)
}

View File

@ -0,0 +1,39 @@
package eu.kanade.tachiyomi.extension.all.hentai3
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
SelectFilter("Sort by", getSortsList),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
Filter.Header("Use 'Male Tags' or 'Female Tags' for specific categories. 'Tags' searches all categories."),
TextFilter("Tags", "tags"),
TextFilter("Male Tags", "tags", "male"),
TextFilter("Female Tags", "tags", "female"),
TextFilter("Series", "series"),
TextFilter("Characters", "characters"),
TextFilter("Artists", "artist"),
TextFilter("Groups", "groups"),
TextFilter("Languages", "language"),
Filter.Separator(),
Filter.Header("Filter by pages, for example: (>20)"),
TextFilter("Pages", "page"),
)
}
internal open class TextFilter(name: String, val type: String, val specific: String = "") : Filter.Text(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) {
fun getValue() = vals[state].second
}
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Recent", ""),
Pair("Popular: All Time", "popular"),
Pair("Popular: Week", "popular-7d"),
Pair("Popular: Today", "popular-24h"),
)

View File

@ -0,0 +1,10 @@
ext {
extName = 'HentaiEra'
extClass = '.HentaiEraFactory'
themePkg = 'galleryadults'
baseUrl = 'https://hentaiera.com'
overrideVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,105 @@
package eu.kanade.tachiyomi.extension.all.hentaiera
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
import eu.kanade.tachiyomi.multisrc.galleryadults.Genre
import eu.kanade.tachiyomi.multisrc.galleryadults.SearchFlagFilter
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
import eu.kanade.tachiyomi.multisrc.galleryadults.toBinary
import eu.kanade.tachiyomi.network.GET
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class HentaiEra(
lang: String = "all",
override val mangaLang: String = LANGUAGE_MULTI,
) : GalleryAdults(
"HentaiEra",
"https://hentaiera.com",
lang = lang,
) {
override val supportsLatest = true
override val useIntermediateSearch: Boolean = true
override val supportSpeechless: Boolean = true
override fun Element.mangaTitle(selector: String): String? =
mangaFullTitle(selector.replace("caption", "gallery_title")).let {
if (preferences.shortTitle) it?.shortenTitle() else it
}
override fun Element.mangaLang() =
select("a:has(.g_flag)").attr("href")
.removeSuffix("/").substringAfterLast("/")
.let {
// Include Speechless in search results
if (it == LANGUAGE_SPEECHLESS) mangaLang else it
}
override fun popularMangaRequest(page: Int): Request {
// Only for query string or multiple tags
val url = "$baseUrl/search/".toHttpUrl().newBuilder().apply {
addQueryParameter("pp", "1")
getLanguageURIs().forEach { pair ->
addQueryParameter(
pair.second,
toBinary(mangaLang == pair.first || mangaLang == LANGUAGE_MULTI),
)
}
addPageUri(page)
}
return GET(url.build(), headers)
}
/* Details */
override fun Element.getInfo(tag: String): String {
return select("li:has(.tags_text:contains($tag)) .tag .item_name")
.joinToString {
val name = it.ownText()
if (tag.contains(regexTag)) {
genres[name] = it.parent()!!.attr("href")
.removeSuffix("/").substringAfterLast('/')
}
listOf(
name,
it.select(".split_tag").text()
.trim()
.removePrefix("| "),
)
.filter { s -> s.isNotBlank() }
.joinToString()
}
}
override fun Element.getCover() =
selectFirst(".left_cover img")?.imgAttr()
override fun tagsParser(document: Document): List<Genre> {
return document.select("h2.gallery_title a")
.mapNotNull {
Genre(
it.text(),
it.attr("href")
.removeSuffix("/").substringAfterLast('/'),
)
}
}
override val mangaDetailInfoSelector = ".gallery_first"
/* Pages */
override val thumbnailSelector = ".gthumb"
override val pageUri = "view"
override fun getCategoryURIs() = listOf(
SearchFlagFilter("Manga", "mg"),
SearchFlagFilter("Doujinshi", "dj"),
SearchFlagFilter("Western", "ws"),
SearchFlagFilter("Image Set", "is"),
SearchFlagFilter("Artist CG", "ac"),
SearchFlagFilter("Game CG", "gc"),
)
}

View File

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.extension.all.hentaiera
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class HentaiEraFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
HentaiEra("en", GalleryAdults.LANGUAGE_ENGLISH),
HentaiEra("ja", GalleryAdults.LANGUAGE_JAPANESE),
HentaiEra("es", GalleryAdults.LANGUAGE_SPANISH),
HentaiEra("fr", GalleryAdults.LANGUAGE_FRENCH),
HentaiEra("ko", GalleryAdults.LANGUAGE_KOREAN),
HentaiEra("de", GalleryAdults.LANGUAGE_GERMAN),
HentaiEra("ru", GalleryAdults.LANGUAGE_RUSSIAN),
HentaiEra("all", GalleryAdults.LANGUAGE_MULTI),
)
}

View File

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

View File

@ -3,79 +3,50 @@ package eu.kanade.tachiyomi.extension.all.hitomi
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
typealias OrderType = Pair<String?, String>
typealias ParsedFilter = Pair<String, OrderType>
private fun parseFilter(query: StringBuilder, area: String, filterState: String) {
filterState
.trim()
.split(',')
.filter { it.isNotBlank() }
.forEach {
val trimmed = it.trim()
val negativePrefix = if (trimmed.startsWith("-")) "-" else ""
query.append(" $negativePrefix$area:${trimmed.removePrefix("-").replace(" ", "_")}")
}
fun getFilters(): FilterList {
return FilterList(
SelectFilter("Sort by", getSortsList),
TypeFilter("Types"),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TextFilter("Groups", "group"),
TextFilter("Artists", "artist"),
TextFilter("Series", "series"),
TextFilter("Characters", "character"),
TextFilter("Male Tags", "male"),
TextFilter("Female Tags", "female"),
Filter.Header("Please don't put Female/Male tags here, they won't work!"),
TextFilter("Tags", "tag"),
)
}
fun parseFilters(filters: FilterList): ParsedFilter {
val query = StringBuilder()
var order: OrderType = Pair("date", "added")
filters.forEach { filter ->
when (filter) {
is SortFilter -> {
order = filter.getOrder
}
is AreaFilter -> {
parseFilter(query, filter.getAreaName, filter.state)
}
else -> { /* Do Nothing */ }
}
}
return Pair(query.toString(), order)
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
internal open class SelectFilter(name: String, val vals: List<Triple<String, String?, String>>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
fun getArea() = vals[state].second
fun getValue() = vals[state].third
}
internal class TypeFilter(name: String) :
Filter.Group<CheckBoxFilter>(
name,
listOf(
Pair("Anime", "anime"),
Pair("Artist CG", "artistcg"),
Pair("Doujinshi", "doujinshi"),
Pair("Game CG", "gamecg"),
Pair("Image Set", "imageset"),
Pair("Manga", "manga"),
).map { CheckBoxFilter(it.first, it.second, true) },
)
internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state)
private class OrderFilter(val name: String, val order: OrderType) {
val getFilterName: String
get() = name
val getOrder: OrderType
get() = order
}
private class SortFilter : UriPartFilter(
"Sort By",
arrayOf(
OrderFilter("Date Added", Pair(null, "index")),
OrderFilter("Date Published", Pair("date", "published")),
OrderFilter("Popular: Today", Pair("popular", "today")),
OrderFilter("Popular: Week", Pair("popular", "week")),
OrderFilter("Popular: Month", Pair("popular", "month")),
OrderFilter("Popular: Year", Pair("popular", "year")),
),
)
private open class UriPartFilter(displayName: String, val vals: Array<OrderFilter>) :
Filter.Select<String>(displayName, vals.map { it.getFilterName }.toTypedArray()) {
val getOrder: OrderType
get() = vals[state].getOrder
}
private class AreaFilter(displayName: String, val areaName: String) :
Filter.Text(displayName) {
val getAreaName: String
get() = areaName
}
fun getFilterListInternal(): FilterList = FilterList(
SortFilter(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
AreaFilter("Artist(s)", "artist"),
AreaFilter("Character(s)", "character"),
AreaFilter("Group(s)", "group"),
AreaFilter("Series", "series"),
AreaFilter("Female Tag(s)", "female"),
AreaFilter("Male Tag(s)", "male"),
Filter.Header("Don't put Female/Male tags here, they won't work!"),
AreaFilter("Tag(s)", "tag"),
private val getSortsList: List<Triple<String, String?, String>> = listOf(
Triple("Date Added", null, "index"),
Triple("Date Published", "date", "published"),
Triple("Popular: Today", "popular", "today"),
Triple("Popular: Week", "popular", "week"),
Triple("Popular: Month", "popular", "month"),
Triple("Popular: Year", "popular", "year"),
Triple("Random", "popular", "year"),
)

View File

@ -1,12 +1,7 @@
package eu.kanade.tachiyomi.extension.all.hitomi
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
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
@ -22,16 +17,16 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.LinkedList
import java.util.Locale
@ -41,7 +36,7 @@ import kotlin.math.min
class Hitomi(
override val lang: String,
private val nozomiLang: String,
) : ConfigurableSource, HttpSource() {
) : HttpSource() {
override val name = "Hitomi"
@ -57,19 +52,13 @@ class Hitomi(
override val client = network.cloudflareClient
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private var iconified = preferences.getBoolean(PREF_TAG_GENDER_ICON, false)
override fun headersBuilder() = super.headersBuilder()
.set("referer", "$baseUrl/")
.set("origin", baseUrl)
override fun fetchPopularManga(page: Int): Observable<MangasPage> = Observable.fromCallable {
runBlocking {
val entries = getGalleryIDsFromNozomi("popular", "today", nozomiLang, page.nextPageRange())
val entries = getGalleryIDsFromNozomi("popular", "year", nozomiLang, page.nextPageRange())
.toMangaList()
MangasPage(entries, entries.size >= 24)
@ -88,26 +77,23 @@ class Hitomi(
private lateinit var searchResponse: List<Int>
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.fromCallable {
val parsedFilter = parseFilters(filters)
runBlocking {
if (page == 1) {
searchResponse = hitomiSearch(
"$query${parsedFilter.first}".trim(),
parsedFilter.second,
query.trim(),
filters,
nozomiLang,
).toList()
)
}
val end = min(page * 25, searchResponse.size)
val entries = searchResponse.subList((page - 1) * 25, end)
.toMangaList()
MangasPage(entries, end != searchResponse.size)
MangasPage(entries, end < searchResponse.size)
}
}
override fun getFilterList(): FilterList = getFilterListInternal()
override fun getFilterList() = getFilters()
private fun Int.nextPageRange(): LongRange {
val byteOffset = ((this - 1) * 25) * 4L
@ -115,27 +101,73 @@ class Hitomi(
}
private suspend fun getRangedResponse(url: String, range: LongRange?): ByteArray {
val rangeHeaders = when (range) {
null -> headers
else -> headersBuilder()
.set("Range", "bytes=${range.first}-${range.last}")
.build()
val request = when (range) {
null -> GET(url, headers)
else -> {
val rangeHeaders = headersBuilder()
.set("Range", "bytes=${range.first}-${range.last}")
.build()
GET(url, rangeHeaders, CacheControl.FORCE_NETWORK)
}
}
return client.newCall(GET(url, rangeHeaders)).awaitSuccess().use { it.body.bytes() }
return client.newCall(request).awaitSuccess().use { it.body.bytes() }
}
private suspend fun hitomiSearch(
query: String,
order: OrderType,
filters: FilterList,
language: String = "all",
): Set<Int> =
): List<Int> =
coroutineScope {
var sortBy: Pair<String?, String> = Pair(null, "index")
var random = false
val terms = query
.trim()
.replace(Regex("""^\?"""), "")
.lowercase()
.split(Regex("\\s+"))
.toMutableList()
filters.forEach {
when (it) {
is SelectFilter -> {
sortBy = Pair(it.getArea(), it.getValue())
random = (it.vals[it.state].first == "Random")
}
is TypeFilter -> {
val (activeFilter, inactiveFilters) = it.state.partition { stIt -> stIt.state }
terms += when {
inactiveFilters.size < 5 -> inactiveFilters.map { fil -> "-type:${fil.value}" }
inactiveFilters.size == 5 -> listOf("type:${activeFilter[0].value}")
else -> listOf("type: none")
}
}
is TextFilter -> {
if (it.state.isNotEmpty()) {
terms += it.state.split(",").filter(String::isNotBlank).map { tag ->
val trimmed = tag.trim()
buildString {
if (trimmed.startsWith('-')) {
append("-")
}
append(it.type)
append(":")
append(trimmed.lowercase().removePrefix("-"))
}
}
}
}
else -> {}
}
}
if (language != "all" && sortBy == Pair(null, "index") && !terms.any { it.contains(":") }) {
terms += "language:$language"
}
val positiveTerms = LinkedList<String>()
val negativeTerms = LinkedList<String>()
@ -150,22 +182,35 @@ class Hitomi(
val positiveResults = positiveTerms.map {
async {
runCatching {
getGalleryIDsForQuery(it, language, order)
}.getOrDefault(emptySet())
try {
getGalleryIDsForQuery(it, language)
} catch (e: IllegalArgumentException) {
if (e.message?.equals("HTTP error 404") == true) {
throw Exception("Unknown query: \"$it\"")
} else {
throw e
}
}
}
}
val negativeResults = negativeTerms.map {
async {
runCatching {
getGalleryIDsForQuery(it, language, order)
}.getOrDefault(emptySet())
try {
getGalleryIDsForQuery(it, language)
} catch (e: IllegalArgumentException) {
if (e.message?.equals("HTTP error 404") == true) {
throw Exception("Unknown query: \"$it\"")
} else {
throw e
}
}
}
}
val results = when {
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(order.first, order.second, language)
positiveTerms.isEmpty() || sortBy != Pair(null, "index")
-> getGalleryIDsFromNozomi(sortBy.first, sortBy.second, language)
else -> emptySet()
}.toMutableSet()
@ -190,14 +235,17 @@ class Hitomi(
filterNegative(it.await())
}
results
if (random) {
results.toList().shuffled()
} else {
results.toList()
}
}
// search.js
private suspend fun getGalleryIDsForQuery(
query: String,
language: String = "all",
order: OrderType,
): Set<Int> {
query.replace("_", " ").let {
if (it.indexOf(':') > -1) {
@ -220,20 +268,6 @@ class Hitomi(
}
}
if (area != null) {
if (order.first != null) {
area = "$area/${order.first}"
if (tag.isBlank()) {
tag = order.second
} else {
area = "$area/${order.second}"
}
}
} else {
area = order.first
tag = order.second
}
return getGalleryIDsFromNozomi(area, tag, lang)
}
@ -435,12 +469,18 @@ class Hitomi(
private suspend fun Collection<Int>.toMangaList() = coroutineScope {
map { id ->
async {
runCatching {
try {
client.newCall(GET("$ltnUrl/galleries/$id.js", headers))
.awaitSuccess()
.parseScriptAs<Gallery>()
.toSManga()
}.getOrNull()
} catch (e: IllegalArgumentException) {
if (e.message?.equals("HTTP error 404") == true) {
return@async null
} else {
throw e
}
}
}
}.awaitAll().filterNotNull()
}
@ -450,7 +490,7 @@ class Hitomi(
url = galleryurl
author = groups?.joinToString { it.formatted }
artist = artists?.joinToString { it.formatted }
genre = tags?.joinToString { it.getFormatted(iconified) }
genre = tags?.joinToString { it.formatted }
thumbnail_url = files.first().let {
val hash = it.hash
val imageId = imageIdFromHash(hash)
@ -459,14 +499,15 @@ class Hitomi(
"https://${subDomain}tn.$domain/webpbigtn/${thumbPathFromHash(hash)}/$hash.webp"
}
description = buildString {
parodys?.joinToString { it.formatted }?.let {
append("Series: ", it, "\n")
}
characters?.joinToString { it.formatted }?.let {
append("Characters: ", it, "\n")
}
parodys?.joinToString { it.formatted }?.let {
append("Parodies: ", it, "\n")
}
append("Type: ", type, "\n")
append("Pages: ", files.size, "\n")
append("Language: ", language)
language?.let { append("Language: ", language) }
}
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
@ -487,26 +528,21 @@ class Hitomi(
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
override fun chapterListRequest(manga: SManga): Request {
val id = manga.url
.substringAfterLast("-")
.substringBefore(".")
return GET("$ltnUrl/galleries/$id.js#${manga.url}", headers)
}
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
val gallery = response.parseScriptAs<Gallery>()
val mangaUrl = response.request.url.fragment!!
return listOf(
SChapter.create().apply {
name = "Chapter"
url = mangaUrl
url = gallery.galleryurl
scanlator = gallery.type
date_upload = runCatching {
date_upload = try {
dateFormat.parse(gallery.date.substringBeforeLast("-"))!!.time
}.getOrDefault(0L)
} catch (_: ParseException) {
0L
}
},
)
}
@ -525,6 +561,9 @@ class Hitomi(
override fun pageListParse(response: Response) = runBlocking {
val gallery = response.parseScriptAs<Gallery>()
val id = gallery.galleryurl
.substringAfterLast("-")
.substringBefore(".")
gallery.files.mapIndexed { idx, img ->
val hash = img.hash
@ -620,28 +659,9 @@ class Hitomi(
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = PREF_TAG_GENDER_ICON
title = "Show gender as text or icon in tags (requires refresh)"
summaryOff = "Show gender as text"
summaryOn = "Show gender as icon"
setOnPreferenceChangeListener { _, newValue ->
iconified = newValue == true
true
}
}.also(screen::addPreference)
}
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
companion object {
private const val PREF_TAG_GENDER_ICON = "pref_tag_gender_icon"
}
}

View File

@ -4,12 +4,12 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
@Serializable
data class Gallery(
class Gallery(
val galleryurl: String,
val title: String,
val date: String,
val type: String,
val language: String,
val type: String?,
val language: String?,
val tags: List<Tag>?,
val artists: List<Artist>?,
val groups: List<Group>?,
@ -19,49 +19,49 @@ data class Gallery(
)
@Serializable
data class ImageFile(
class ImageFile(
val hash: String,
)
@Serializable
data class Tag(
val female: JsonPrimitive?,
val male: JsonPrimitive?,
val tag: String,
class Tag(
private val female: JsonPrimitive?,
private val male: JsonPrimitive?,
private val tag: String,
) {
fun getFormatted(iconified: Boolean) = if (female?.content == "1") {
tag.toCamelCase() + if (iconified) "" else " (Female)"
val formatted get() = if (female?.content == "1") {
tag.toCamelCase() + ""
} else if (male?.content == "1") {
tag.toCamelCase() + if (iconified) "" else " (Male)"
tag.toCamelCase() + ""
} else {
tag.toCamelCase()
}
}
@Serializable
data class Artist(
val artist: String,
class Artist(
private val artist: String,
) {
val formatted get() = artist.toCamelCase()
}
@Serializable
data class Group(
val group: String,
class Group(
private val group: String,
) {
val formatted get() = group.toCamelCase()
}
@Serializable
data class Character(
val character: String,
class Character(
private val character: String,
) {
val formatted get() = character.toCamelCase()
}
@Serializable
data class Parody(
val parody: String,
class Parody(
private val parody: String,
) {
val formatted get() = parody.toCamelCase()
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.MangaFireFactory'
themePkg = 'mangareader'
baseUrl = 'https://mangafire.to'
overrideVersionCode = 3
overrideVersionCode = 5
isNsfw = true
}

View File

@ -138,13 +138,13 @@ open class MangaFire(
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
val document = Jsoup.parse(result)
val elements = document.select("ul li")
val selector = if (isVolume) "div.unit" else "ul li"
val elements = document.select(selector)
if (elements.size > 0) {
val linkToFirstChapter = elements[0].selectFirst(Evaluator.Tag("a"))!!.attr("href")
val mangaId = linkToFirstChapter.toString().substringAfter('.').substringBefore('/')
val request = GET("$baseUrl/ajax/read/$mangaId/chapter/$langCode", headers)
val type = if (isVolume) volumeType else chapterType
val request = GET("$baseUrl/ajax/read/$mangaId/$type/$langCode", headers)
val response = client.newCall(request).execute()
val res = json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html
val chapterInfoDocument = Jsoup.parse(res)
@ -177,6 +177,7 @@ open class MangaFire(
val element = elements[i]
val number = element.attr("data-number").toFloatOrNull() ?: -1f
if (chapter.chapter_number != number) throw Exception("Chapter number doesn't match. Try updating again.")
chapter.name = element.select(Evaluator.Tag("span"))[0].ownText()
val date = element.select(Evaluator.Tag("span"))[1].ownText()
chapter.date_upload = try {
dateFormat.parse(date)!!.time

View File

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

View File

@ -4,6 +4,7 @@ import android.app.Application
import android.widget.Toast
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.cookieinterceptor.CookieInterceptor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
@ -17,10 +18,14 @@ 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 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
@ -28,6 +33,8 @@ import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
class MangaPark(
override val lang: String,
@ -53,6 +60,7 @@ class MangaPark(
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()
@ -90,8 +98,6 @@ class MangaPark(
}
override fun searchMangaParse(response: Response): MangasPage {
runCatching(::getGenres)
val result = response.parseAs<SearchResponse>()
val entries = result.data.searchComics.items.map { it.data.toSManga() }
@ -126,6 +132,10 @@ class MangaPark(
}
override fun getFilterList(): FilterList {
CoroutineScope(Dispatchers.IO).launch {
runCatching(::getGenres)
}
val filters = mutableListOf<Filter<*>>(
SortFilter(),
OriginalLanguageFilter(),
@ -175,7 +185,13 @@ class MangaPark(
override fun chapterListParse(response: Response): List<SChapter> {
val result = response.parseAs<ChapterListResponse>()
return result.data.chapterList.map { it.data.toSChapter() }.reversed()
return if (preference.getBoolean(DUPLICATE_CHAPTER_PREF_KEY, false)) {
result.data.chapterList.flatMap {
it.data.dupChapters.map { it.data.toSChapter() }
}.reversed()
} else {
result.data.chapterList.map { it.data.toSChapter() }.reversed()
}
}
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
@ -211,6 +227,13 @@ class MangaPark(
true
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = DUPLICATE_CHAPTER_PREF_KEY
title = "Fetch Duplicate Chapters"
summary = "Refresh chapter list to apply changes"
setDefaultValue(false)
}.also(screen::addPreference)
}
private inline fun <reified T> Response.parseAs(): T =
@ -222,6 +245,35 @@ class MangaPark(
private inline fun <reified T : Any> T.toJsonRequestBody() =
json.encodeToString(this).toRequestBody(JSON_MEDIA_TYPE)
private val cookiesNotSet = AtomicBoolean(true)
private val latch = CountDownLatch(1)
// sets necessary cookies to not block genres like `Hentai`
private fun siteSettingsInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val settingsUrl = "$baseUrl/aok/settings-save"
if (
request.url.toString() != settingsUrl &&
request.url.host == domain
) {
if (cookiesNotSet.getAndSet(false)) {
val payload =
"""{"data":{"general_autoLangs":[],"general_userLangs":[],"general_excGenres":[],"general_prefLangs":[]}}"""
.toRequestBody(JSON_MEDIA_TYPE)
client.newCall(POST(settingsUrl, headers, payload)).execute().close()
latch.countDown()
} else {
latch.await()
}
}
return chain.proceed(request)
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
@ -248,5 +300,7 @@ class MangaPark(
"parkmanga.org",
"mpark.to",
)
private const val DUPLICATE_CHAPTER_PREF_KEY = "pref_dup_chapters"
}
}

View File

@ -12,34 +12,34 @@ typealias ChapterListResponse = Data<ChapterList>
typealias PageListResponse = Data<ChapterPages>
@Serializable
data class Data<T>(val data: T)
class Data<T>(val data: T)
@Serializable
data class Items<T>(val items: List<T>)
class Items<T>(val items: List<T>)
@Serializable
data class SearchComics(
class SearchComics(
@SerialName("get_searchComic") val searchComics: Items<Data<MangaParkComic>>,
)
@Serializable
data class ComicNode(
class ComicNode(
@SerialName("get_comicNode") val comic: Data<MangaParkComic>,
)
@Serializable
data class MangaParkComic(
val id: String,
val name: String,
val altNames: List<String>? = null,
val authors: List<String>? = null,
val artists: List<String>? = null,
val genres: List<String>? = null,
val originalStatus: String? = null,
val uploadStatus: String? = null,
val summary: String? = null,
@SerialName("urlCoverOri") val cover: String? = null,
val urlPath: String,
class MangaParkComic(
private val id: String,
private val name: String,
private val altNames: List<String>? = null,
private val authors: List<String>? = null,
private val artists: List<String>? = null,
private val genres: List<String>? = null,
private val originalStatus: String? = null,
private val uploadStatus: String? = null,
private val summary: String? = null,
@SerialName("urlCoverOri") private val cover: String? = null,
private val urlPath: String,
) {
fun toSManga() = SManga.create().apply {
url = "$urlPath#$id"
@ -100,18 +100,21 @@ data class MangaParkComic(
}
@Serializable
data class ChapterList(
class ChapterList(
@SerialName("get_comicChapterList") val chapterList: List<Data<MangaParkChapter>>,
)
@Serializable
data class MangaParkChapter(
val id: String,
@SerialName("dname") val displayName: String,
val title: String? = null,
val dateCreate: Long? = null,
val dateModify: Long? = null,
val urlPath: String,
class MangaParkChapter(
private val id: String,
@SerialName("dname") private val displayName: String,
private val title: String? = null,
private val dateCreate: Long? = null,
private val dateModify: Long? = null,
private val urlPath: String,
private val srcTitle: String? = null,
private val userNode: Data<Name>? = null,
val dupChapters: List<Data<MangaParkChapter>> = emptyList(),
) {
fun toSChapter() = SChapter.create().apply {
url = "$urlPath#$id"
@ -120,20 +123,24 @@ data class MangaParkChapter(
title?.let { append(": ", it) }
}
date_upload = dateModify ?: dateCreate ?: 0L
scanlator = userNode?.data?.name ?: srcTitle ?: "Unknown"
}
}
@Serializable
data class ChapterPages(
class Name(val name: String)
@Serializable
class ChapterPages(
@SerialName("get_chapterNode") val chapterPages: Data<ImageFiles>,
)
@Serializable
data class ImageFiles(
class ImageFiles(
val imageFile: UrlList,
)
@Serializable
data class UrlList(
class UrlList(
val urlList: List<String>,
)

View File

@ -4,28 +4,28 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GraphQL<T>(
val variables: T,
val query: String,
class GraphQL<T>(
private val variables: T,
private val query: String,
)
@Serializable
data class SearchVariables(val select: SearchPayload)
class SearchVariables(private val select: SearchPayload)
@Serializable
data class SearchPayload(
@SerialName("word") val query: String? = null,
val incGenres: List<String>? = null,
val excGenres: List<String>? = null,
val incTLangs: List<String>? = null,
val incOLangs: List<String>? = null,
val sortby: String? = null,
val chapCount: String? = null,
val origStatus: String? = null,
val siteStatus: String? = null,
val page: Int,
val size: Int,
class SearchPayload(
@SerialName("word") private val query: String? = null,
private val incGenres: List<String>? = null,
private val excGenres: List<String>? = null,
private val incTLangs: List<String>? = null,
private val incOLangs: List<String>? = null,
private val sortby: String? = null,
private val chapCount: String? = null,
private val origStatus: String? = null,
private val siteStatus: String? = null,
private val page: Int,
private val size: Int,
)
@Serializable
data class IdVariables(val id: String)
class IdVariables(private val id: String)

View File

@ -75,6 +75,28 @@ val CHAPTERS_QUERY = buildQuery {
dateModify
dateCreate
urlPath
srcTitle
userNode {
data {
name
}
}
dupChapters {
data {
id
dname
title
dateModify
dateCreate
urlPath
srcTitle
userNode {
data {
name
}
}
}
}
}
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Meitua.top'
extClass = '.MeituaTop'
extVersionCode = 5
extVersionCode = 6
isNsfw = true
}

View File

@ -23,7 +23,7 @@ class MeituaTop : HttpSource() {
override val lang = "all"
override val supportsLatest = false
override val baseUrl = "https://meitu1.xyz"
override val baseUrl = "https://mt1.meitu1.sbs"
override fun popularMangaRequest(page: Int) = GET("$baseUrl/arttype/0b-$page.html", headers)

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,253 @@
package eu.kanade.tachiyomi.extension.all.pandachaika
import eu.kanade.tachiyomi.network.GET
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.lang.String.CASE_INSENSITIVE_ORDER
import java.math.BigInteger
class PandaChaika(
override val lang: String = "all",
private val searchLang: String = "",
) : HttpSource() {
override val name = "PandaChaika"
override val baseUrl = "https://panda.chaika.moe"
private val baseSearchUrl = "$baseUrl/search"
override val supportsLatest = true
override val client = network.cloudflareClient
.newBuilder()
.addInterceptor(::Intercept)
.build()
private val json: Json by injectLazy()
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseSearchUrl/?tags=$searchLang&sort=rating&apply=&json=&page=$page", headers)
}
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseSearchUrl/?tags=$searchLang&sort=public_date&apply=&json=&page=$page", headers)
}
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
if (num < 0) return minPages to maxPages
return when (query.firstOrNull()) {
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
'=' -> when (query[1]) {
'>' -> limitedNum() to maxPages
'<' -> 1 to limitedNum(maxPages)
else -> limitedNum() to limitedNum()
}
else -> limitedNum() to limitedNum()
}
}
override fun searchMangaParse(response: Response): MangasPage {
val library = response.parseAs<ArchiveResponse>()
val mangas = library.archives.map(LongArchive::toSManga)
val hasNextPage = library.has_next
return MangasPage(mangas, hasNextPage)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseSearchUrl.toHttpUrl().newBuilder().apply {
val tags = mutableListOf<String>()
var reason = ""
var uploader = ""
var pagesMin = 1
var pagesMax = 9999
tags.add(searchLang)
filters.forEach {
when (it) {
is SortFilter -> {
addQueryParameter("sort", it.getValue())
addQueryParameter("asc_desc", if (it.state!!.ascending) "asc" else "desc")
}
is SelectFilter -> {
addQueryParameter("category", it.vals[it.state].replace("All", ""))
}
is PageFilter -> {
if (it.state.isNotBlank()) {
val (min, max) = parsePageRange(it.state)
pagesMin = min
pagesMax = max
}
}
is TextFilter -> {
if (it.state.isNotEmpty()) {
when (it.type) {
"reason" -> reason = it.state
"uploader" -> uploader = it.state
else -> {
it.state.split(",").filter(String::isNotBlank).map { tag ->
val trimmed = tag.trim()
tags.add(
buildString {
if (trimmed.startsWith('-')) append("-")
append(it.type)
if (it.type.isNotBlank()) append(":")
append(trimmed.lowercase().removePrefix("-"))
},
)
}
}
}
}
}
else -> {}
}
}
addQueryParameter("title", query)
addQueryParameter("tags", tags.joinToString())
addQueryParameter("filecount_from", pagesMin.toString())
addQueryParameter("filecount_to", pagesMax.toString())
addQueryParameter("reason", reason)
addQueryParameter("uploader", uploader)
addQueryParameter("page", page.toString())
addQueryParameter("apply", "")
addQueryParameter("json", "")
}.build()
return GET(url, headers)
}
override fun chapterListRequest(manga: SManga): Request {
return GET("$baseUrl/api?archive=${manga.url}", headers)
}
override fun getFilterList() = getFilters()
// Details
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.just(manga.apply { initialized = true })
}
// Chapters
override fun chapterListParse(response: Response): List<SChapter> {
val archive = response.parseAs<Archive>()
return listOf(
SChapter.create().apply {
name = "Chapter"
url = archive.download.substringBefore("/download/")
date_upload = archive.posted * 1000
},
)
}
override fun getMangaUrl(manga: SManga) = "$baseUrl/archive/${manga.url}"
override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}"
// Pages
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
fun List<String>.sort() = this.sortedWith(compareBy(CASE_INSENSITIVE_ORDER) { it })
val url = "$baseUrl${chapter.url}/download/"
val (fileType, contentLength) = getZipType(url)
val remoteZip = ZipHandler(url, client, headers, fileType, contentLength).populate()
val fileListing = remoteZip.files().sort()
val files = remoteZip.toJson()
return Observable.just(
fileListing.mapIndexed { index, filename ->
Page(index, imageUrl = "https://127.0.0.1/#$filename&$files")
},
)
}
private fun getZipType(url: String): Pair<String, BigInteger> {
val request = Request.Builder()
.url(url)
.headers(headers)
.method("HEAD", null)
.build()
val contentLength = (
client.newCall(request).execute().header("content-length")
?: throw Exception("Could not get Content-Length of URL")
)
.toBigInteger()
return (if (contentLength > Int.MAX_VALUE.toBigInteger()) "zip64" else "zip") to contentLength
}
private fun Intercept(chain: Interceptor.Chain): Response {
val url = chain.request().url.toString()
return if (url.startsWith("https://127.0.0.1/#")) {
val fragment = url.toHttpUrl().fragment!!
val remoteZip = fragment.substringAfter("&").parseAs<Zip>()
val filename = fragment.substringBefore("&")
val byteArray = remoteZip.fetch(filename, client)
var type = filename.substringAfterLast('.').lowercase()
type = if (type == "jpg") "jpeg" else type
Response.Builder().body(byteArray.toResponseBody("image/$type".toMediaType()))
.request(chain.request())
.protocol(Protocol.HTTP_1_0)
.code(200)
.message("")
.build()
} else {
chain.proceed(chain.request())
}
}
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private inline fun <reified T> String.parseAs(): T {
return json.decodeFromString(this)
}
private fun Zip.toJson(): String {
return json.encodeToString(this)
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
}

View File

@ -0,0 +1,102 @@
package eu.kanade.tachiyomi.extension.all.pandachaika
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags: List<String>): String {
return tags.filter { it.startsWith("$include:") && exclude.none { substring -> it.startsWith("$substring:") } }
.joinToString {
it.substringAfter(":").replace("_", " ").split(" ").joinToString(" ") { s ->
s.replaceFirstChar { sr ->
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
}
}
}
}
fun getReadableSize(bytes: Double): String {
return when {
bytes >= 300 * 1024 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0 * 1024.0))} GB"
bytes >= 100 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0))} MB"
bytes >= 1024 -> "${"%.2f".format(bytes / (1024.0))} KB"
else -> "$bytes B"
}
}
@Serializable
class Archive(
val download: String,
val posted: Long,
)
@Serializable
class LongArchive(
private val thumbnail: String,
private val title: String,
private val id: Int,
private val posted: Long?,
private val public_date: Long?,
private val filecount: Int,
private val filesize: Double,
private val tags: List<String>,
private val title_jpn: String?,
private val uploader: String,
) {
fun toSManga() = SManga.create().apply {
val groups = filterTags("group", tags = tags)
val artists = filterTags("artist", tags = tags)
val publishers = filterTags("publisher", tags = tags)
val male = filterTags("male", tags = tags)
val female = filterTags("female", tags = tags)
val others = filterTags(exclude = listOf("female", "male", "artist", "publisher", "group", "parody"), tags = tags)
val parodies = filterTags("parody", tags = tags)
url = id.toString()
title = this@LongArchive.title
thumbnail_url = thumbnail
author = groups.ifEmpty { artists }
artist = artists
genre = listOf(male, female, others).joinToString()
description = buildString {
append("Uploader: ", uploader.ifEmpty { "Anonymous" }, "\n")
publishers.takeIf { it.isNotBlank() }?.let {
append("Publishers: ", it, "\n\n")
}
parodies.takeIf { it.isNotBlank() }?.let {
append("Parodies: ", it, "\n\n")
}
male.takeIf { it.isNotBlank() }?.let {
append("Male tags: ", it, "\n\n")
}
female.takeIf { it.isNotBlank() }?.let {
append("Female tags: ", it, "\n\n")
}
others.takeIf { it.isNotBlank() }?.let {
append("Other tags: ", it, "\n\n")
}
title_jpn?.let { append("Japanese Title: ", it, "\n") }
append("Pages: ", filecount, "\n")
append("File Size: ", getReadableSize(filesize), "\n")
try {
append("Public Date: ", dateReformat.format(Date(public_date!! * 1000)), "\n")
} catch (_: Exception) {}
try {
append("Posted: ", dateReformat.format(Date(posted!! * 1000)), "\n")
} catch (_: Exception) {}
}
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
initialized = true
}
}
@Serializable
class ArchiveResponse(
val archives: List<LongArchive>,
val has_next: Boolean,
)

View File

@ -0,0 +1,29 @@
package eu.kanade.tachiyomi.extension.all.pandachaika
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class PandaChaikaFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
PandaChaika(),
PandaChaika("en", "english"),
PandaChaika("zh", "chinese"),
PandaChaika("ko", "korean"),
PandaChaika("es", "spanish"),
PandaChaika("ru", "russian"),
PandaChaika("pt", "portuguese"),
PandaChaika("fr", "french"),
PandaChaika("th", "thai"),
PandaChaika("vi", "vietnamese"),
PandaChaika("ja", "japanese"),
PandaChaika("id", "indonesian"),
PandaChaika("ar", "arabic"),
PandaChaika("uk", "ukrainian"),
PandaChaika("tr", "turkish"),
PandaChaika("cs", "czech"),
PandaChaika("tl", "tagalog"),
PandaChaika("fi", "finnish"),
PandaChaika("jv", "javanese"),
PandaChaika("el", "greek"),
)
}

View File

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.extension.all.pandachaika
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.Filter.Sort.Selection
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
SortFilter("Sort by", Selection(0, false), getSortsList),
SelectFilter("Types", getTypes),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
Filter.Header("Use 'Male Tags' or 'Female Tags' for specific categories. 'Tags' searches all categories."),
TextFilter("Tags", ""),
TextFilter("Male Tags", "male"),
TextFilter("Female Tags", "female"),
TextFilter("Artists", "artist"),
TextFilter("Parodies", "parody"),
Filter.Separator(),
TextFilter("Reason", "reason"),
TextFilter("Uploader", "reason"),
Filter.Separator(),
Filter.Header("Filter by pages, for example: (>20)"),
PageFilter("Pages"),
)
}
internal open class PageFilter(name: String) : Filter.Text(name)
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
internal open class SelectFilter(name: String, val vals: List<String>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it }.toTypedArray(), state)
internal open class SortFilter(name: String, selection: Selection, private val vals: List<Pair<String, String>>) :
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
fun getValue() = vals[state!!.index].second
}
private val getTypes = listOf(
"All",
"Doujinshi",
"Manga",
"Image Set",
"Artist CG",
"Game CG",
"Western",
"Non-H",
"Misc",
)
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Public Date", "public_date"),
Pair("Posted Date", "posted_date"),
Pair("Title", "title"),
Pair("Japanese Title", "title_jpn"),
Pair("Rating", "rating"),
Pair("Images", "images"),
Pair("File Size", "size"),
Pair("Category", "category"),
)

View File

@ -0,0 +1,287 @@
package eu.kanade.tachiyomi.extension.all.pandachaika
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.inflateRaw
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseAllCDs
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseEOCD
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseEOCD64
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseLocalFile
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.Serializable
import okhttp3.Headers
import okhttp3.OkHttpClient
import java.io.ByteArrayOutputStream
import java.math.BigInteger
import java.nio.ByteBuffer
import java.nio.ByteOrder.LITTLE_ENDIAN
import java.util.zip.Inflater
import kotlin.text.Charsets.UTF_8
const val CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE = 0x02014b50
const val END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50
const val END_OF_CENTRAL_DIRECTORY_64_SIGNATURE = 0x06064b50
const val LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50
class EndOfCentralDirectory(
val centralDirectoryByteSize: BigInteger,
val centralDirectoryByteOffset: BigInteger,
)
@Serializable
class CentralDirectoryRecord(
val length: Int,
val compressedSize: Int,
val localFileHeaderRelativeOffset: Int,
val filename: String,
)
class LocalFileHeader(
val compressedData: ByteArray,
val compressionMethod: Int,
)
@Serializable
class Zip(
private val url: String,
private val centralDirectoryRecords: List<CentralDirectoryRecord>,
) {
fun files(): List<String> {
return centralDirectoryRecords.map {
it.filename
}
}
fun fetch(path: String, client: OkHttpClient): ByteArray {
val file = centralDirectoryRecords.find { it.filename == path }
?: throw Exception("File not found in ZIP: $path")
val MAX_LOCAL_FILE_HEADER_SIZE = 256 + 32 + 30 + 100
val headersBuilder = Headers.Builder()
.set(
"Range",
"bytes=${file.localFileHeaderRelativeOffset}-${
file.localFileHeaderRelativeOffset +
file.compressedSize +
MAX_LOCAL_FILE_HEADER_SIZE
}",
).build()
val request = GET(url, headersBuilder)
val response = client.newCall(request).execute()
val byteArray = response.body.byteStream().use { it.readBytes() }
val localFile = parseLocalFile(byteArray, file.compressedSize)
?: throw Exception("Failed to parse local file header in ZIP")
return if (localFile.compressionMethod == 0) {
localFile.compressedData
} else {
inflateRaw(localFile.compressedData)
}
}
}
class ZipHandler(
private val url: String,
private val client: OkHttpClient,
private val additionalHeaders: Headers = Headers.Builder().build(),
private val zipType: String = "zip",
private val contentLength: BigInteger,
) {
fun populate(): Zip {
val endOfCentralDirectory = fetchEndOfCentralDirectory(contentLength, zipType)
val centralDirectoryRecords = fetchCentralDirectoryRecords(endOfCentralDirectory)
return Zip(
url,
centralDirectoryRecords,
)
}
private fun fetchEndOfCentralDirectory(zipByteLength: BigInteger, zipType: String): EndOfCentralDirectory {
val EOCD_MAX_BYTES = 128.toBigInteger()
val eocdInitialOffset = maxOf(0.toBigInteger(), zipByteLength - EOCD_MAX_BYTES)
val headers = additionalHeaders
.newBuilder()
.set("Range", "bytes=$eocdInitialOffset-$zipByteLength")
.build()
val request = GET(url, headers)
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
throw Exception("Could not fetch ZIP: HTTP status ${response.code}")
}
val eocdBuffer = response.body.byteStream().use { it.readBytes() }
if (eocdBuffer.isEmpty()) throw Exception("Could not get Range request to start looking for EOCD")
val eocd =
(if (zipType == "zip64") parseEOCD64(eocdBuffer) else parseEOCD(eocdBuffer))
?: throw Exception("Could not get EOCD record of the ZIP")
return eocd
}
private fun fetchCentralDirectoryRecords(endOfCentralDirectory: EndOfCentralDirectory): List<CentralDirectoryRecord> {
val headersBuilder = Headers.Builder()
.set(
"Range",
"bytes=${endOfCentralDirectory.centralDirectoryByteOffset}-${
endOfCentralDirectory.centralDirectoryByteOffset +
endOfCentralDirectory.centralDirectoryByteSize
}",
).build()
val request = GET(url, headersBuilder)
val response = client.newCall(request).execute()
val cdBuffer = response.body.byteStream().use { it.readBytes() }
return parseAllCDs(cdBuffer)
}
}
object ZipParser {
fun parseAllCDs(buffer: ByteArray): List<CentralDirectoryRecord> {
val cds = ArrayList<CentralDirectoryRecord>()
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
var i = 0
while (i <= buffer.size - 4) {
val signature = view.getInt(i)
if (signature == CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE) {
val cd = parseCD(buffer.sliceArray(i until buffer.size))
if (cd != null) {
cds.add(cd)
i += cd.length - 1
continue
}
} else if (signature == END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
break
}
i++
}
return cds
}
fun parseCD(buffer: ByteArray): CentralDirectoryRecord? {
val MIN_CD_LENGTH = 46
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
for (i in 0..buffer.size - MIN_CD_LENGTH) {
if (view.getInt(i) == CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE) {
val filenameLength = view.getShort(i + 28).toInt()
val extraFieldLength = view.getShort(i + 30).toInt()
val fileCommentLength = view.getShort(i + 32).toInt()
return CentralDirectoryRecord(
length = 46 + filenameLength + extraFieldLength + fileCommentLength,
compressedSize = view.getInt(i + 20),
localFileHeaderRelativeOffset = view.getInt(i + 42),
filename = buffer.sliceArray(i + 46 until i + 46 + filenameLength).toString(UTF_8),
)
}
}
return null
}
fun parseEOCD(buffer: ByteArray): EndOfCentralDirectory? {
val MIN_EOCD_LENGTH = 22
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
for (i in 0 until buffer.size - MIN_EOCD_LENGTH + 1) {
if (view.getInt(i) == END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
return EndOfCentralDirectory(
centralDirectoryByteSize = view.getInt(i + 12).toBigInteger(),
centralDirectoryByteOffset = view.getInt(i + 16).toBigInteger(),
)
}
}
return null
}
fun parseEOCD64(buffer: ByteArray): EndOfCentralDirectory? {
val MIN_EOCD_LENGTH = 56
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
for (i in 0 until buffer.size - MIN_EOCD_LENGTH + 1) {
if (view.getInt(i) == END_OF_CENTRAL_DIRECTORY_64_SIGNATURE) {
return EndOfCentralDirectory(
centralDirectoryByteSize = view.getLong(i + 40).toBigInteger(),
centralDirectoryByteOffset = view.getLong(i + 48).toBigInteger(),
)
}
}
return null
}
fun parseLocalFile(buffer: ByteArray, compressedSizeOverride: Int = 0): LocalFileHeader? {
val MIN_LOCAL_FILE_LENGTH = 30
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
for (i in 0..buffer.size - MIN_LOCAL_FILE_LENGTH) {
if (view.getInt(i) == LOCAL_FILE_HEADER_SIGNATURE) {
val filenameLength = view.getShort(i + 26).toInt() and 0xFFFF
val extraFieldLength = view.getShort(i + 28).toInt() and 0xFFFF
val bitflags = view.getShort(i + 6).toInt() and 0xFFFF
val hasDataDescriptor = (bitflags shr 3) and 1 != 0
val headerEndOffset = i + 30 + filenameLength + extraFieldLength
val regularCompressedSize = view.getInt(i + 18)
val compressedData = if (hasDataDescriptor) {
buffer.copyOfRange(
headerEndOffset,
headerEndOffset + compressedSizeOverride,
)
} else {
buffer.copyOfRange(
headerEndOffset,
headerEndOffset + regularCompressedSize,
)
}
return LocalFileHeader(
compressedData = compressedData,
compressionMethod = view.getShort(i + 8).toInt(),
)
}
}
return null
}
fun inflateRaw(compressedData: ByteArray): ByteArray {
val inflater = Inflater(true)
inflater.setInput(compressedData)
val buffer = ByteArray(8192)
val output = ByteArrayOutputStream()
try {
while (!inflater.finished()) {
val count = inflater.inflate(buffer)
if (count > 0) {
output.write(buffer, 0, count)
}
}
} catch (e: Exception) {
throw Exception("Invalid compressed data format: ${e.message}", e)
} finally {
inflater.end()
output.close()
}
return output.toByteArray()
}
}

View File

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

View File

@ -7,27 +7,33 @@ 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.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
abstract class Pururin(
override val lang: String = "all",
private val searchLang: String? = null,
private val searchLang: Pair<String, String>? = null,
private val langPath: String = "",
) : ParsedHttpSource() {
override val name = "Pururin"
override val baseUrl = "https://pururin.to"
final override val baseUrl = "https://pururin.to"
override val supportsLatest = true
override val client = network.cloudflareClient
// Popular
private val json: Json by injectLazy()
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers)
}
@ -45,7 +51,6 @@ abstract class Pururin(
override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]"
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/browse$langPath?page=$page", headers)
}
@ -58,40 +63,131 @@ abstract class Pururin(
// Search
private fun List<String>.toValue(): String {
return "[${this.joinToString(",")}]"
private fun List<Pair<String, String>>.toValue(): String {
return "[${this.joinToString(",") { "{\"id\":${it.first},\"name\":\"${it.second}\"}" }}]"
}
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
if (num < 0) return minPages to maxPages
return when (query.firstOrNull()) {
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
'=' -> when (query[1]) {
'>' -> limitedNum() to maxPages
'<' -> 1 to limitedNum(maxPages)
else -> limitedNum() to limitedNum()
}
else -> limitedNum() to limitedNum()
}
}
@Serializable
class Tag(
val id: Int,
val name: String,
)
private fun findTagByNameSubstring(tags: List<Tag>, substring: String): Pair<String, String>? {
val tag = tags.find { it.name.contains(substring, ignoreCase = true) }
return tag?.let { Pair(tag.id.toString(), tag.name) }
}
private fun tagSearch(tag: String, type: String): Pair<String, String>? {
val requestBody = FormBody.Builder()
.add("text", tag)
.build()
val request = Request.Builder()
.url("$baseUrl/api/get/tags/search")
.headers(headers)
.post(requestBody)
.build()
val response = client.newCall(request).execute()
return findTagByNameSubstring(response.parseAs<List<Tag>>(), type)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val includeTags = mutableListOf<String>()
val excludeTags = mutableListOf<String>()
var pagesMin: Int
var pagesMax: Int
val includeTags = mutableListOf<Pair<String, String>>()
val excludeTags = mutableListOf<Pair<String, String>>()
var pagesMin = 1
var pagesMax = 9999
var sortBy = "newest"
if (searchLang != null) includeTags.add(searchLang)
filters.filterIsInstance<TagGroup<*>>().map { group ->
group.state.map {
if (it.isIncluded()) includeTags.add(it.id)
if (it.isExcluded()) excludeTags.add(it.id)
filters.forEach {
when (it) {
is SelectFilter -> sortBy = it.getValue()
is TypeFilter -> {
val (_, inactiveFilters) = it.state.partition { stIt -> stIt.state }
excludeTags += inactiveFilters.map { fil -> Pair(fil.value, "${fil.name} [Category]") }
}
is PageFilter -> {
if (it.state.isNotEmpty()) {
val (min, max) = parsePageRange(it.state)
pagesMin = min
pagesMax = max
}
}
is TextFilter -> {
if (it.state.isNotEmpty()) {
it.state.split(",").filter(String::isNotBlank).map { tag ->
val trimmed = tag.trim()
if (trimmed.startsWith('-')) {
tagSearch(trimmed.lowercase().removePrefix("-"), it.type)?.let { tagInfo ->
excludeTags.add(tagInfo)
}
} else {
tagSearch(trimmed.lowercase(), it.type)?.let { tagInfo ->
includeTags.add(tagInfo)
}
}
}
}
}
else -> {}
}
}
filters.find<PagesGroup>().range.let {
pagesMin = it.first
pagesMax = it.last
// Searching with just one tag usually gives wrong results
if (query.isEmpty()) {
when {
excludeTags.size == 1 && includeTags.isEmpty() -> excludeTags.addAll(excludeTags)
includeTags.size == 1 && excludeTags.isEmpty() -> {
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("browse")
addPathSegment("tags")
addPathSegment("content")
addPathSegment(includeTags[0].first)
addQueryParameter("sort", sortBy)
addQueryParameter("start_page", pagesMin.toString())
addQueryParameter("last_page", pagesMax.toString())
if (page > 1) addQueryParameter("page", page.toString())
}.build()
return GET(url, headers)
}
}
}
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("search")
addQueryParameter("q", query)
addQueryParameter("sort", sortBy)
addQueryParameter("start_page", pagesMin.toString())
addQueryParameter("last_page", pagesMax.toString())
if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
if (page > 1) addQueryParameter("page", page.toString())
}
return GET(url.build().toString(), headers)
}.build()
return GET(url, headers)
}
override fun searchMangaSelector(): String = popularMangaSelector()
@ -107,8 +203,13 @@ abstract class Pururin(
document.select(".box-gallery").let { e ->
initialized = true
title = e.select(".title").text()
author = e.select("[itemprop=author]").text()
author = e.select("a[href*=/circle/]").text().ifEmpty { e.select("[itemprop=author]").text() }
artist = e.select("[itemprop=author]").text()
genre = e.select("a[href*=/content/]").text()
description = e.select(".box-gallery .table-info tr")
.filter { tr ->
tr.select("td").none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
}
.joinToString("\n") { tr ->
tr.select("td")
.joinToString(": ") { it.text() }
@ -156,8 +257,8 @@ abstract class Pururin(
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun getFilterList() = FilterList(
CategoryGroup(),
PagesGroup(),
)
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
override fun getFilterList() = getFilters()
}

View File

@ -14,11 +14,11 @@ class PururinFactory : SourceFactory {
class PururinAll : Pururin()
class PururinEN : Pururin(
"en",
"{\"id\":13010,\"name\":\"English [Language]\"}",
Pair("13010", "english"),
"/tags/language/13010/english",
)
class PururinJA : Pururin(
"ja",
"{\"id\":13011,\"name\":\"Japanese [Language]\"}",
Pair("13011", "japanese"),
"/tags/language/13011/japanese",
)

View File

@ -1,57 +1,57 @@
package eu.kanade.tachiyomi.extension.all.pururin
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
sealed class TagFilter(
name: String,
val id: String,
) : Filter.TriState(name)
sealed class TagGroup<T : TagFilter>(
name: String,
values: List<T>,
) : Filter.Group<T>(name, values)
class Category(name: String, id: String) : TagFilter(name, id)
class CategoryGroup(
values: List<Category> = categories,
) : TagGroup<Category>("Categories", values) {
companion object {
private val categories get() = listOf(
Category("Doujinshi", "{\"id\":13003,\"name\":\"Doujinshi [Category]\"}"),
Category("Manga", "{\"id\":13004,\"name\":\"Manga [Category]\"}"),
Category("Artist CG", "{\"id\":13006,\"name\":\"Artist CG [Category]\"}"),
Category("Game CG", "{\"id\":13008,\"name\":\"Game CG [Category]\"}"),
Category("Artbook", "{\"id\":17783,\"name\":\"Artbook [Category]\"}"),
Category("Webtoon", "{\"id\":27939,\"name\":\"Webtoon [Category]\"}"),
)
}
fun getFilters(): FilterList {
return FilterList(
SelectFilter("Sort by", getSortsList),
TypeFilter("Types"),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TextFilter("Tags", "[Content]"),
TextFilter("Artists", "[Artist]"),
TextFilter("Circles", "[Circle]"),
TextFilter("Parodies", "[Parody]"),
TextFilter("Languages", "[Language]"),
TextFilter("Scanlators", "[Scanlator]"),
TextFilter("Conventions", "[Convention]"),
TextFilter("Collections", "[Collections]"),
TextFilter("Categories", "[Category]"),
TextFilter("Uploaders", "[Uploader]"),
Filter.Separator(),
Filter.Header("Filter by pages, for example: (>20)"),
PageFilter("Pages"),
)
}
internal class TypeFilter(name: String) :
Filter.Group<CheckBoxFilter>(
name,
listOf(
Pair("Artbook", "17783"),
Pair("Artist CG", "13004"),
Pair("Doujinshi", "13003"),
Pair("Game CG", "13008"),
Pair("Manga", "13004"),
Pair("Webtoon", "27939"),
).map { CheckBoxFilter(it.first, it.second, true) },
)
class PagesFilter(
name: String,
default: Int,
values: Array<Int> = range,
) : Filter.Select<Int>(name, values, default) {
companion object {
private val range get() = Array(301) { it }
}
internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state)
internal open class PageFilter(name: String) : Filter.Text(name)
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
internal open class SelectFilter(name: String, val vals: List<Pair<String, String>>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
fun getValue() = vals[state].second
}
class PagesGroup(
values: List<PagesFilter> = minmax,
) : Filter.Group<PagesFilter>("Pages", values) {
inline val range get() = IntRange(state[0].state, state[1].state).also {
require(it.first <= it.last) { "'Minimum' cannot exceed 'Maximum'" }
}
companion object {
private val minmax get() = listOf(
PagesFilter("Minimum", 0),
PagesFilter("Maximum", 300),
)
}
}
inline fun <reified T> List<Filter<*>>.find() = find { it is T } as T
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Newest", "newest"),
Pair("Most Popular", "most-popular"),
Pair("Highest Rated", "highest-rated"),
Pair("Most Viewed", "most-viewed"),
Pair("Title", "title"),
)

View File

@ -1,7 +1,7 @@
ext {
extName = 'Union Mangas'
extClass = '.UnionMangasFactory'
extVersionCode = 3
extVersionCode = 5
isNsfw = true
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.all.unionmangas
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
@ -10,22 +9,16 @@ 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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.TimeUnit
class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
override val lang = langOption.lang
@ -38,39 +31,12 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
private val json: Json by injectLazy()
val langApiInfix = when (lang) {
"it" -> langOption.infix
else -> "v3/po"
}
override val client = network.client.newBuilder()
.rateLimit(5, 2, TimeUnit.SECONDS)
.rateLimit(2)
.build()
private fun apiHeaders(url: String): Headers {
val date = apiDateFormat.format(Date())
val path = url.toUrlWithoutDomain()
return headersBuilder()
.add("_hash", authorization(apiSeed, domain, date))
.add("_tranId", authorization(apiSeed, domain, date, path))
.add("_date", date)
.add("_domain", domain)
.add("_path", path)
.add("Origin", baseUrl)
.add("Host", apiUrl.removeProtocol())
.add("Referer", "$baseUrl/")
.build()
}
private fun authorization(vararg payloads: String): String {
val md = MessageDigest.getInstance("MD5")
val bytes = payloads.joinToString("").toByteArray()
val digest = md.digest(bytes)
return digest
.fold("") { str, byte -> str + "%02x".format(byte) }
.padStart(32, '0')
}
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.set("Referer", "$baseUrl/")
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
@ -79,95 +45,101 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
var currentPage = 0
do {
val chaptersDto = fetchChapterListPageable(manga, currentPage)
chapters += chaptersDto.toSChapter(langOption)
chapters += chaptersDto.data.map { chapter ->
SChapter.create().apply {
name = chapter.name
date_upload = chapter.date.toDate()
url = chapter.toChapterUrl(langOption.infix)
}
}
currentPage++
} while (chaptersDto.hasNextPage())
return Observable.just(chapters.reversed())
return Observable.just(chapters)
}
private fun fetchChapterListPageable(manga: SManga, page: Int): ChapterPageDto {
private fun fetchChapterListPageable(manga: SManga, page: Int): Pageable<ChapterDto> {
manga.apply {
url = getURLCompatibility(url)
}
val maxResult = 16
val url = "$apiUrl/api/$langApiInfix/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/ASC"
return client.newCall(GET(url, apiHeaders(url))).execute()
.parseAs<ChapterPageDto>()
val url = "$apiUrl/${langOption.infix}/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/ASC"
return client.newCall(GET(url, headers)).execute()
.parseAs<Pageable<ChapterDto>>()
}
override fun latestUpdatesParse(response: Response): MangasPage {
val nextData = response.parseNextData<LatestUpdateProps>()
val dto = nextData.data.latestUpdateDto
val mangas = dto.mangas.map { mangaParse(it, nextData.query) }
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun latestUpdatesRequest(page: Int): Request {
val maxResult = 24
val url = "$apiUrl/${langOption.infix}/HomeLastUpdate".toHttpUrl().newBuilder()
.addPathSegment("$maxResult")
.addPathSegment("${page - 1}")
.build()
return GET(url, headers)
}
override fun getMangaUrl(manga: SManga): String {
manga.apply {
url = getURLCompatibility(url)
}
return baseUrl + manga.url.replace(langOption.infix, langOption.mangaSubstring)
}
override fun mangaDetailsRequest(manga: SManga): Request {
manga.apply {
url = getURLCompatibility(url)
}
val url = "$apiUrl/${langOption.infix}/getInfoManga".toHttpUrl().newBuilder()
.addPathSegment(manga.slug())
.build()
return GET(url, headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val dto = response.parseAs<MangaDetailsDto>()
return mangaParse(dto.details)
}
override fun pageListRequest(chapter: SChapter): Request {
val chapterSlug = getURLCompatibility(chapter.url)
.substringAfter(langOption.infix)
val url = "$apiUrl/${langOption.infix}/GetImageChapter$chapterSlug"
return GET(url, headers)
}
override fun pageListParse(response: Response): List<Page> {
val location = response.request.url.toString()
val dto = response.parseAs<PageDto>()
return dto.pages.mapIndexed { index, url ->
Page(index, location, imageUrl = url)
}
}
override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseAs<Pageable<MangaDto>>()
val mangas = dto.data.map(::mangaParse)
return MangasPage(
mangas = mangas,
hasNextPage = dto.hasNextPage(),
)
}
override fun latestUpdatesRequest(page: Int): Request {
val url = "$baseUrl/${langOption.infix}/latest-releases".toHttpUrl().newBuilder()
.addQueryParameter("page", "$page")
.build()
return GET(url, headers)
override fun popularMangaRequest(page: Int): Request {
val maxResult = 24
return GET("$apiUrl/${langOption.infix}/HomeTopFllow/$maxResult/${page - 1}")
}
override fun mangaDetailsParse(response: Response): SManga {
val nextData = response.parseNextData<MangaDetailsProps>()
val dto = nextData.data.mangaDetailsDto
return SManga.create().apply {
title = dto.title
genre = dto.genres
thumbnail_url = dto.thumbnailUrl
url = mangaUrlParse(dto.slug, nextData.query.type)
status = dto.status
}
}
override fun pageListParse(response: Response): List<Page> {
val chaptersDto = decryptChapters(response)
return chaptersDto.images.mapIndexed { index, imageUrl ->
Page(index, imageUrl = imageUrl)
}
}
private fun decryptChapters(response: Response): ChaptersDto {
val document = response.asJsoup()
val password = findChapterPassword(document)
val pageListData = document.parseNextData<ChaptersProps>().data.pageListData
val decodedData = CryptoAES.decrypt(pageListData, password)
return ChaptersDto(
data = json.decodeFromString<ChaptersDto>(decodedData).data,
delimiter = langOption.pageDelimiter,
)
}
private fun findChapterPassword(document: Document): String {
val regxPasswordUrl = """\/pages\/%5Btype%5D\/%5Bidmanga%5D\/%5Biddetail%5D-.+\.js""".toRegex()
val regxFindPassword = """AES\.decrypt\(\w+,"(?<password>[^"]+)"\)""".toRegex(RegexOption.MULTILINE)
val jsDecryptUrl = document.select("script")
.map { it.absUrl("src") }
.first { regxPasswordUrl.find(it) != null }
val jsDecrypt = client.newCall(GET(jsDecryptUrl, headers)).execute().asJsoup().html()
return regxFindPassword.find(jsDecrypt)?.groups?.get("password")!!.value.trim()
}
override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseNextData<PopularMangaProps>()
val mangas = dto.data.mangas.map { it.details }.map { mangaParse(it, dto.query) }
return MangasPage(
mangas = mangas,
hasNextPage = false,
)
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/${langOption.infix}")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val maxResult = 6
val url = "$apiUrl/api/$langApiInfix/searchforms/$maxResult/".toHttpUrl().newBuilder()
val maxResult = 20
val url = "$apiUrl/${langOption.infix}/QuickSearch/".toHttpUrl().newBuilder()
.addPathSegment(query)
.addPathSegment("${page - 1}")
.addPathSegment("$maxResult")
.build()
return GET(url, apiHeaders(url.toString()))
return GET(url, headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
@ -185,52 +157,54 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
override fun imageUrlParse(response: Response): String = ""
override fun searchMangaParse(response: Response): MangasPage {
val mangasDto = response.parseAs<MangaListDto>().apply {
currentPage = response.request.url.pathSegments.last()
}
val dto = response.parseAs<SearchDto>()
return MangasPage(
mangas = mangasDto.toSManga(langOption.infix),
hasNextPage = mangasDto.hasNextPage(),
dto.mangas.map(::mangaParse),
false,
)
}
private inline fun <reified T> Response.parseNextData() = asJsoup().parseNextData<T>()
/*
* Keeps compatibility with pt-BR previous version
* */
private fun getURLCompatibility(url: String): String {
val slugSuffix = "-br"
val mangaSubString = "manga-br"
private inline fun <reified T> Document.parseNextData(): NextData<T> {
val jsonContent = selectFirst("script#__NEXT_DATA__")!!.html()
return json.decodeFromString<NextData<T>>(jsonContent)
val oldSlug = url.substringAfter(mangaSubString)
.substring(1)
.split("/")
.first()
val newSlug = oldSlug.substringBeforeLast(slugSuffix)
return url.replace(oldSlug, newSlug)
}
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private fun String.removeProtocol() = trim().replace("https://", "")
private fun SManga.slug() = this.url.split("/").last()
private fun String.toUrlWithoutDomain() = trim().replace(apiUrl, "")
private fun mangaParse(dto: MangaDto, query: QueryDto): SManga {
private fun mangaParse(dto: MangaDto): SManga {
return SManga.create().apply {
title = dto.title
thumbnail_url = dto.thumbnailUrl
status = dto.status
url = mangaUrlParse(dto.slug, query.type)
url = "/${langOption.infix}/${dto.slug}"
genre = dto.genres
initialized = true
}
}
private fun mangaUrlParse(slug: String, pathSegment: String) = "/$pathSegment/$slug"
private fun String.toDate(): Long =
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
companion object {
const val SEARCH_PREFIX = "slug:"
val apiUrl = "https://api.unionmanga.xyz"
val apiSeed = "8e0550790c94d6abc71d738959a88d209690dc86"
val domain = "yaoi-chan.xyz"
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
val apiDateFormat = SimpleDateFormat("EE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH)
.apply { timeZone = TimeZone.getTimeZone("GMT") }
val apiUrl = "https://app.unionmanga.xyz/api"
val oldApiUrl = "https://api.unionmanga.xyz"
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
}
}

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